Design patterns essentiels en JavaScript
Mark Toledo
1 avril 2026

J'ai lu le livre du Gang of Four (GoF) en début de carrière et j'ai trouvé les exemples en C++ ou Java assez éloignés de ce que je faisais au quotidien en JavaScript. Il m'a fallu quelques années pour comprendre que ces patterns sont des solutions à des problèmes récurrents — pas des recettes à appliquer mécaniquement.
Ce guide présente les patterns les plus utiles dans un contexte JavaScript moderne, avec des exemples tirés de problèmes que j'ai réellement rencontrés.
Singleton : une seule instance
Le Singleton garantit qu'une classe n'a qu'une seule instance dans toute l'application. En JavaScript, le module system nous donne ça naturellement.
// config.js — Singleton naturel avec les modules ES6
const config = {
db: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432
},
api: {
timeout: 5000,
retries: 3
}
};
export default config; // Même objet partagé dans toute l'app
Pour une connexion base de données, c'est encore plus utile :
// database.js
import { createPool } from 'mysql2/promise';
let pool = null;
export function getPool() {
if (!pool) {
pool = createPool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
connectionLimit: 10
});
}
return pool;
}
On n'instancie le pool qu'une fois, peu importe combien de fois getPool est appelé. Utile pour éviter d'ouvrir des centaines de connexions par inadvertance.
Observer : réagir aux événements
Le pattern Observer permet à des objets de s'abonner et de réagir aux événements d'un autre objet. Node.js l'implémente nativement avec EventEmitter.
import { EventEmitter } from 'events';
class SystemeCommandes extends EventEmitter {
constructor() {
super();
this.commandes = [];
}
async passer(commande) {
const resultat = await this.traiter(commande);
this.commandes.push(resultat);
this.emit('commande:passee', resultat);
if (resultat.montant > 1000) {
this.emit('commande:importante', resultat);
}
return resultat;
}
async traiter(commande) {
// Logique métier...
return { ...commande, id: Date.now(), statut: 'confirmee' };
}
}
// Abonnements
const systeme = new SystemeCommandes();
systeme.on('commande:passee', (commande) => {
console.log(`Commande ${commande.id} confirmée`);
envoyerEmail(commande);
});
systeme.on('commande:importante', (commande) => {
notifierEquipeVentes(commande);
});
systeme.on('commande:passee', (commande) => {
mettreAJourStock(commande);
});
Chaque abonné est indépendant. Ajouter une nouvelle réaction à un événement ne nécessite pas de modifier la logique principale.
Factory : créer des objets sans se soucier de leur type
Le pattern Factory délègue la création d'objets à une fonction ou méthode dédiée. Utile quand le type exact de l'objet dépend d'un paramètre.
class ConnecteurHTTP {
async requete(url, options) {
return fetch(url, options);
}
}
class ConnecteurGRPC {
async requete(service, methode, donnees) {
// Implémentation gRPC
}
}
class ConnecteurMock {
async requete() {
return { status: 200, data: { mock: true } };
}
}
function creerConnecteur(type, config = {}) {
switch (type) {
case 'http': return new ConnecteurHTTP(config);
case 'grpc': return new ConnecteurGRPC(config);
case 'mock': return new ConnecteurMock();
default: throw new Error(`Type de connecteur inconnu : ${type}`);
}
}
// Usage
const connecteur = creerConnecteur(
process.env.NODE_ENV === 'test' ? 'mock' : 'http'
);
En test, on obtient automatiquement un mock. En production, la vraie implémentation. Sans changer le code qui utilise le connecteur.
Strategy : changer l'algorithme à la volée
Le pattern Strategy permet de choisir un algorithme parmi plusieurs interchangeables. J'utilise souvent ce pattern pour la validation de formulaires ou les stratégies de tri.
// Stratégies de tarification
const strategieStandard = (produit) => produit.prixBase;
const strategiePremium = (produit) => produit.prixBase * 1.2;
const strategiePromotion = (reduction) => (produit) =>
produit.prixBase * (1 - reduction / 100);
class PanierAchat {
constructor(strategieTarif = strategieStandard) {
this.articles = [];
this.strategieTarif = strategieTarif;
}
changerStrategie(strategie) {
this.strategieTarif = strategie;
}
calculerTotal() {
return this.articles.reduce((total, article) => {
return total + this.strategieTarif(article) * article.quantite;
}, 0);
}
}
const panier = new PanierAchat();
panier.articles = [{ prixBase: 100, quantite: 2 }];
console.log(panier.calculerTotal()); // 200 (stratégie standard)
panier.changerStrategie(strategiePremium);
console.log(panier.calculerTotal()); // 240
panier.changerStrategie(strategiePromotion(20));
console.log(panier.calculerTotal()); // 160
Decorator : enrichir un objet sans le modifier
Le pattern Decorator ajoute des comportements à un objet sans modifier sa classe. En JavaScript, les fonctions d'ordre supérieur le rendent particulièrement naturel.
// Décorateur de cache pour n'importe quelle fonction async
function avecCache(fn, options = {}) {
const { ttl = 60000 } = options;
const cache = new Map();
return async function(...args) {
const cle = JSON.stringify(args);
const maintenant = Date.now();
if (cache.has(cle)) {
const { valeur, expiration } = cache.get(cle);
if (maintenant < expiration) {
return valeur;
}
}
const valeur = await fn.apply(this, args);
cache.set(cle, { valeur, expiration: maintenant + ttl });
return valeur;
};
}
// Décorateur de logs
function avecLogs(fn, nomFonction) {
return async function(...args) {
console.time(nomFonction);
try {
const resultat = await fn.apply(this, args);
console.timeEnd(nomFonction);
return resultat;
} catch (erreur) {
console.timeEnd(nomFonction);
console.error(`${nomFonction} a échoué:`, erreur);
throw erreur;
}
};
}
// Empiler les décorateurs
const recupererUtilisateur = async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
const recupererUtilisateurOptimise = avecLogs(
avecCache(recupererUtilisateur, { ttl: 30000 }),
'recupererUtilisateur'
);
Command : encapsuler des actions
Le pattern Command encapsule une requête sous forme d'objet. Utile pour implémenter undo/redo, les queues de tâches, ou les transactions.
class GestionnaireUndo {
constructor() {
this.historique = [];
}
executer(commande) {
commande.executer();
this.historique.push(commande);
}
annuler() {
const commande = this.historique.pop();
if (commande) {
commande.annuler();
}
}
}
class CommandeAjouterLigne {
constructor(editeur, ligne) {
this.editeur = editeur;
this.ligne = ligne;
}
executer() {
this.editeur.lignes.push(this.ligne);
}
annuler() {
this.editeur.lignes.pop();
}
}
// Usage
const editeur = { lignes: [] };
const gestionnaire = new GestionnaireUndo();
gestionnaire.executer(new CommandeAjouterLigne(editeur, 'Ligne 1'));
gestionnaire.executer(new CommandeAjouterLigne(editeur, 'Ligne 2'));
console.log(editeur.lignes); // ['Ligne 1', 'Ligne 2']
gestionnaire.annuler();
console.log(editeur.lignes); // ['Ligne 1']
Proxy : contrôler l'accès à un objet
L'objet Proxy JavaScript natif implémente le pattern Proxy nativement. C'est d'ailleurs sur ce mécanisme que Vue 3 construit sa réactivité.
function creerObservable(cible, onChange) {
return new Proxy(cible, {
set(obj, propriete, valeur) {
const ancienneValeur = obj[propriete];
obj[propriete] = valeur;
if (ancienneValeur !== valeur) {
onChange(propriete, valeur, ancienneValeur);
}
return true;
},
get(obj, propriete) {
const valeur = obj[propriete];
// Rendre les objets imbriqués aussi observables
if (typeof valeur === 'object' && valeur !== null) {
return creerObservable(valeur, onChange);
}
return valeur;
}
});
}
const etat = creerObservable({ compteur: 0, nom: 'Mark' }, (prop, nouvelle, ancienne) => {
console.log(`${prop} changé : ${ancienne} → ${nouvelle}`);
});
etat.compteur = 5; // "compteur changé : 0 → 5"
etat.nom = 'Pierre'; // "nom changé : Mark → Pierre"
Quand ne pas utiliser de design pattern
C'est la partie la plus importante. Les design patterns sont des solutions à des problèmes — pas des objectifs en soi. J'ai vu des projets rendus illisibles par une surabondance de patterns appliqués pour leur élégance théorique plutôt que pour résoudre un problème réel.
Quelques questions à se poser avant d'introduire un pattern :
- Quel problème concret ce pattern résout-il ici ?
- Le code est-il plus lisible après qu'avant ?
- Un autre développeur comprendra-t-il le code sans connaître ce pattern ?
Si les réponses ne sont pas clairement positives, la solution la plus simple est probablement la meilleure.
Ce qu'on retient
Les patterns les plus utiles dans le quotidien JavaScript : Observer (EventEmitter, Vue/React reactivity), Factory (création d'objets conditionnelle), Strategy (algorithmes interchangeables), et Decorator (enrichir des fonctions sans les modifier).
La valeur des design patterns n'est pas dans leur implémentation — c'est dans le vocabulaire commun qu'ils fournissent. Quand tu dis "c'est un Observer pattern", l'équipe comprend immédiatement la structure et les responsabilités sans que tu aies à tout expliquer.
