Concept #019
Design PatternsLe Singleton
- design-patterns
- architecture
- programmation
Le Singleton est l'un des design patterns les plus connus — et les plus controversés — de la programmation orientée objet. Son principe est d'une simplicité désarmante : garantir qu'une classe n'est instanciée qu'une seule fois dans toute l'application, et fournir un point d'accès global à cette instance unique.
Le principe : une seule instance, un seul point d'accès
Dans la plupart des programmes, créer plusieurs instances d'une classe est parfaitement normal. Mais certains objets, par nature, ne doivent exister qu'en un seul exemplaire. Imaginez deux gestionnaires de configuration qui lisent des fichiers différents, ou deux loggers qui écrivent dans des fichiers distincts sans se coordonner. Le résultat serait incohérent, voire catastrophique.
Le Singleton résout ce problème en trois temps :
- Cacher le constructeur (le rendre privé) pour empêcher toute instanciation directe depuis l'extérieur.
- Stocker l'instance unique dans un attribut statique de la classe elle-même.
- Exposer une méthode statique — traditionnellement appelée
getInstance()— qui crée l'instance si elle n'existe pas encore, puis la retourne systématiquement.
L'analogie de la tour de contrôle
Imaginez un aéroport sans tour de contrôle unique. Chaque pilote aurait sa propre tour, émettrait ses propres instructions, gérerait ses propres pistes. Le chaos serait immédiat : deux avions sur la même piste, des autorisations contradictoires, des collisions en approche.
La tour de contrôle est un Singleton naturel. Il n'en existe qu'une par aéroport, tous les pilotes communiquent avec elle, et elle maintient une vision cohérente et centralisée de l'espace aérien. Peu importe combien de vols arrivent simultanément, le point de coordination reste unique.
C'est exactement le rôle du Singleton en programmation : un point central, une autorité unique, une source de vérité.
Implémentation en TypeScript
Voici une implémentation classique, thread-safe dans un contexte synchrone comme Node.js :
class ConfigManager {
private static instance: ConfigManager | null = null;
private config: Record<string, string> = {};
// Le constructeur est privé : personne ne peut faire `new ConfigManager()`
private constructor() {
// Chargement initial de la configuration
this.config = {
apiUrl: process.env.API_URL ?? "https://api.example.com",
timeout: process.env.TIMEOUT ?? "5000",
env: process.env.NODE_ENV ?? "development",
};
}
// Le seul moyen d'obtenir l'instance
public static getInstance(): ConfigManager {
if (ConfigManager.instance === null) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
public get(key: string): string | undefined {
return this.config[key];
}
}
// Utilisation
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
console.log(config1 === config2); // true — même instance
console.log(config1.get("env")); // "development"La vérification instance === null avant la création garantit que l'objet n'est construit qu'une seule fois, peu importe combien de fois getInstance() est appelée dans le code.
Cas d'usage typiques
Le Singleton brille dans des situations précises :
Gestionnaire de configuration — Les paramètres de l'application (URLs, clés API, timeouts) sont lus une fois au démarrage et consultés partout. Un singleton évite de relire les fichiers de config à chaque accès.
Logger — Un système de journalisation centralisé garantit que tous les logs arrivent dans le même fichier ou le même flux, avec un formatage cohérent et sans doublons.
Pool de connexions à la base de données — Créer une nouvelle connexion à chaque requête est coûteux. Un pool partagé, géré par un singleton, réutilise les connexions existantes et limite leur nombre total.
Cache en mémoire — Un cache applicatif doit être partagé entre tous les composants pour être efficace. Deux caches indépendants invalideraient mutuellement leur intérêt.
Les critiques du Singleton : un anti-pattern ?
Malgré sa popularité, le Singleton est régulièrement qualifié d'anti-pattern par une partie de la communauté. Ces critiques méritent d'être prises au sérieux.
Problème de testabilité — L'état global persiste entre les tests. Si un test modifie le singleton, il peut polluer les tests suivants. Il est également difficile de substituer le singleton par un mock, car l'instance est figée dans le code appelant.
Couplage global implicite — Tout code qui appelle ConfigManager.getInstance() dépend implicitement de cette classe concrète. Cette dépendance n'est pas visible dans la signature des fonctions, ce qui rend le code plus difficile à comprendre et à maintenir.
Violation du principe de responsabilité unique — La classe gère à la fois sa logique métier et son propre cycle de vie (création, accès). Ces deux responsabilités devraient idéalement être séparées.
Problèmes en environnements concurrents — Dans des langages multi-threadés, une implémentation naïve peut créer plusieurs instances si deux threads atteignent getInstance() simultanément. Des mécanismes de verrouillage sont alors nécessaires.
Alternatives modernes : l'injection de dépendances
La critique la plus constructive du Singleton a donné naissance à une pratique désormais standard : l'injection de dépendances (DI).
Plutôt que de laisser chaque classe aller chercher elle-même son instance via getInstance(), un conteneur IoC (Inversion of Control) crée les objets et les injecte là où ils sont nécessaires. Le cycle de vie — y compris le scope "singleton" — est configuré une fois, à l'extérieur des classes.
// Avec NestJS ou un conteneur DI similaire
@Injectable()
export class ConfigService {
// Pas de getInstance(), pas de constructeur privé
// Le framework garantit qu'il n'existe qu'une instance
get(key: string): string {
return process.env[key] ?? "";
}
}
@Controller("users")
export class UserController {
// L'instance est injectée automatiquement
constructor(private readonly config: ConfigService) {}
}Avec cette approche, ConfigService est bien un singleton dans l'application — mais c'est le conteneur qui l'assure, pas la classe elle-même. Le code est testable (on peut injecter un mock), découplé (la classe ne se connaît pas elle-même) et explicite (les dépendances sont visibles dans le constructeur).
En résumé
Le Singleton résout un vrai problème — coordonner l'accès à une ressource partagée — mais son implémentation classique transfère trop de responsabilités à la classe elle-même. Utilisé avec parcimonie et conscience de ses limites, il reste un outil valide. Mais dans la plupart des architectures modernes, l'injection de dépendances avec un scope singleton offre les mêmes garanties sans les inconvénients. Comme souvent en développement : le pattern n'est pas mauvais en soi, c'est son usage aveugle qui l'est.