Design patterns
En 1994, quatre auteurs — Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides, restés dans l’histoire sous le nom de Gang of Four — publient Design Patterns: Elements of Reusable Object-Oriented Software et y catalogue vingt-trois solutions récurrentes à des problèmes de conception objet.
Leur apport réel n’est pas d’avoir inventé ces solutions : la plupart existaient déjà dans les bases de code de l’époque. C’est de leur avoir donné un nom. Dire « ici, un Observer » transmet en deux mots une intention, une structure et ses conséquences connues — c’est un vocabulaire de conception, et c’est à ce titre qu’il reste utile trente ans plus tard.
Les auteurs les répartissent en trois familles : créateurs (instanciation), structurels (composition), comportementaux (répartition des responsabilités). Ce classement est celui du livre ; il n’aide pas beaucoup à décider quoi apprendre. Les patrons sont donc présentés ici par fréquence d’usage décroissante dans le code applicatif d’aujourd’hui.
Note
Chaque exemple est donné en TypeScript, Python et Rust ; l’onglet choisi est retenu pour toute la page. Les trois versions ne sont pas des traductions littérales, délibérément : un patron s’écrit avec les moyens du langage, et la comparaison montre mieux que n’importe quel commentaire ce que chacun rend facile ou inutile.
Note
Une partie du catalogue a été absorbée par les langages eux-mêmes. Les fonctions
de première classe rendent Strategy et Command presque invisibles ; les
générateurs de Python et le trait Iterator de Rust intègrent Iterator ; les
énumérations de Rust remplacent souvent Composite, State et Visitor ;
l’immuabilité rend Memento sans objet. Reconnaître un patron ne signifie pas
devoir écrire les cinq classes du diagramme d’origine : le plus souvent, la
version idiomatique du langage tient en trois lignes.
Les incontournables
Ces huit patrons sont présents dans à peu près tout logiciel de taille moyenne, souvent sans que personne ait décidé de les employer.
Strategy
Comportemental. Encapsuler des algorithmes interchangeables derrière une même interface, et choisir à l’exécution.
C’est le patron le plus employé du catalogue, et le remède standard au switch
sur un type. Sous sa forme moderne, la « stratégie » est simplement une fonction.
Tip
Le type de la stratégie révèle le degré de liberté offert. fn en Rust n’accepte
que des fonctions sans état ; impl Fn(&Article, &Article) -> Ordering accepte en
plus les closures qui capturent leur environnement, et Box<dyn Fn(...)> permet
de les stocker et de les choisir à l’exécution.
Observer
Comportemental. Notifier automatiquement un ensemble de dépendants du changement d’état d’un sujet.
Le fondement de toute programmation événementielle : addEventListener, les
EventEmitter de Node, les signals et les Observable de RxJS en sont des
implémentations directes.
Warning
La fuite mémoire classique de ce patron : un observateur qui ne se désinscrit
jamais maintient une référence vers un objet censé être détruit. C’est la raison
d’être des takeUntilDestroyed et autres unsubscribe dans les frameworks
front-end.
Rust ne supprime pas le problème mais le rend visible : le Box<dyn Fn> possède
ce qu’il capture, et une notification vers le sujet lui-même impose un
Weak<RefCell<...>> — le compilateur force à décider qui possède quoi avant que le
cycle n’existe.
Factory Method
Créateur. Déléguer le choix de la classe concrète à instancier.
Dès qu’un new doit varier selon le contexte, on cesse de l’écrire en clair
pour le confier à une fonction ou une méthode dédiée.
Le switch n’a pas disparu, mais il est désormais seul et localisé : tout le
reste de l’application ne connaît que Parseur. La version Rust illustre au
passage deux exigences supplémentaires du langage : le type de retour doit être
uniforme, d’où le Box<dyn Parseur>, et l’échec fait partie de la signature.
Adapter
Structurel. Convertir l’interface d’une classe en celle qu’attend le client.
Le patron de l’intégration. Chaque fois qu’une bibliothèque tierce, une API héritée ou un service externe ne parle pas le langage du domaine, un adaptateur fait la traduction — et concentre en un seul fichier la connaissance du format étranger.
Decorator
Structurel. Ajouter dynamiquement des responsabilités à un objet, sans modifier sa classe.
Le décorateur implémente la même interface que l’objet qu’il enveloppe, ce qui permet de les empiler. C’est le mécanisme des middlewares HTTP, des intercepteurs et des flux d’entrée-sortie composables.
En Rust, l’empilement par généricité produit un type unique connu à la
compilation — DepotAvecJournal<DepotAvecCache<DepotS3>> — et donc des appels
directs, sans indirection. Le prix à payer est qu’une pile décidée à l’exécution
impose de revenir à Box<dyn Depot>.
Tip
Le décorateur est à distinguer nettement du décorateur syntaxique — @Component
en TypeScript, @lru_cache en Python : ce sont des annotations ou des fonctions
d’ordre supérieur, sans rapport avec le patron du GoF malgré le nom partagé. Le
@ de Python en est toutefois plus proche que celui de TypeScript, puisqu’il
enveloppe réellement l’appelable.
Facade
Structurel. Offrir une interface simple et unifiée à un sous-système complexe.
Une façade ne cache pas seulement de la complexité, elle définit un usage attendu. Les couches de service d’une application en sont presque toujours, comme les modules qui masquent trois appels HTTP et deux transformations derrière une méthode au nom métier.
Singleton
Créateur. Garantir une instance unique et un point d’accès global.
Le patron le plus connu du catalogue, et le plus contesté. Il résout un vrai problème — une connexion à la base, un pool de threads, un cache applicatif — mais l’accès global qu’il ajoute est une dépendance invisible, qui n’apparaît dans aucune signature et rend le test difficile.
La version Rust met en évidence ce que les deux autres passent sous silence :
l’initialisation d’un état global doit être sûre en présence de plusieurs
threads. OnceLock garantit qu’un seul initialise, et la référence obtenue est
immuable — un singleton mutable exigerait un Mutex, ce que le compilateur
impose de reconnaître explicitement.
Warning
L’unicité et l’accès global sont deux besoins distincts que ce patron confond. Un
conteneur d’injection de dépendances fournit le premier — une seule instance par
portée — sans le second : la dépendance reste déclarée dans le constructeur, donc
visible et remplaçable en test. C’est la raison pour laquelle @Injectable({ providedIn: 'root' }) a remplacé l’écriture manuelle du patron.
Template Method
Comportemental. Définir le squelette d’un algorithme dans une classe de base, en laissant des étapes aux sous-classes.
C’est le patron de tous les hooks de cycle de vie et de toutes les classes
abstraites AbstractQuelqueChose. La classe parente garde le contrôle de
l’enchaînement, les enfants ne remplissent que les trous.
Rust n’a pas d’héritage, et n’en a pas besoin ici : une méthode par défaut d’un
trait peut appeler les méthodes requises du même trait, ce qui est exactement
le mécanisme du patron. C’est le ressort d’Iterator, dont les soixante méthodes
par défaut reposent toutes sur next().
Note
Template Method repose sur l’héritage, Strategy sur la composition. À intention comparable, la seconde est presque toujours préférable : elle se change à l’exécution, se teste isolément, et ne consomme pas l’unique héritage disponible.
Les courants
Fréquents, mais liés à des besoins plus spécifiques : on les rencontre dans les bibliothèques et les parties structurantes d’une application plus que dans le code métier quotidien.
Command
Comportemental. Transformer une requête en objet, ce qui permet de la stocker, de la mettre en file, de la rejouer ou de l’annuler.
Le patron de l’annulation et de la traçabilité. Réifier l’action rend possible tout ce qu’on ne peut pas faire avec un simple appel de méthode.
Les actions de Redux ou de NgRx en sont une variante : la commande y est un objet de données pur, et l’exécution est déportée dans un réducteur. Rust y arrive naturellement, l’emprunt mutable interdisant à chaque commande de garder une référence sur le document qu’elle modifie.
Builder
Créateur. Construire un objet complexe par étapes, en séparant la construction de la représentation.
Il répond au constructeur à huit paramètres optionnels dont personne ne retient l’ordre. On le trouve surtout dans les API fluides — requêtes SQL, clients HTTP — et dans les fixtures de test.
Note
La fréquence de ce patron dépend fortement du langage. Python, qui a les arguments
nommés et les valeurs par défaut, s’en passe presque toujours. Rust, qui n’a ni
l’un ni l’autre, en fait au contraire une convention omniprésente — au point que
derive_builder le génère automatiquement.
Composite
Structurel. Traiter uniformément un objet simple et une composition d’objets.
Toute structure arborescente en relève : système de fichiers, arbre DOM, hiérarchie de composants, groupes d’un éditeur graphique. La force du patron est que le client ignore s’il manipule une feuille ou un nœud.
L’écart est révélateur : là où les deux premiers langages ouvrent l’ensemble des
sortes de nœuds, Rust le ferme avec une énumération. Ajouter un lien
symbolique impose alors de compléter chaque match — mais le compilateur les
énumère un par un, et aucun cas ne peut être oublié. Box<dyn Noeud> reste
disponible quand l’ensemble doit être extensible par du code tiers.
Iterator
Comportemental. Parcourir les éléments d’une collection sans exposer sa structure interne.
Le cas d’école du patron devenu fonctionnalité du langage : Symbol.iterator en
JavaScript, le protocole __iter__ en Python, le trait Iterator en Rust. On ne
l’écrit presque plus à la main, on l’implémente.
Faute de générateurs stables, Rust impose d’écrire la machine à états du parcours — ici une pile explicite. En échange, l’itérateur est une valeur ordinaire, sans allocation cachée, et hérite gratuitement de toute la chaîne d’adaptateurs décrite dans Les itérateurs.
Proxy
Structurel. Substituer à un objet un représentant qui contrôle son accès.
Structurellement proche du décorateur, mais d’intention différente : le décorateur
ajoute un comportement, le proxy contrôle l’accès — chargement paresseux,
droits, appel distant, mise en cache. Les stubs clients de gRPC et les entités
paresseuses d’un ORM en sont ; chaque langage fournit en outre son propre crochet,
Proxy en JavaScript, __getattr__ en Python, les smart pointers et
Deref en Rust.
State
Comportemental. Faire varier le comportement d’un objet selon son état interne, en représentant chaque état par une classe.
C’est Strategy dont la stratégie change toute seule : chaque état décide de
l’état suivant. Utile dès qu’une machine à états devient assez grosse pour que le
switch sur this.statut devienne illisible.
L’intérêt : les transitions interdites n’existent plus dans le code, au lieu
d’être vérifiées par des conditions dispersées. La transition Rust consomme
l’état — self: Box<Self> — ce qui garantit à la compilation qu’un état périmé ne
peut plus être utilisé après le changement.
Abstract Factory
Créateur. Fournir une interface pour créer des familles d’objets liés, sans préciser leurs classes concrètes.
Un cran au-dessus de Factory Method : on ne produit plus un objet mais un jeu cohérent, dont les éléments doivent tous provenir du même fournisseur. Les drivers de base de données — connexion, transaction, curseur devant appartenir au même moteur — en sont le cas typique.
Les types associés de Rust expriment ce patron mieux que l’héritage
d’interfaces : FabriquePostgres::Requete et FabriquePostgres::Connexion sont
liés par le type de la fabrique, et mélanger une connexion PostgreSQL avec un
constructeur MySQL ne compile pas.
Les occasionnels
Utiles et bien identifiés, mais réservés à des situations que l’on ne rencontre pas tous les mois.
Chain of Responsibility
Comportemental. Faire passer une requête le long d’une chaîne de gestionnaires jusqu’à ce que l’un la traite.
Les pipelines de middlewares — Express, ASGI, tower en Rust, les
intercepteurs HTTP d’Angular — en sont l’application massive : chaque maillon
traite, transmet, ou interrompt.
Mediator
Comportemental. Centraliser dans un objet les interactions entre plusieurs composants, pour qu’ils ne se connaissent pas entre eux.
Il remplace un graphe de dépendances mutuelles par une étoile. Le risque est connu : le médiateur devient un objet fourre-tout si son périmètre n’est pas tenu. En Rust, il a un attrait supplémentaire — un graphe d’objets qui se référencent mutuellement est pénible à écrire, une étoile ne l’est pas.
Prototype
Créateur. Créer un objet par clonage d’une instance existante.
Peu écrit explicitement, mais central pour le presse-papiers, la duplication
d’éléments dans un éditeur, ou la préparation de jeux de données de test. Les
langages le fournissent d’office : structuredClone en JavaScript,
copy.deepcopy en Python, #[derive(Clone)] en Rust — où la distinction entre
copie superficielle et profonde est portée par le type lui-même.
Visitor
Comportemental. Séparer un algorithme de la structure de données qu’il parcourt, pour en ajouter sans modifier les classes visitées.
Le domaine des compilateurs et des outils d’analyse : un arbre syntaxique stable que traversent des dizaines de traitements — vérification de types, formatage, génération de code. Son compromis est brutal : ajouter un traitement est gratuit, ajouter un type de nœud oblige à toucher tous les visiteurs.
Les langages à filtrage par motif le rendent largement superflu : un match
exhaustif sur une énumération Rust, ou un functools.singledispatch en Python,
obtiennent le même découplage sans les deux hiérarchies parallèles.
Bridge
Structurel. Découpler une abstraction de son implémentation pour les faire varier indépendamment.
Le patron des matrices de variation : n abstractions × m plateformes, qui donneraient n × m classes par héritage et n + m par pontage. Rarement nommé, mais présent dans toute bibliothèque multi-plateformes.
Les rares
Memento
Comportemental. Capturer l’état interne d’un objet pour le restaurer plus tard, sans violer son encapsulation.
L’annulation par instantané, plutôt que par action inverse comme dans Command.
Les structures de données immuables et le stockage d’un état applicatif complet le
rendent largement superflu — en Rust, un simple Clone de l’état tient le rôle.
Flyweight
Structurel. Partager les parties communes d’un très grand nombre d’objets pour réduire l’empreinte mémoire.
Une optimisation, non une décision de conception. Elle se justifie sur des
volumes considérables — glyphes d’un moteur de rendu, particules, tuiles d’une
carte — et se paie en complexité partout ailleurs. Les mécanismes d’internement
des langages en relèvent : sys.intern en Python, Rc<str> et les string
interners en Rust.
Interpreter
Comportemental. Représenter une grammaire par une hiérarchie de classes et évaluer ses expressions.
Le plus spécialisé du catalogue : moteurs de règles, langages dédiés, évaluation de filtres saisis par l’utilisateur. En dehors de ces cas, écrire un interpréteur est presque toujours le signe qu’un format existant aurait suffi.
Récapitulatif
| # | Patron | Famille | Intention en une ligne |
|---|---|---|---|
| 1 | Strategy | Comportemental | Algorithmes interchangeables |
| 2 | Observer | Comportemental | Notification des dépendants |
| 3 | Factory Method | Créateur | Déléguer le choix de la classe concrète |
| 4 | Adapter | Structurel | Traduire une interface étrangère |
| 5 | Decorator | Structurel | Empiler des responsabilités |
| 6 | Facade | Structurel | Simplifier l’accès à un sous-système |
| 7 | Singleton | Créateur | Instance unique et globale |
| 8 | Template Method | Comportemental | Squelette d’algorithme à trous |
| 9 | Command | Comportemental | Réifier une action |
| 10 | Builder | Créateur | Construction par étapes |
| 11 | Composite | Structurel | Traiter l’arbre comme la feuille |
| 12 | Iterator | Comportemental | Parcours sans exposer la structure |
| 13 | Proxy | Structurel | Contrôler l’accès à un objet |
| 14 | State | Comportemental | Comportement piloté par l’état |
| 15 | Abstract Factory | Créateur | Familles d’objets cohérentes |
| 16 | Chain of Responsibility | Comportemental | Chaîne de gestionnaires |
| 17 | Mediator | Comportemental | Centraliser les interactions |
| 18 | Prototype | Créateur | Créer par clonage |
| 19 | Visitor | Comportemental | Algorithme externe à la structure |
| 20 | Bridge | Structurel | Abstraction et implémentation séparées |
| 21 | Memento | Comportemental | Instantané restaurable |
| 22 | Flyweight | Structurel | Partage massif pour la mémoire |
| 23 | Interpreter | Comportemental | Évaluer une grammaire |
Warning
Un patron est un diagnostic, pas un objectif. Le connaître sert à nommer une solution vers laquelle le problème conduit déjà, jamais à choisir la structure avant d’avoir le problème : appliqué par anticipation, il produit exactement la complexité qu’il est censé maîtriser. Les critères qui permettent d’arbitrer sont ceux de SOLID.
Références
- Gamma, Helm, Johnson, Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1994
- https://refactoring.guru/fr/design-patterns — catalogue illustré, en français
- https://rust-unofficial.github.io/patterns/ — les idiomes Rust, et ce qu’ils remplacent