Aller au contenu
  1. Exemples/

Le pattern Singleton - garantir une instance unique

·12 mins· loading · loading · · ·
Conception Back-End
Adrien D'acunto
Auteur
Adrien D’acunto
Sommaire

Le pattern Singleton : garantir une instance unique
#

Introduction
#

Le Singleton est l’un des design patterns les plus connus (et parfois controversés) du Gang of Four. Son objectif est simple : garantir qu’une classe n’a qu’une seule instance dans toute l’application et fournir un point d’accès global à cette instance.

Dans cet article, nous allons explorer le pattern Singleton à travers un exemple concret : une classe Coordinateur qui ne peut exister qu’en un seul exemplaire dans le système.

Qu’est-ce que le Singleton ?
#

Définition
#

Le pattern Singleton assure qu’une classe :

  • N’a qu’une seule instance durant toute la vie de l’application
  • Fournit un point d’accès global à cette instance
  • Contrôle sa propre instanciation via un constructeur privé

Structure UML
#

classDiagram
    class Singleton {
        -static instance: Singleton
        -constructor()
        +static getInstance() Singleton
        +businessMethod()
    }
    
    note for Singleton "Constructeur privé\nInstance statique\nGetter statique"
Caractéristique Description
Constructeur privé Empêche l’instanciation directe avec new
Instance statique Stocke l’unique instance de la classe
Méthode statique getInstance() retourne l’instance unique
Lazy initialization L’instance est créée à la première demande

Cas d’usage : Le coordinateur unique
#

Contexte
#

Imaginons un système de gestion d’équipe où :

  • Un seul coordinateur supervise toute l’équipe
  • Ce coordinateur doit être accessible depuis n’importe où dans l’application
  • Il ne doit jamais y avoir deux coordinateurs simultanément

Problématique sans Singleton
#

// PROBLÈME : Plusieurs instances possibles
const coord1 = new Coordinateur();
const coord2 = new Coordinateur();

// coord1 et coord2 sont deux instances différentes
console.log(coord1 === coord2); // false

Conséquences :

  • Incohérence des données
  • Confusion sur “qui est le vrai coordinateur”
  • Gaspillage de ressources
  • Bugs difficiles à tracer

Implémentation typeScript
#

Architecture complète
#

classDiagram
    class Person {
        <>
        #name: string
        #firstname: string
        #email: string
        #phone: string
        +birthdate: Date
        +gender: string
        -strategy: NameStrategy
        
        +getName() string
        +setName(name: string) void
        +getFirstname() string
        +setFirstname(firstname: string) void
        +getEmail() string
        +setEmail(email: string) void
        +getPhone() string
        +setPhone(phone: string) void
        +getDisplayName() string
        +setStrategy(strategy: NameStrategy) void
        +sayHello()* void
    }
    
    class Coordinateur {
        -static instance: Coordinateur
        -static instanciations: number
        -constructor()
        +static getInstance() Coordinateur
        +sayHello() void
    }
    
    class NameStrategy {
        <>
        +transform(person: Person) string
    }
    
    class NameFirstStrategy {
        +transform(person: Person) string
    }
    
    Person <|-- Coordinateur : hérite
    Person --> NameStrategy : utilise
    NameStrategy <|.. NameFirstStrategy : implémente
    
    note for Coordinateur "Singleton Pattern\nUne seule instance possible"

Classe abstraite Person
#

import { NameFirstStrategy } from "./strategies/name-first-strategy";
import type { NameStrategy } from "./strategies/name-strategy";

export abstract class Person {
    // Attributs protégés (accessibles aux sous-classes)
    protected name: string = '';
    protected firstname: string = '';
    protected email: string = '';
    protected phone: string = '';
    
    // Attributs publics
    public birthdate: Date = new Date();
    public gender: string = '';

    // Pattern Strategy pour le formatage du nom
    private strategy: NameStrategy = new NameFirstStrategy()

    constructor() {}
    
    // Getters et Setters avec validation
    public getName(): string {
        return this.name;
    }

    public setName(name: string): void {
        // Empêche la modification si déjà défini
        if (this.name === '') {
            this.name = name;
        }
    }

    public getFirstname(): string {
        return this.firstname;
    }

    public setFirstname(firstname: string): void {
        if (this.firstname === '') {
            this.firstname = firstname;
        }
    }

