Avant de plonger
Un design pattern est une solution générale, nommée et réutilisable, à un problème de conception qui revient souvent. Ce n'est pas une librairie qu'on installe, ni un bout de code à copier : c'est un plan qu'on adapte à sa situation.
Pourquoi s'embêter avec ça ?
- Un vocabulaire commun. Dire « ici c'est un Observer » communique en trois mots une intention qui prendrait un paragraphe. Toute l'équipe se comprend instantanément.
- Des solutions éprouvées. Ces problèmes ont été résolus mille fois ; inutile de réinventer une roue carrée.
- Du découplage. Le fil rouge de presque tous les patterns : dépendre d'abstractions, pas de détails concrets, pour que les morceaux évoluent indépendamment.
Les trois familles
Le « Gang of Four » (le livre fondateur de 1994) range les 23 patterns en trois familles, selon la question à laquelle ils répondent :
Créationnels — « comment créer des objets ? » (Factory, Builder, Singleton…)
Structurels — « comment les assembler en structures ? » (Adapter, Decorator, Proxy…)
Comportementaux — « comment se parlent-ils ? » (Observer, Strategy, State…)
Le piège à éviter (le plus important)
« Quand on a un marteau, tout ressemble à un clou. » Le débutant qui découvre les patterns veut les caser partout — et transforme un programme simple en cathédrale d'abstractions. Chaque pattern a un coût : de l'indirection, des classes en plus, de la complexité. Ne le payez que lorsque le problème est réel, pas anticipé.
On ne « place » pas un pattern : on le reconnaît quand le problème qu'il résout apparaît. Apprenez-les pour les repérer, et appliquez-les avec parcimonie. Le meilleur code reste le plus simple qui fasse le travail.
Factory Method
Intention — Déléguer la création d'objets à des sous-classes, en programmant contre une interface plutôt qu'une classe concrète.
Le problème
Votre code fait des new ClasseConcrète() un peu partout. Le jour où il faut un autre type d'objet, vous chassez chaque new dispersé dans la base — et vous ajoutez un if à chaque fois.
La solution
On isole la création derrière une méthode (la « factory method »). La classe de base utilise le produit via son interface ; chaque sous-classe décide quel produit concret fabriquer.
🌍 Analogie — Un transporteur. RoadLogistics crée des camions, SeaLogistics crée des bateaux. La planification d'un trajet est identique ; seul le « véhicule » créé change.
Utilisez-le quand vous ne connaissez pas à l'avance les types concrets à créer, ou pour offrir un point d'extension aux utilisateurs de votre lib.
Le piège — une sous-classe par produit peut faire proliférer les classes. Pour un cas trivial, une simple fonction suffit.
Abstract Factory
Intention — Créer des familles d'objets liés (qui vont ensemble) sans préciser leurs classes concrètes.
Le problème
Vous avez plusieurs produits qui doivent être cohérents entre eux : des boutons et des cases « style macOS », ou « style Windows ». Vous voulez éviter de mélanger un bouton Mac avec une case Windows.
La solution
Une fabrique abstraite regroupe la création de toute une famille. Chaque fabrique concrète produit une famille cohérente.
🌍 Analogie — Un fabricant de meubles en kit. La gamme « Art déco » fournit chaise + table + canapé assortis ; la gamme « Moderne » aussi. On choisit une gamme, tout est cohérent.
Utilisez-le quand votre code doit fonctionner avec plusieurs familles de produits liés, en garantissant leur cohérence (thèmes UI, drivers de BDD, multi-plateformes).
Le piège — ajouter un nouveau type de produit oblige à modifier l'interface de fabrique et toutes ses implémentations. Lourd si la famille grossit souvent.
Builder
Intention — Construire un objet complexe étape par étape, en séparant la construction de la représentation finale.
Le problème
Le fameux constructeur « télescopique » : new Truc(a, b, undefined, null, true, undefined, "x"). Illisible, et chaque option facultative ajoute une surcharge.
La solution
Un builder propose des méthodes lisibles, chaînables, puis un build() final. On ne paie que pour les options qu'on choisit.
🌍 Analogie — Commander un burger : pain, steak, fromage, sauce… on coche les options une à une, et « valider » assemble le tout.
Utilisez-le quand un objet a beaucoup de paramètres optionnels, ou que sa construction se fait en plusieurs étapes (requêtes, configurations, documents).
Le piège — pour un objet à 2-3 champs, un objet de config { } en paramètre est plus simple. N'en faites pas une usine à gaz.
Prototype
Intention — Créer de nouveaux objets en clonant un exemplaire existant, plutôt qu'en les reconstruisant de zéro.
Le problème
Recréer un objet à l'identique demande de reconfigurer tous ses champs — parfois privés, ou coûteux à recalculer. Et vous voulez éviter de coupler votre code à sa classe concrète.
La solution
L'objet sait se cloner lui-même via une méthode clone().
🌍 Analogie — La photocopie. Plus rapide que de retaper le document : on duplique l'existant, puis on annote la copie.
Utilisez-le quand la création est coûteuse, ou que vous voulez dupliquer des objets sans dépendre de leurs classes concrètes (éditeurs graphiques, objets de jeu).
Le piège — attention aux références partagées : un clone superficiel copie les pointeurs, pas les objets imbriqués. Pensez clone profond si besoin.
Singleton
Intention — Garantir qu'une classe n'a qu'une seule instance et fournir un point d'accès global à celle-ci.
Le problème
Certaines choses doivent être uniques : un pool de connexions, une config applicative. On veut empêcher d'en créer plusieurs par accident.
La solution
Constructeur privé + une méthode statique qui crée l'instance à la demande et la réutilise ensuite.
Le Singleton est souvent considéré comme un anti-pattern : c'est de l'état global déguisé. Il rend les tests difficiles (impossible de l'isoler ou de le remplacer), masque les dépendances et crée du couplage caché. Avant d'en mettre un, demandez-vous si une simple injection de dépendance ne ferait pas mieux le travail.
🌍 Analogie — Le gouvernement d'un pays : peu importe qui on interroge, il n'y en a qu'un, et c'est le point d'accès officiel.
Utilisez-le quand une ressource doit être strictement unique et partagée globalement (et que l'injection n'est pas praticable).
Le piège — testabilité et concurrence. En général, préférez injecter l'instance plutôt que d'y accéder via une statique globale.
Adapter
Intention — Faire collaborer deux interfaces incompatibles en intercalant un traducteur.
Le problème
Une lib tierce (ou un vieux module) expose une API qui ne correspond pas à ce que votre code attend — et vous ne pouvez pas la modifier.
La solution
Un adaptateur implémente l'interface attendue par votre code, et traduit chaque appel vers la lib existante.
🌍 Analogie — L'adaptateur de prise électrique en voyage : il ne change ni la prise murale ni l'appareil, il les réconcilie.
Utilisez-le quand vous intégrez une lib externe, un système legacy, ou des APIs aux signatures divergentes derrière une interface unifiée.
Le piège — empiler les adaptateurs d'adaptateurs masque la dette. Parfois, mieux vaut assainir l'interface d'origine.
Bridge
Intention — Découpler une abstraction de son implémentation pour que les deux varient indépendamment.
Le problème
Deux dimensions varient en même temps. Ex. : Télécommande (basique, avancée) × Appareil (TV, radio). En héritage classique, on explose en TVBasicRemote, RadioAdvancedRemote… combinatoire ingérable.
La solution
On sépare les deux hiérarchies : l'abstraction contient (compose) une implémentation et lui délègue le travail.
🌍 Analogie — Une télécommande universelle : un seul boîtier (abstraction) pilote n'importe quel téléviseur (implémentation), chacun évoluant de son côté.
Utilisez-le quand deux axes varient indépendamment (UI × plateforme, forme × rendu), pour éviter l'explosion combinatoire de sous-classes.
Le piège — souvent confondu avec l'Adapter. Le Bridge se conçoit en amont ; l'Adapter répond après coup à une incompatibilité.
Composite
Intention — Composer des objets en arborescences, et traiter un élément simple et un groupe d'éléments de façon identique.
Le problème
Vous manipulez une hiérarchie (fichiers/dossiers, menus/sous-menus, formes/groupes) et vous truffez votre code de if (estUnDossier) … else ….
La solution
Feuilles et conteneurs implémentent la même interface. Un conteneur délègue récursivement à ses enfants.
🌍 Analogie — Un organigramme d'entreprise : on peut demander « combien de personnes ? » à un employé (1) comme à un département entier (sa somme).
Utilisez-le quand vous représentez des hiérarchies « tout/partie » et voulez les parcourir uniformément (arbres DOM, systèmes de fichiers, scènes graphiques).
Le piège — une interface trop large : si add() n'a aucun sens sur une feuille, il faut décider comment le gérer (erreur, no-op).
Decorator
Intention — Ajouter des responsabilités à un objet dynamiquement, en l'enveloppant dans un autre objet de même interface.
Le problème
Vous voulez combiner des options (café + lait + sucre + chantilly). En héritage, vous créeriez une classe par combinaison… explosion garantie.
La solution
Chaque option est un décorateur qui enveloppe l'objet, expose la même interface, et ajoute son comportement avant/après de déléguer.
Le pattern Decorator (envelopper un objet) n'est pas la même chose que les @decorators de TypeScript (une syntaxe pour annoter classes/méthodes). Mêmes mot, idées différentes — même si la syntaxe s'en inspire.
🌍 Analogie — S'habiller : t-shirt, puis pull, puis manteau. Chaque couche ajoute une fonction (chaleur) sans modifier les précédentes.
Utilisez-le quand vous voulez ajouter/retirer des comportements à la volée sans toucher la classe (flux de données : compression, chiffrement ; UI ; logging).
Le piège — de longues piles de décorateurs rendent le débogage et l'ordre d'application difficiles à suivre.
Facade
Intention — Offrir une interface simple et unifiée à un sous-système complexe.
Le problème
Pour une tâche courante, le client doit orchestrer dix classes du sous-système, dans le bon ordre, avec les bons réglages. Couplage maximal.
La solution
Une façade expose une poignée de méthodes de haut niveau et cache toute la plomberie.
🌍 Analogie — Un standard téléphonique : vous dites « je veux réserver », sans savoir qui, en interne, gère le paiement, le stock et la logistique.
Utilisez-le quand vous voulez un point d'entrée simple vers une lib ou un sous-système touffu, ou découpler vos clients de ses détails.
Le piège — la façade peut devenir un « objet-dieu » qui sait tout faire. Gardez-la fine ; elle orchestre, elle n'implémente pas tout.
Flyweight (poids-mouche)
Intention — Partager l'état commun entre une multitude d'objets pour économiser énormément de mémoire.
Le problème
Vous créez des millions d'objets quasi identiques (arbres d'une forêt, particules, caractères d'un texte). Chacun duplique des données lourdes → la RAM explose.
La solution
On sépare l'état intrinsèque (partagé, immuable) de l'état extrinsèque (propre à chaque objet). Une fabrique réutilise les flyweights partagés.
🌍 Analogie — Une police de caractères : la forme du « e » est stockée une fois ; le document ne retient que les positions où l'afficher.
Utilisez-le quand un très grand nombre d'objets partagent un état lourd et que la mémoire est un vrai goulot (rendu, jeux, éditeurs).
Le piège — optimisation pointue : complexifie le code. Ne l'introduisez qu'une fois la consommation mémoire mesurée comme problématique.
Proxy
Intention — Substituer un objet par un intermédiaire de même interface, qui contrôle l'accès au vrai objet.
Le problème
Vous voulez ajouter du cache, du lazy-loading, du contrôle d'accès ou du logging autour d'un objet — sans modifier ni l'objet, ni le client.
La solution
Un proxy implémente la même interface, intercepte les appels, fait son travail, puis (éventuellement) délègue au vrai objet.
🌍 Analogie — Une carte bancaire : c'est un proxy vers votre compte. Même usage (payer), mais elle contrôle l'accès et la limite.
Utilisez-le quand vous voulez du cache, du chargement différé, du contrôle d'accès, des logs ou un objet distant — de façon transparente pour le client.
Le piège — proche du Decorator (même structure). La nuance : le Decorator ajoute des fonctions, le Proxy contrôle l'accès.
Chain of Responsibility
Intention — Faire transiter une requête le long d'une chaîne de gestionnaires, chacun décidant de la traiter ou de la passer au suivant.
Le problème
Une requête doit passer plusieurs étapes (authentification, quotas, validation, routage). Tout coder dans un seul bloc devient un monstre conditionnel.
La solution
Chaque étape est un maillon autonome ; on les chaîne, et la requête circule jusqu'à ce qu'un maillon réponde.
🌍 Analogie — Une demande de congé qui remonte : manager → RH → direction. Chacun valide à son niveau ou transmet plus haut.
Utilisez-le quand plusieurs objets peuvent traiter une requête et que le bon gestionnaire n'est connu qu'à l'exécution (middlewares, pipelines de validation, gestion d'événements).
Le piège — une requête peut traverser toute la chaîne sans être traitée. Prévoyez toujours un comportement par défaut.
Command
Intention — Transformer une requête en objet autonome — pour la paramétrer, la mettre en file, l'historiser ou… l'annuler.
Le problème
Vous voulez de l'undo/redo, des macros, ou découpler « qui déclenche » de « qui exécute ». Difficile avec de simples appels de méthode.
La solution
Chaque action devient un objet Command avec execute() (et souvent undo()). On peut alors les stocker, rejouer, inverser.
🌍 Analogie — Une commande au restaurant : le ticket (objet) circule du serveur à la cuisine. On peut l'empiler, la rejouer, l'annuler.
Utilisez-le quand vous avez besoin d'undo/redo, de files de tâches, de macros, ou de découpler l'émetteur d'une action de son exécuteur (boutons, raccourcis, jobs).
Le piège — beaucoup de petites classes de commandes. À doser ; pour un cas trivial, un callback suffit.
Iterator
Intention — Parcourir une collection séquentiellement sans exposer sa représentation interne.
Le problème
Vous voulez offrir un parcours uniforme de vos collections (liste, arbre, graphe) sans que le client connaisse leur structure.
La solution
En JS/TS, le pattern est natif : implémentez le protocole d'itération via [Symbol.iterator], et for…of / le spread fonctionnent gratuitement.
Les générateurs (function* avec yield) sont la façon la plus concise d'écrire un itérateur : ils gèrent l'état du parcours pour vous, sans objet next() manuel.
🌍 Analogie — La télécommande « chaîne suivante » : vous passez d'une chaîne à l'autre sans savoir comment la télé les stocke.
Utilisez-le quand vous exposez vos propres structures de données au parcours, ou voulez plusieurs façons de parcourir (avant/arrière, filtré).
Le piège — pour un simple tableau, n'inventez rien : for…of et les méthodes natives suffisent largement.
Mediator
Intention — Centraliser les communications entre objets dans un médiateur, pour qu'ils ne se référencent plus directement.
Le problème
Quand chaque composant connaît tous les autres, on obtient un plat de spaghettis : couplage « tous-à-tous », impossible à faire évoluer.
La solution
Les composants ne parlent qu'au médiateur ; lui seul connaît tout le monde et orchestre les échanges.
🌍 Analogie — La tour de contrôle d'un aéroport : les avions ne se coordonnent pas entre eux, ils passent tous par la tour.
Utilisez-le quand un ensemble d'objets communiquent de façon désordonnée (composants d'un formulaire, salons de chat, UI complexe).
Le piège — le médiateur peut devenir un « objet-dieu » omniscient. Surveillez sa taille ; sinon vous déplacez le problème.
Memento
Intention — Capturer et restaurer l'état d'un objet (des instantanés) sans violer son encapsulation.
Le problème
Vous voulez des sauvegardes/restaurations (undo, points de contrôle) sans exposer tous les champs internes de l'objet au reste du code.
La solution
L'objet produit un memento opaque (un snapshot) et sait se restaurer à partir de l'un d'eux. Un « gardien » conserve l'historique sans en lire le contenu.
🌍 Analogie — Les points de sauvegarde d'un jeu vidéo : on enregistre l'état complet, et on y revient si ça tourne mal.
Utilisez-le quand vous avez besoin d'undo/snapshots/historique (souvent en duo avec Command) sans exposer l'intérieur de l'objet.
Le piège — garder beaucoup de snapshots complets coûte cher en mémoire. Pensez aux diffs ou à limiter l'historique.
Observer
Intention — Définir une dépendance un-à-plusieurs : quand un sujet change, tous ses abonnés sont notifiés automatiquement.
Le problème
Plusieurs parties de l'app doivent réagir à un changement (nouvelle donnée, événement). Si la source appelle chaque consommateur à la main, elle les connaît tous → couplage fort.
La solution
Les intéressés s'abonnent au sujet ; celui-ci les notifie sans connaître leurs détails. C'est le socle des systèmes d'événements et de réactivité.
🌍 Analogie — Un abonnement à une newsletter : l'éditeur publie, tous les abonnés reçoivent, et il ignore qui ils sont.
Utilisez-le quand un changement doit se propager à plusieurs objets sans les coupler (events DOM, state management, réactivité, MVC).
Le piège — fuites mémoire si on oublie de se désabonner, et cascades de notifications difficiles à tracer. Pensez unsubscribe.
State
Intention — Changer le comportement d'un objet quand son état interne change — comme s'il changeait de classe.
Le problème
Un objet se comporte différemment selon son statut (brouillon, publié, archivé), et vous accumulez des switch (this.status) dans toutes ses méthodes.
La solution
Chaque état devient un objet qui encapsule son comportement. L'objet délègue à son état courant, et les états gèrent les transitions. C'est une machine à états propre.
🌍 Analogie — Un feu tricolore : rouge, orange, vert. Chaque couleur « sait » quelle est la suivante et quel comportement imposer.
Utilisez-le quand un objet a de nombreux états aux comportements distincts, et que les if/switch sur le statut prolifèrent (workflows, lecteurs, connexions).
Le piège — proche de Strategy structurellement. Différence : ici les états connaissent les transitions entre eux.
Strategy
Intention — Encapsuler une famille d'algorithmes interchangeables derrière une même interface, et les rendre permutables à l'exécution.
Le problème
Une méthode contient un gros if/else qui choisit un comportement selon un paramètre (mode de paiement, calcul de remise, tri). Ajouter un cas = rouvrir et risquer de casser le reste.
La solution
Chaque variante devient une stratégie isolée ; le contexte en reçoit une et l'utilise, sans connaître les détails. C'est le pattern pour tuer les conditionnels.
🌍 Analogie — Un GPS : « le plus rapide », « le plus court », « éviter les péages ». Même trajet à calculer, stratégies interchangeables.
Utilisez-le quand vous avez plusieurs façons de faire une même chose et voulez en changer dynamiquement (paiement, tri, compression, pricing).
Le piège — en JS, une stratégie peut être une simple fonction passée en paramètre — inutile de créer une classe par variante.
Template Method
Intention — Définir le squelette d'un algorithme dans une méthode, en laissant les sous-classes redéfinir certaines étapes sans changer l'ordre global.
Le problème
Plusieurs algorithmes partagent la même structure d'ensemble, mais diffèrent sur quelques étapes. Tout dupliquer mène à du copier-coller fragile.
La solution
La classe de base fige l'enchaînement (la « template method ») et déclare des étapes abstract que les sous-classes remplissent.
Les deux varient un algorithme. Template Method le fait par héritage (à la compilation, on remplit des trous). Strategy le fait par composition (à l'exécution, on échange un objet). Préférez souvent Strategy : plus souple, moins couplé.
🌍 Analogie — Une recette de base de gâteau : étapes fixes (mélanger, cuire), mais le parfum (chocolat, vanille) varie selon la déclinaison.
Utilisez-le quand plusieurs variantes partagent une ossature commune et ne diffèrent que par des étapes précises (pipelines, parseurs, frameworks).
Le piège — l'héritage crée un couplage fort base ↔ sous-classes. Si la structure bouge souvent, la composition (Strategy) vieillit mieux.
Visitor
Intention — Ajouter de nouvelles opérations à une hiérarchie d'objets sans modifier ses classes.
Le problème
Vous avez une structure stable (formes, nœuds d'un AST) et voulez lui ajouter des traitements variés (calculer l'aire, exporter, valider) sans polluer chaque classe à chaque nouvelle opération.
La solution
On externalise les opérations dans des visiteurs. Chaque élément « accepte » un visiteur et l'aiguille vers la bonne méthode (le « double dispatch »).
🌍 Analogie — Un inspecteur qui visite des bâtiments : selon sa spécialité (incendie, électricité), il applique son propre contrôle, sans modifier les bâtiments.
Utilisez-le quand une hiérarchie d'objets est stable mais que vous ajoutez souvent de nouvelles opérations dessus (compilateurs/AST, sérialisation, rapports).
Le piège — l'inverse coince : ajouter un nouveau type d'élément oblige à modifier tous les visiteurs. Réservez-le aux structures qui changent peu.
Interpreter
Intention — Représenter une grammaire simple par un arbre d'objets, et savoir interpréter ses phrases.
Le problème
Vous devez évaluer un mini-langage récurrent : expressions booléennes, règles métier, filtres. Le bricoler avec des chaînes et des conditions devient vite ingérable.
La solution
Chaque règle de grammaire devient une classe d'expression avec une méthode interpret(). On compose ces expressions en arbre, qu'on évalue.
🌍 Analogie — Une calculatrice : « 2 + 3 × 4 » est un arbre (×, puis +) que la machine parcourt pour donner le résultat.
Utilisez-le quand vous avez un petit langage stable à évaluer (règles, filtres, expressions), simple et bien borné.
Le piège — le moins utilisé du GoF. Pour une vraie grammaire, un générateur de parseur ou une lib dédiée sera bien plus robuste.
Comment choisir ?
Ne partez jamais d'un pattern. Partez d'un symptôme dans votre code, et laissez-le vous guider. Voici les correspondances les plus fréquentes.
if/else ou switch qui choisit un comportement1. Écrivez la solution la plus simple. 2. Quand un vrai point de douleur apparaît (duplication, rigidité, couplage), 3. reconnaissez le pattern qui le soigne. Jamais l'inverse. Un pattern appliqué « au cas où » est de la complexité gratuite.
L'antisèche
Les 23 patterns, leur intention en une ligne. À épingler.
| Créationnels — créer des objets | |
| Factory Method | Déléguer la création à des sous-classes via une interface. |
| Abstract Factory | Créer des familles d'objets cohérents entre eux. |
| Builder | Assembler un objet complexe étape par étape. |
| Prototype | Créer en clonant un objet existant. |
| Singleton | Une instance unique, point d'accès global (à manier avec prudence). |
| Structurels — assembler | |
| Adapter | Réconcilier deux interfaces incompatibles. |
| Bridge | Séparer abstraction et implémentation pour qu'elles varient seules. |
| Composite | Traiter un élément et un arbre d'éléments de la même façon. |
| Decorator | Ajouter des comportements en enveloppant l'objet. |
| Facade | Une interface simple sur un sous-système complexe. |
| Flyweight | Partager l'état commun pour économiser la mémoire. |
| Proxy | Un intermédiaire qui contrôle l'accès à un objet. |
| Comportementaux — communiquer | |
| Chain of Resp. | Faire transiter une requête le long d'une chaîne de gestionnaires. |
| Command | Transformer une requête en objet (file, undo, macro). |
| Iterator | Parcourir une collection sans exposer sa structure. |
| Mediator | Centraliser les échanges entre objets. |
| Memento | Capturer / restaurer un état (snapshots). |
| Observer | Notifier automatiquement une foule d'abonnés. |
| State | Changer de comportement selon l'état interne. |
| Strategy | Des algorithmes interchangeables derrière une interface. |
| Template Method | Squelette figé, étapes personnalisables par les sous-classes. |
| Visitor | Ajouter des opérations à une hiérarchie sans la modifier. |
| Interpreter | Représenter et interpréter une petite grammaire. |
Les design patterns ne sont pas une médaille à exhiber : ce sont des outils de communication et des réponses à des douleurs réelles. Apprenez-les pour les reconnaître, gardez votre code simple, et ne dégainez le bon plan que quand le problème, lui, est bien là. Bon design ! 🔵