Polar Code 🎭

Command Palette

Search for a command to run...

06
Pièce N°06

CHAPITRE 5 : SINGLETON

L’appartement sentait le renfermé et le café froid. Sur l’écran, une ligne de log s’affichait, encore et encore. Nouvelle connexion BD établie. Nouvelle connexion BD établie. La mémoire grimpait en flèche. Une fuite. Une de plus. Le type d’avant avait pensé bien faire. Chaque service ouvrait sa propre connexion. C’était la foire. Le chaos. Le besoin d’un seul point de contrôle, d’une instance unique, était palpable. C’est là qu’il entre en scène. Le Singleton. Le patron. Celui qui dit : « Ici, c’est moi. Un et un seul. »

Problème adressé

Parfois, dans un système, il faut une instance unique d’une classe. Un point d’accès global.

  • Une connexion à une base de données partagée.
  • Un cache d’application.
  • Un logger centralisé qui écrit dans le même fichier.
  • Un gestionnaire de configuration qui charge un fichier une seule fois.
  • Un coordonnateur qui ne doit pas être dupliqué (sous peine de conflits).

Le problème, c’est que l’opérateur new ne garantit pas l’unicité. Il crée. Un point. Laisser les développeurs instancier librement mène à des duplications coûteuses, des incohérences d’état, des conflits de ressources.

// LE CAUCHEMAR : Des instances multiples d'une ressource censée être unique.
class DatabaseConnection {
    constructor() {
        console.log('Coût élevé : Connexion physique ouverte');
    }
    query(sql: string) { /* ... */ }
}

// Dans le service A :
const dbA = new DatabaseConnection(); // "Coût élevé..."
// Dans le service B :
const dbB = new DatabaseConnection(); // "Coût élevé..." AGAIN !
// Deux connexions physiques ? Deux pools ? Deux caches séparés ? Incohérence.

Principe

Le principe du Singleton est simple, brutal :

  1. Garantir qu’une classe n’a qu’une seule instance.
  2. Fournir un point d’accès global à cette instance.

On y parvient en :

  • Rendant le constructeur privé (ou protégé) pour empêcher toute instanciation externe via new.
  • Créant l’instance à l’intérieur même de la classe.
  • Fournissant une méthode statique (souvent appelée getInstance) qui retourne toujours cette même instance.

Implémentation

Voici la forme canonique, la plus basique, en TypeScript.

class Singleton {
    // L'instance unique, stockée en variable statique privée.
    private static instance: Singleton;

    // Le CONSTRUCTEUR EST PRIVÉ. C'est la clé.
    // Personne ne peut faire `new Singleton()`.
    private constructor() {
        // Initialisation potentiellement coûteuse.
        console.log('Singleton créé (une seule fois)');
    }

    // Méthode statique publique, point d'accès global.
    public static getInstance(): Singleton {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }

    // Une méthode métier.
    public someBusinessLogic() {
        console.log('Logique métier exécutée');
    }
}

Utilisation :

// Client code
const s1 = Singleton.getInstance(); // Affiche : "Singleton créé (une seule fois)"
const s2 = Singleton.getInstance(); // N'affiche RIEN. L'instance existe déjà.

s1.someBusinessLogic(); // "Logique métier exécutée"

// Vérification de l'unicité
console.log(s1 === s2); // true. C'est bien la MÊME instance.

Diagramme de classe (au format ASCII simple)

       +---------------------+
       |     Singleton       |
       +---------------------+
       | - instance: Singleton |
       +---------------------+
       | - Singleton()        |
       | + getInstance(): Singleton |
       | + someBusinessLogic() |
       +---------------------+
                ^
                |
      L'appel à getInstance()
      retourne toujours cette
      unique instance statique.

Avantages

  1. Contrôle d’accès strict : Garantit qu’une instance unique existe. Fin des duplications accidentelles.
  2. Accès global pratique : L’instance est accessible de n’importe où dans le code, sans avoir à la passer à travers toute une chaîne de constructeurs (au premier abord, ça semble pratique).
  3. Initialisation paresseuse (Lazy Initialization) : L’instance n’est créée qu’au premier appel de getInstance(). Si personne n’utilise le Singleton, il n’est jamais créé, économisant des ressources.
  4. Économie de ressources : Pour des objets lourds (connexions, gros caches), c’est une nécessité.

Dérives et anti-pattern