    public getEmail(): string {
        return this.email;
    }

    public setEmail(email: string): void {
        if (this.email === '') {
            this.email = email;
        }
    }

    public getPhone(): string {
        return this.phone;
    }

    public setPhone(phone: string): void {
        if (this.phone === '') {
            this.phone = phone;
        }
    }

    // Utilisation du pattern Strategy
    public getDisplayName(): string {
        return this.strategy.transform(this)
    }

    public setStrategy(strategy: NameStrategy): void {
        this.strategy = strategy
    }

    // Méthode abstraite (doit être implémentée par les sous-classes)
    public abstract sayHello(): void;
}

Points clés :

  • Classe abstraite : ne peut pas être instanciée directement
  • Encapsulation : attributs protégés avec getters/setters
  • Validation : empêche la modification des données une fois définies
  • Strategy Pattern : formatage du nom configurable

Classe Singleton Coordinateur
#

import { Person } from "./person"

export class Coordinateur extends Person {

    // Instance statique privée (le cœur du Singleton)
    private static instance: Coordinateur | undefined = undefined
    
    // Compteur optionnel pour tracer les tentatives d'instanciation
    private static instanciations: number = 0

    // Constructeur privé : interdit `new Coordinateur()`
    private constructor() {
        super()
        Coordinateur.instanciations++
    }

    // Méthode statique : seul moyen d'obtenir l'instance
    public static getInstance(): Coordinateur {
        // Lazy initialization : création à la première demande
        if (Coordinateur.instance === undefined) {
            Coordinateur.instance = new Coordinateur()
        }

        return Coordinateur.instance
    }

    // Implémentation de la méthode abstraite
    public sayHello(): void {
        console.log(`Bonjour, je suis ${this.getDisplayName()}, le coordinateur unique !`);
    }
    
    // Méthode utilitaire pour déboguer
    public static getInstanciationCount(): number {
        return Coordinateur.instanciations
    }
}

Stratégie de Formatage (Pattern Strategy)
#

// Interface Strategy
export interface NameStrategy {
    transform(person: Person): string;
}

// Implémentation : Prénom puis Nom
export class NameFirstStrategy implements NameStrategy {
    transform(person: Person): string {
        return `${person.getFirstname()} ${person.getName()}`;
    }
}

// Implémentation alternative : Nom puis Prénom
export class LastNameFirstStrategy implements NameStrategy {
    transform(person: Person): string {
        return `${person.getName()} ${person.getFirstname()}`;
    }
}

Analyse du Code
#

Diagramme de Séquence : Obtention de l’Instance
#

sequenceDiagram
    participant Client1
    participant Client2
    participant Coordinateur
    
    Note over Client1: Première demande
    Client1->>Coordinateur: getInstance()
    
    alt Instance n'existe pas
        Coordinateur->>Coordinateur: new Coordinateur()
        Note over Coordinateur: instance créée
    end
    
    Coordinateur-->>Client1: retourne instance
    
    Note over Client2: Deuxième demande
    Client2->>Coordinateur: getInstance()
    
    alt Instance existe déjà
        Note over Coordinateur: Retourne instance existante
    end
    
    Coordinateur-->>Client2: retourne MÊME instance
    
    Note over Client1,Client2: client1 === client2

Le Constructeur privé : Le gardien
#

// Constructeur privé
private constructor() {
    super()
    Coordinateur.instanciations++
}

Pourquoi privé ?

  • Empêche : new Coordinateur() (erreur de compilation)
  • Force l’utilisation de getInstance()
  • Contrôle total sur l’instanciation

Tentative d’instanciation directe :

// ERREUR : Constructor of class 'Coordinateur' is private
const coord = new Coordinateur();

getInstance() : Le Point d’Accès Unique
#

public static getInstance(): Coordinateur {
    // Vérification de l'existence
    if (Coordinateur.instance === undefined) {
        // Création lazy (à la première demande)
        Coordinateur.instance = new Coordinateur()
    }

    // Retourne toujours la même instance
    return Coordinateur.instance
}

Caractéristiques :

  • Thread-safe en JavaScript (single-threaded)
  • Lazy initialization : création différée
  • Point d’accès unique et contrôlé

Le compteur d’instanciations : Déboggage
#

