
Le Singleton garantit qu’une classe n’a qu’une seule instance et fournit un point d’accès global à celle-ci. En JavaScript moderne, la façon la plus simple est un champ statique privé et une méthode getInstance() : static #instance; static getInstance() { return Database.#instance ??= new Database(); }. Un module ES qui exporte un objet est déjà un singleton : c’est souvent la meilleure réponse.
Le Singleton est probablement le design pattern le plus connu et le plus discuté. Sa promesse est simple : une classe dont il ne peut exister qu’une seule instance, accessible de partout. Une connexion à une base de données, un logger, la configuration de l’application sont les exemples classiques. En JavaScript, il s’écrit en quelques lignes, et le langage offre même une alternative gratuite : le module. Voici les implémentations qui fonctionnent aujourd’hui, avec leurs pièges.
Le Singleton en une phrase
Un Singleton empêche la création de plusieurs instances d’une classe et fournit un point d’accès unique à l’instance existante. Le premier appel crée l’objet, les suivants renvoient le même. On l’utilise quand une ressource doit vraiment être partagée : ouvrir dix connexions à la même base parce que dix modules ont fait new Database() serait un gaspillage, et un journal éclaté en dix fichiers serait inutilisable.
On le déconseille dès qu’il sert seulement à éviter de passer un paramètre : il devient alors une variable globale déguisée, difficile à tester et à remplacer. Nous y revenons en fin d’article.
Implémentation 1 : classe avec champ statique privé
C’est l’écriture moderne recommandée. Le champ #instance est privé et statique : il appartient à la classe, pas aux objets, et personne ne peut le lire ou l’écraser de l’extérieur. getInstance() crée l’instance au premier appel grâce à l’opérateur ??= (« affecte si nul »).
class Database {
static #instance;
constructor(dsn) {
this.dsn = dsn;
this.connectedAt = new Date();
}
static getInstance(dsn = 'mongodb://localhost') {
Database.#instance ??= new Database(dsn);
return Database.#instance;
}
query(sql) {
return `${this.dsn} > ${sql}`;
}
}
const a = Database.getInstance('mongodb://prod');
const b = Database.getInstance('mysql://autre');
console.log(a === b); // true : même objet
console.log(b.dsn); // 'mongodb://prod' : le second dsn a été ignoréLa dernière ligne illustre un piège du pattern : les paramètres du second appel sont perdus en silence. Si votre singleton prend des paramètres, soit ils viennent d’une source unique (la configuration), soit getInstance() lève une erreur quand on lui en passe de différents.
Rien n’empêche encore d’écrire new Database() directement. JavaScript n’a pas de constructeur privé, on le simule avec un jeton connu de la seule classe :
const cle = Symbol('Database');
class Database {
static #instance;
constructor(jeton) {
if (jeton !== cle) {
throw new Error('Utilisez Database.getInstance()');
}
}
static getInstance() {
return (Database.#instance ??= new Database(cle));
}
}
new Database(); // Error: Utilisez Database.getInstance()Implémentation 2 : le constructeur renvoie l’instance existante
C’est la version historique de cet article, et elle fonctionne toujours : si un constructeur renvoie explicitement un objet, new renvoie cet objet au lieu du nouveau. L’instance est mémorisée dans une propriété statique.
class Database {
constructor(data) {
if (Database.instance) {
return Database.instance; // new renvoie l'objet existant
}
this._data = data;
Database.instance = this;
}
getData() {
return this._data;
}
setData(data) {
this._data = data;
}
}
const mongo = new Database('mongo');
console.log(mongo.getData()); // 'mongo'
const mysql = new Database('mysql');
console.log(mysql.getData()); // 'mongo' : c'est le même objet que mongo
console.log(mongo === mysql); // trueElle a l’avantage de laisser la syntaxe new Database() intacte pour le code appelant. Elle a l’inconvénient de surprendre : un new qui ne crée rien va contre l’intuition de qui lit le code, et Database.instance est public, donc modifiable. La version à champ privé est plus explicite.
Implémentation 3 : un module ES est déjà un singleton
Un module JavaScript n’est évalué qu’une fois par application, quel que soit le nombre de fichiers qui l’importent. Exporter une instance suffit donc à obtenir un singleton, sans classe ni getInstance().
const config = Object.freeze({
env: process.env.NODE_ENV ?? 'development',
apiUrl: 'https://api.example.com',
});
export default config;class Logger {
#lines = [];
log(message) {
this.#lines.push(`${new Date().toISOString()} ${message}`);
}
get count() {
return this.#lines.length;
}
}
export const logger = new Logger(); // une seule instance pour toute l'applicationimport { logger } from './logger.js';
import config from './config.js';
logger.log(`Démarrage en ${config.env}`);Tout fichier qui importe logger reçoit le même objet. C’est la solution à préférer dans la plupart des projets : elle est lisible, sans magie, et le système de modules garantit l’unicité. Deux limites : l’instance est créée au chargement du module, même si personne ne l’utilise, et le singleton n’est unique que par chemin de fichier (deux copies d’un paquet dans node_modules font deux instances).
Version TypeScript
TypeScript ajoute ce qui manque à JavaScript : un constructeur réellement private, qui interdit new à la compilation.
class Database {
private static instance: Database | undefined;
private constructor(private readonly dsn: string) {}
static getInstance(dsn = 'mongodb://localhost'): Database {
Database.instance ??= new Database(dsn);
return Database.instance;
}
query(sql: string): string {
return `${this.dsn} > ${sql}`;
}
}
const db = Database.getInstance();
// new Database('x'); // Erreur TS2673 : le constructeur est privéL’équivalent en PHP
La structure est identique : propriété statique, constructeur privé, méthode getInstance(). PHP y ajoute deux verrous utiles, __clone privé et __wakeup qui refuse la désérialisation, pour qu’aucune copie ne puisse apparaître.
final class Database
{
private static ?Database $instance = null;
private function __construct(private readonly string $dsn)
{
}
public static function getInstance(string $dsn = 'mysql:host=localhost'): Database
{
return self::$instance ??= new self($dsn);
}
public function query(string $sql): string
{
return "{$this->dsn} > {$sql}";
}
private function __clone()
{
}
public function __wakeup(): void
{
throw new LogicException('Un singleton ne se désérialise pas.');
}
}
$a = Database::getInstance('mysql:host=prod');
$b = Database::getInstance();
var_dump($a === $b); // bool(true)
echo $a->query('SELECT 1'); // mysql:host=prod > SELECT 1Quand ne pas utiliser un Singleton
Le pattern est critiqué pour de bonnes raisons, et il faut les connaître avant de l’adopter :
- Il cache les dépendances. Une fonction qui appelle
Database.getInstance()au milieu de son code dépend de la base sans le dire dans sa signature. Passer la connexion en paramètre (injection de dépendances) rend la dépendance visible et remplaçable. - Il complique les tests. L’état survit d’un test à l’autre. Si vous gardez un singleton, ajoutez une méthode
static reset()réservée aux tests, ou testez à travers l’injection. - C’est un état global. Tout ce qui est vrai des variables globales l’est du singleton : couplage, effets à distance, ordre d’initialisation.
La règle pratique : un module qui exporte une instance (implémentation 3) pour la configuration et les utilitaires sans état, l’injection de dépendances pour tout ce qui touche à une ressource externe, et la classe getInstance() quand une bibliothèque impose ce point d’accès unique. Le pattern guard clauses et les classes en JavaScript prolongent ce sujet côté structure du code.
Erreurs fréquentes
static reset() réservée aux tests, ou injectez la dépendance plutôt que d’appeler getInstance() dans le code métier.new Database('mysql') renvoie l’instance créée avec « mongo » : les arguments du second appel sont perdus sans avertissement. Documentez-le, ou levez une erreur si les paramètres diffèrent.getInstance().