C’est ici que le polar vire au film d’horreur. Le Singleton est l’un des patterns les plus mal utilisés. Il devient facilement un anti-pattern global.

  1. État global déguisé : Il introduit un état accessible de partout. C’est le contraire de l’encapsulation. N’importe quel module peut modifier le Singleton, rendant le système imprévisible et difficile à déboguer (“Qui a changé la config ?!”).
  2. Couplage caché : Les classes qui utilisent le Singleton deviennent fortement couplées à une implémentation concrète et globale. C’est l’inverse de l’Inversion de Dépendance (DIP). Les tests unitaires deviennent un enfer car on ne peut pas isoler une classe de son Singleton.
    // Code difficilement testable
    class UserService {
        getUser(id: string) {
            const db = DatabaseSingleton.getInstance(); // COUPLAGE CACHÉ
            return db.query('SELECT ...');
        }
    }
    // Pour tester UserService, je dois *forcer* l'état de DatabaseSingleton.
    // C'est fragile et dépend de l'ordre des tests.
    
  3. Violation du SRP : La classe a désormais deux responsabilités : son métier ET la gestion de son propre cycle de vie.
  4. Problèmes en environnement concurrentiel (threads) : L’implémentation naïve n’est pas thread-safe. Deux threads peuvent appeler getInstance() en même temps avant la création, et créer deux instances. (En JS/TS mono-thread, ce point est moins critique, sauf avec des workers).

Alternatives modernes (DI, Scope)

La programmation moderne a développé des outils pour résoudre les vrais problèmes du Singleton sans ses inconvénients.

  1. Injection de Dépendances (DI) / Conteneur IoC : C’est l’alternative suprême. On déclare qu’un service doit avoir une durée de vie unique (singleton scope) dans un conteneur. Le conteneur se charge de créer l’instance unique et de l’injecter partout où elle est demandée.

    // Avec un framework comme NestJS, Angular, ou InversifyJS
    @Injectable({ scope: Scope.DEFAULT }) // Scope 'singleton' par défaut
    class DatabaseConnection { /* ... */ }
    
    class UserService {
        // Le conteneur INJECTE l'instance unique. Pas de `getInstance`.
        constructor(private db: DatabaseConnection) {}
    
        getUser() {
            return this.db.query('...'); // Testable : on peut mock 'db'.
        }
    }
    

    Avantages : Couplage faible, testabilité parfaite (on injecte un mock), contrôle externe du cycle de vie.

  2. Module ES6 / Objet littéral : Parfois, un simple objet ou un module exporté fait l’affaire sans la complexité d’une classe Singleton.

    // configuration.ts
    export const config = Object.freeze({
        apiUrl: process.env.API_URL,
        maxConnections: 10
    });
    // C'est un "singleton" par la nature du système de modules.
    // Utilisation : `import { config } from './configuration';`
    
  3. Paramétrage explicite : Passer l’instance unique en argument aux fonctions ou constructeurs qui en ont besoin. C’est plus verbeux, mais d’une clarté absolue et parfaitement testable.

    function processData(db: DatabaseConnection, data: any) {
        // La dépendance est EXPLICITE.
    }
    

Je fermai la fenêtre du terminal qui affichait les logs de connexion. Le Singleton, dans son implémentation naïve, était une rustine. Une rustine qui, dans un moment de panique (fuite de mémoire, besoin de contrôle), semblait la seule solution.

Mais comme toutes les rustines, elle pourrit le tissu autour. Elle crée une dépendance silencieuse, un état global, un cauchemar pour les tests.

Le vrai pattern, la leçon à retenir, n’est pas « Comment coder un Singleton ? ». C’est : « Comment garantir l’unicité d’une instance sans créer de couplage global et avec la possibilité de tester ? ».

La réponse moderne est rarement la classe Singleton privée avec getInstance(). La réponse est l’Injection de Dépendances avec un scope singleton. Vous obtenez l’unicité, mais vous gardez le contrôle, la transparence et la testabilité.

Le Singleton du GoF est un vestige. Un indice dans une vieille affaire. Il faut le connaître, comprendre son mécanisme, car on le croise dans du code legacy. Mais pour les nouveaux dossiers, on utilise les outils du métier moderne. On laisse le conteneur gérer l’unicité. On reste propre.

Le prochain pattern serait différent. Plus utile, plus flexible. La Factory Method. Une façon de créer sans dire exactement quoi.