private static instanciations: number = 0

public static getInstanciationCount(): number {
    return Coordinateur.instanciations
}

Utilité :

  • Vérifier que le constructeur n’est appelé qu’une fois
  • Tracer les tentatives d’instanciation
  • Tests unitaires : valider le comportement Singleton

Utilisation pratique
#

Exemple complet
#

// main.ts
import { Coordinateur } from './coordinateur';
import { LastNameFirstStrategy } from './strategies/last-name-first-strategy';

// Obtenir l'instance unique
const coord1 = Coordinateur.getInstance();

// Configurer le coordinateur
coord1.setFirstname('Jean');
coord1.setName('Dupont');
coord1.setEmail('jean.dupont@example.com');
coord1.birthdate = new Date('1980-05-15');
coord1.gender = 'M';

// Dire bonjour
coord1.sayHello();
// Output : "Bonjour, je suis Jean Dupont, le coordinateur unique !"

// Obtenir à nouveau l'instance (même objet)
const coord2 = Coordinateur.getInstance();

console.log(coord1 === coord2); // true
console.log(coord2.getEmail()); // "jean.dupont@example.com"

// Changer la stratégie de formatage
coord2.setStrategy(new LastNameFirstStrategy());
console.log(coord2.getDisplayName()); // "Dupont Jean"

// Vérifier le nombre d'instanciations
console.log(Coordinateur.getInstanciationCount()); // 1
Tests Unitaires
typescript// coordinateur.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { Coordinateur } from './coordinateur';

describe('Coordinateur Singleton', () => {
  
  it('devrait retourner la même instance', () => {
    const coord1 = Coordinateur.getInstance();
    const coord2 = Coordinateur.getInstance();
    
    expect(coord1).toBe(coord2);
  });
  
  it('ne devrait instancier qu\'une seule fois', () => {
    const coord = Coordinateur.getInstance();
    
    expect(Coordinateur.getInstanciationCount()).toBe(1);
  });
  
  it('devrait partager l\'état entre les références', () => {
    const coord1 = Coordinateur.getInstance();
    coord1.setFirstname('Alice');
    
    const coord2 = Coordinateur.getInstance();
    
    expect(coord2.getFirstname()).toBe('Alice');
  });
  
  it('ne devrait pas permettre new Coordinateur()', () => {
    // TypeScript compile-time check
    // @ts-expect-error - Constructor is private
    expect(() => new Coordinateur()).toThrow();
  });
});

Avantages et inconvénients
#

Avantages
#

Avantage Description Exemple
Instance unique garantie Impossible de créer plusieurs instances Un seul coordinateur
Accès global Accessible depuis n’importe où Coordinateur.getInstance()
Contrôle strict Initialisation maîtrisée (Lazy loading)
Économie de ressources Une seule instance en mémoire Optimisation mémoire
État partagé Toutes les parties partagent les mêmes données Configuration globale

Inconvénients
#

Inconvénient Description Impact
État global Variable globale déguisée Couplage fort
Tests difficiles État persistant entre tests Besoin de reset
Violation SRP Gère instanciation + logique métier Responsabilités multiples
Concurrence Problèmes en multi-threading Non applicable en JS
Dépendances cachées Difficile de tracer les dépendances Maintenance complexe

Quand utiliser le Singleton ?
#

Utilisez le Singleton pour :

// Configuration globale
class AppConfig {
    private static instance: AppConfig;
    private config: Record<string, any> = {};
    
    private constructor() {
        // Charger la config depuis un fichier
    }
    
    public static getInstance(): AppConfig {
        if (!AppConfig.instance) {
            AppConfig.instance = new AppConfig();
        }
        return AppConfig.instance;
    }
}

// Logger global
class Logger {
    private static instance: Logger;
    
    private constructor() {}
    
    public static getInstance(): Logger {
        if (!Logger.instance) {
            Logger.instance = new Logger();
        }
        return Logger.instance;
    }
    
    public log(message: string): void {
        console.log(`[${new Date().toISOString()}] ${message}`);
    }
}

// Connection unique à une base de données
class DatabaseConnection {
    private static instance: DatabaseConnection;
    private connection: any;
    
    private constructor() {
        // Établir la connexion
    }
    
