JavaScript

Design patterns essentiels en JavaScript

Mark Toledo

Mark Toledo

1 avril 2026

Design patterns essentiels en JavaScript

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.