    public static getInstance(): DatabaseConnection {
        if (!DatabaseConnection.instance) {
            DatabaseConnection.instance = new DatabaseConnection();
        }
        return DatabaseConnection.instance;
    }
}

N’utilisez PAS le Singleton pour :

// Objets métier simples
class User {
    // Pas besoin de Singleton
}

// Services avec état modifiable fréquent
class ShoppingCart {
    // Chaque utilisateur a son propre panier
}

// Objets facilement mockables pour les tests
class ApiClient {
    // Mieux vaut l'injecter en dépendance
}

Variantes et alternatives
#

Singleton Eager (chargement immédiat)
#

export class EagerCoordinateur extends Person {
    // Instance créée dès le chargement de la classe
    private static instance: EagerCoordinateur = new EagerCoordinateur();
    
    private constructor() {
        super();
    }
    
    public static getInstance(): EagerCoordinateur {
        return EagerCoordinateur.instance;
    }
    
    public sayHello(): void {
        console.log("Coordinateur eager chargé !");
    }
}

Différences :

  • Plus simple (pas de vérification)
  • Chargement immédiat (même si jamais utilisé)
  • Thread-safe par défaut (si applicable)

Singleton avec méthode de réinitialisation (Tests)
#

export class TestableCoordinateur extends Person {
    private static instance: TestableCoordinateur | undefined;
    
    private constructor() {
        super();
    }
    
    public static getInstance(): TestableCoordinateur {
        if (!TestableCoordinateur.instance) {
            TestableCoordinateur.instance = new TestableCoordinateur();
        }
        return TestableCoordinateur.instance;
    }
    
    // À utiliser UNIQUEMENT dans les tests
    public static resetInstance(): void {
        TestableCoordinateur.instance = undefined;
    }
    
    public sayHello(): void {
        console.log("Coordinateur testable !");
    }
}

// Dans les tests
afterEach(() => {
    TestableCoordinateur.resetInstance();
});

Module Singleton (Pattern ES6)
#

En JavaScript/TypeScript, les modules sont déjà des singletons :

// coordinateur-module.ts
class CoordinateurClass extends Person {
    constructor() {
        super();
    }
    
    public sayHello(): void {
        console.log("Coordinateur via module !");
    }
}

// Export d'une instance unique
export const coordinateur = new CoordinateurClass();
// Utilisation
import { coordinateur } from './coordinateur-module';

coordinateur.setFirstname('Marie');
coordinateur.sayHello();

Avantages :

  • Plus simple (pas de getInstance())
  • Idiomatique en JavaScript
  • Facile à mocker (export nommé)

Injection de dépendances (alternative moderne)
#

// Container IoC (ex: InversifyJS, TSyringe)
import { injectable, singleton } from 'tsyringe';

@singleton()
@injectable()
export class CoordinateurService extends Person {
    constructor() {
        super();
    }
    
    public sayHello(): void {
        console.log("Coordinateur via DI !");
    }
}

// Utilisation
import { container } from 'tsyringe';

const coord1 = container.resolve(CoordinateurService);
const coord2 = container.resolve(CoordinateurService);

console.log(coord1 === coord2); // true

Avantages :

  • Testable (injection de mocks)
  • Découplage total
  • Gestion du cycle de vie

Bonnes pratiques
#

Éviter l’état global excessif
#

// MAUVAIS : Trop de logique métier dans le Singleton
class BadCoordinateur {
    private static instance: BadCoordinateur;
    
    public tasks: Task[] = [];
    public team: Member[] = [];
    public projects: Project[] = [];
    // ... 50 propriétés
    
    // Des milliers de lignes de logique métier
}

// BON : Singleton léger qui délègue
class GoodCoordinateur {
    private static instance: GoodCoordinateur;
    
    constructor(
        private taskManager: TaskManager,
        private teamManager: TeamManager,
        private projectManager: ProjectManager
    ) {}
}

Rendre le Singleton testable
#

// Interface pour le coordinateur
export interface ICoordinateur {
    sayHello(): void;
    getDisplayName(): string;
}

// Implémentation Singleton
export class Coordinateur extends Person implements ICoordinateur {
    private static instance: Coordinateur | undefined;
    
    private constructor() {
        super();
    }
    
    public static getInstance(): ICoordinateur {
        if (!Coordinateur.instance) {
            Coordinateur.instance = new Coordinateur();
        }
        return Coordinateur.instance;
    }
    
    public sayHello(): void {
        console.log("Hello from real Coordinateur");
    }
}

// Mock pour les tests
export class MockCoordinateur implements ICoordinateur {
    public sayHello(): void {
        console.log("Hello from mock");
    }
    
    public getDisplayName(): string {
        return "Mock Coordinateur";
    }
}

// Dans les tests
it('should work with mock', () => {
    const mock = new MockCoordinateur();
    const service = new SomeService(mock); // Injection
    
    expect(service.doSomething()).toBe('expected');
});

Documentation claire
#

/**
 * Coordinateur - Singleton Pattern
 * 
 * Représente le coordinateur unique de l'équipe.
 * Une seule instance peut exister dans toute l'application.
 * 
 * @example
 * ```typescript
 * const coord = Coordinateur.getInstance();
 * coord.setFirstname('Alice');
 * coord.sayHello();
 * ```
 * 
 * @remarks
 * Ne jamais utiliser `new Coordinateur()` (constructeur privé)
 * L'instance est partagée globalement
 * 
 * @see Person - Classe de base
 * @see NameStrategy - Stratégie de formatage du nom
 */
export class Coordinateur extends Person {
    // ...
}

Logging et monitoring
#

export class Coordinateur extends Person {
    private static instance: Coordinateur | undefined;
    private static instanciations: number = 0;
    private static accessCount: number = 0;
    
    private constructor() {
        super();
        Coordinateur.instanciations++;
        
        console.log(`[Coordinateur] Instance créée (count: ${Coordinateur.instanciations})`);
    }
    
    public static getInstance(): Coordinateur {
        Coordinateur.accessCount++;
        
        if (!Coordinateur.instance) {
            console.log('[Coordinateur] Lazy initialization...');
            Coordinateur.instance = new Coordinateur();
        }
        
        if (Coordinateur.accessCount % 100 === 0) {
            console.log(`[Coordinateur] Accédé ${Coordinateur.accessCount} fois`);
        }
        
        return Coordinateur.instance;
    }
    
    public static getStats(): { instanciations: number; accesses: number } {
        return {
            instanciations: Coordinateur.instanciations,
            accesses: Coordinateur.accessCount
        };
    }
    
    public sayHello(): void {
        console.log(`Bonjour, je suis ${this.getDisplayName()}`);
    }
}

Conclusion
#

Récapitulatif
#

Le Singleton est un pattern puissant mais à utiliser avec précaution :

Avantages Inconvénients
Instance unique garantie État global
Accès global simplifié Tests difficiles
Contrôle strict Couplage fort
Économie de ressources Violation SRP

Quand l’utiliser ?
#

Utilisez le Singleton pour :

  • Configuration globale de l’application
  • Logger centralisé
  • Connection pool unique
  • Cache partagé
  • Gestionnaire de ressources unique (comme notre Coordinateur)

Évitez le Singleton pour :

  • Objets métier standards
  • Services facilement testables
  • État fréquemment modifié
  • Objets avec plusieurs contextes

Le Singleton est comme un assaisonnement : utilisé avec parcimonie, il rehausse le plat. Utilisé à outrance, il le gâche.

Le pattern Singleton a sa place dans votre boîte à outils, mais ne doit pas être votre premier réflexe. Dans notre exemple du Coordinateur, il est justifié car :

  • Il représente une ressource unique par nature
  • Son accès global est nécessaire
  • Il simplifie l’architecture sans la compromettre

Mais restez vigilant : un Singleton mal utilisé peut transformer votre code en un cauchemar de maintenance.

Articles connexes

Single Responsibility Principle (SRP) - Refactoring d'une Classe Invoice
·14 mins· loading · loading
Conception Back-End
Les Interfaces en Programmation Orientée Objet - Principe SOLID et Exemple Pratique
·7 mins· loading · loading
Conception Back-End
Modélisation de base de données - Le cas Langlois
·7 mins· loading · loading
Conception Back-End
Les patrons de conception
·7 mins· loading · loading
Conception
Algorithmes - Guide Complet
··25 mins· loading · loading
Back-End Front-End
Comment les base de données fonctionnent - Guide Complet
··25 mins· loading · loading
Back-End