Le livre qui a fixé la liste des design patterns date de 1994. Ses quatre auteurs, surnommés le « Gang of Four », y décrivent vingt-trois solutions récurrentes, et cette liste circule encore presque telle quelle dans les cours, les fiches de révision et les entretiens techniques.
Entre la définition d'un pattern et le moment où on le reconnaît dans du code réel, il y a un écart que ces fiches comblent rarement. Un Décorateur appris avec une histoire de café et de sucre ne ressemble pas, au premier regard, au service de journalisation qu'on branche devant un mailer dans Symfony.
Le blocage tient à l'ordre dans lequel on les apprend. Les patterns arrivent sous forme de recettes abstraites, avec des diagrammes UML et des exemples inventés, alors que les frameworks courants en contiennent des dizaines, déjà en place, rarement nommés.
Cet article les range dans un tableau périodique, sur le modèle du tableau des 20 principes de code, et relie chaque case à un endroit vérifiable dans la documentation officielle d'un framework ou d'une bibliothèque standard. Il s'adresse aux personnes en reconversion, aux développeurs juniors et aux curieux non-informaticiens qui lisent du code sans en écrire tous les jours.
Le problème : vingt-trois noms, aucune adresse
En entretien, la question « quels design patterns connais-tu ? » appelle une liste. La relance qui suit, « où en as-tu utilisé un ? », demande autre chose : une adresse dans du code. Un fichier, une classe, une ligne. Réciter Singleton, Fabrique et Observateur ne suffit pas à y répondre, et l'article sur l'entretien technique junior rappelle que l'exercice consiste souvent à réparer un bout de code existant.
La même difficulté apparaît à la relecture. Un assistant de code produit volontiers une classe NotificationStrategyFactory en réponse à une demande simple. Sans les noms, impossible de juger si la structure proposée répond à un besoin réel ou si elle ajoute trois couches pour rien.
Les ressources disponibles accentuent le décalage. Elles présentent chaque pattern isolément, souvent en Java, avec des canards, des pizzas ou des formes géométriques, et presque jamais avec le framework qu'on ouvre le lundi matin.
Le principe : reconnaître une forme, puis lui donner un nom
Un design pattern décrit une solution qui revient face à un problème de conception récurrent. Il se définit par une intention (ce qu'il règle) et par une structure (comment les objets se répartissent les rôles). Le code exact change d'un langage à l'autre, la forme reste reconnaissable.
Le Gang of Four classe ses vingt-trois patterns en trois familles. Les patterns de création répondent à la question « comment un objet naît-il ? ». Ceux de structure répondent à « comment des objets s'assemblent-ils ? », et ceux de comportement à « comment se répartissent-ils le travail ? ». Le tableau ci-dessous suit ce découpage, comme le tableau de Mendeleïev range les éléments par propriétés communes.
Mon avis sur l'ordre d'apprentissage est tranché : pour un junior, partir des frameworks rapporte plus que partir du livre. On manipule déjà ces mécanismes, il reste à les nommer. Le livre devient utile ensuite, quand il faut concevoir sa propre structure plutôt que reconnaître celle des autres.
Les 23 cases, et où les trouver
Chaque entrée donne l'intention du pattern en une phrase, puis un endroit précis où il est en place. Les ancrages ont été vérifiés sur la documentation officielle au moment où ces lignes sont écrites, avec Symfony 7.4, Laravel 13, Spring Framework 7, Django 6.1, Express 5 et Python 3.15.
Création : comment un objet naît
- Singleton (Sg). Une seule instance partagée. Dans Spring, la portée par défaut d'un bean s'appelle singleton : le conteneur crée une instance par définition et la renvoie à chaque demande. La documentation de Spring précise que ce singleton diffère de celui du Gang of Four, qui impose une instance unique par chargeur de classes. Article dédié : Singleton, garder une instance unique sans fabriquer un état global.
- Fabrique (Fm). Une méthode laisse chaque implémentation décider quel objet créer. En Java, l'interface
Iterabledéclareiterator(): chaque collection l'implémente et fabrique son propre itérateur, sans que le code appelant connaisse sa classe. Article dédié : Fabrique (Factory Method), laisser une sous-classe choisir l'objet à créer. - Fabrique abstraite (Fa). Une interface produit une famille d'objets cohérents entre eux. En PHP, la recommandation PSR-17 définit six interfaces de fabrique pour les objets HTTP de PSR-7, de la requête à l'URI. Chaque bibliothèque livre la famille complète, et le code applicatif ne dépend que des interfaces. Article dédié : Fabrique abstraite (Abstract Factory), changer toute une famille d'objets en un seul endroit.
- Monteur (Mo). On assemble un objet complexe étape par étape. Symfony l'expose sous le nom de form builder : une interface fluide où l'on enchaîne les
add(), puisgetForm()produit le formulaire final. - Prototype (Pt). On crée un objet en copiant un modèle existant. PHP l'a intégré au langage : le mot-clé
clonecopie un objet, et la méthode__clone()permet d'ajuster la copie, par exemple pour dupliquer un sous-objet au lieu de le partager.
Structure : comment des objets s'assemblent
- Adaptateur (Ad). Une interface commune posée devant des API différentes. Le stockage de fichiers de Laravel repose sur Flysystem : disque local, SFTP et Amazon S3 répondent aux mêmes méthodes, et la documentation renvoie vers des adaptateurs pour d'autres systèmes.
- Pont (Po). On sépare une abstraction de son implémentation pour faire varier les deux indépendamment. JDBC en Java en est un cas d'école : le code manipule
ConnectionetStatement, définies dansjava.sql, et chaque éditeur de base de données livre le pilote qui les implémente. - Composite (Cp). Un élément seul et un groupe d'éléments partagent la même interface. Dans Symfony, un champ texte, un bloc d'adresse postale et un formulaire entier sont tous des « form types », qui s'imbriquent les uns dans les autres.
- Décorateur (Dc). On enveloppe un objet pour lui ajouter un comportement sans toucher sa classe. La documentation de Symfony 7.4 présente la décoration de services comme une implémentation de ce pattern, avec les attributs
#[AsDecorator]et#[AutowireDecorated]. Le principe composition plutôt qu'héritage explique pourquoi on préfère cette enveloppe à une sous-classe. - Façade (Fc). Une interface simple devant un sous-système compliqué. Le client HTTP de Laravel en est une : une API réduite posée sur Guzzle, centrée sur les cas d'usage courants.
- Poids-mouche (Pm). On partage les petits objets identiques au lieu de les recréer. En Java,
Integer.valueOf()garde en cache au minimum les valeurs de -128 à 127 : deux appels avec 100 renvoient le même objet. - Proxy (Px). Un intermédiaire contrôle l'accès à l'objet réel. Vue 3 rend un objet réactif en l'enveloppant dans un
ProxyJavaScript qui intercepte lectures et écritures. Spring implémente sa programmation par aspects avec des proxys :@Transactional, dans son mode par défaut, entoure le bean d'un proxy qui ouvre et ferme la transaction. Le piège qui en découle est détaillé plus bas.
Comportement : comment ils se répartissent le travail
- Chaîne de responsabilité (Ch). Une requête traverse une suite de maillons, et chacun la traite ou passe la main. Les middlewares d'Express fonctionnent exactement ainsi, exemple de code plus bas.
- Commande (Cm). Une action devient un objet qu'on peut stocker, différer ou rejouer. Symfony Messenger sépare le message, une classe qui porte des données, du handler qui l'exécute, tout de suite ou plus tard via une file d'attente.
- Itérateur (It). On parcourt une collection sans connaître sa structure interne. En Python, une boucle
fors'appuie sur__iter__()et__next__(), et toute classe qui les implémente devient parcourable. - Médiateur (Md). Les objets communiquent par un intermédiaire central au lieu de se connaître entre eux. La documentation de Symfony indique que le composant EventDispatcher implémente les patterns Médiateur et Observateur.
- Mémento (Mm). On capture l'état d'un objet pour le restaurer plus tard. C'est la case la plus vide du côté des frameworks web. On la croise dans les fonctions annuler et rétablir, et la documentation de Vue traite ce cas avec des instantanés d'état, en recommandant Immer quand l'état est volumineux.
- Observateur (Ob). Des abonnés sont prévenus quand un sujet change.
addEventListener()dans le navigateur en est l'exemple quotidien, et l'EventDispatcher de Symfony le revendique aussi. - État (Et). Le comportement d'un objet change selon son état interne. Les machines à états en sont la version outillée, et la documentation de Vue montre l'intégration de XState pour des parcours d'interface complexes. Le pattern d'origine reste plus léger : une classe par état.
- Stratégie (Sy). Des algorithmes interchangeables derrière une même interface. Passport.js porte le mot dans son vocabulaire : chaque mécanisme d'authentification, du mot de passe local à OpenID Connect, est une stratégie installée et enregistrée à part.
- Patron de méthode (Pa). Une classe mère fixe le déroulé, les sous-classes remplissent certaines étapes. Les vues génériques de Django en sont le modèle : on hérite de
ListViewet on surchargeget_queryset()ouget_context_data(). - Visiteur (Vi). On ajoute une opération à une structure d'objets sans modifier ses classes. La bibliothèque standard de Python fournit
ast.NodeVisitor, qui appelle une méthodevisit_suivie du nom du type de nœud pour chaque élément de l'arbre syntaxique. - Interprète (In). On représente une grammaire par des classes pour évaluer des phrases de ce langage. Les langages d'expression embarqués, comme SpEL dans Spring, en reprennent l'idée : une chaîne est analysée, puis évaluée à l'exécution.
Deux exemples à lire de près
Chaîne de responsabilité avec Express 5
Au moment où ces lignes sont écrites, la documentation d'Express porte sur la branche 5.x. L'exemple ci-dessous utilise les modules ES et a été exécuté avec Express 5.3.0 : une requête sans jeton reçoit un 401, une requête avec le bon jeton atteint la route.
// Express 5, modules ES
import express from 'express';
const app = express();
function journaliser(req, res, next) {
console.log(`${req.method} ${req.url}`);
next();
}
function exigerJeton(req, res, next) {
if (req.get('Authorization') !== 'Bearer jeton-de-demo') {
return res.status(401).send('Jeton manquant ou invalide');
}
next();
}
app.use(journaliser);
app.get('/admin', exigerJeton, (req, res) => res.send('Zone admin'));
app.listen(3000);
Chaque maillon a deux sorties : il répond lui-même, ou il appelle next() pour passer au suivant. La documentation d'Express insiste sur un point qui piège souvent les débutants : un middleware qui ne fait ni l'un ni l'autre laisse la requête suspendue, sans erreur visible. L'ordre des app.use() compte aussi, puisque les maillons s'exécutent dans l'ordre où ils sont enregistrés. Depuis Express 5, un middleware asynchrone dont la promesse est rejetée transmet l'erreur à next() automatiquement.
Pour aller plus loin sur ces mécanismes côté JavaScript, la formation JavaScript avancé : async, POO et patterns modernes les travaille sur des projets complets.
Visiteur avec le module ast de Python
Ce visiteur, écrit pour Python 3.15, parcourt un fichier source et relève les lignes qui appellent print(). Il n'utilise aucune API modifiée récemment, et il a été exécuté sur un fichier de test qui contient deux appels.
import ast
from pathlib import Path
class DetecteurPrint(ast.NodeVisitor):
def __init__(self) -> None:
self.lignes: list[int] = []
def visit_Call(self, node: ast.Call) -> None:
if isinstance(node.func, ast.Name) and node.func.id == "print":
self.lignes.append(node.lineno)
self.generic_visit(node)
source = Path("app.py").read_text(encoding="utf-8")
detecteur = DetecteurPrint()
detecteur.visit(ast.parse(source))
print(detecteur.lignes) # [2, 5] sur le fichier de test
L'arbre syntaxique ne bouge pas, toute l'opération vit dans la classe du visiteur. L'appel à generic_visit() est indispensable : sans lui, les nœuds situés sous un appel déjà visité sont ignorés, et un print() imbriqué dans un autre appel disparaît du résultat. Autre détail de version, depuis Python 3.14 les méthodes visit_Num() et visit_Str() ne sont plus appelées, et un visiteur recopié d'un ancien tutoriel doit passer à visit_Constant().
Les pièges
Prendre le nom d'une classe pour le nom du pattern
Les « Facades » de Laravel, comme Cache::get(), sont décrites par leur propre documentation comme des proxys statiques vers des objets du conteneur de services. Elles ne correspondent pas à la Façade du Gang of Four, qui simplifie un sous-système. Même décalage avec le singleton de Spring, unique par conteneur et par définition de bean, pas par classe. Pour classer un mécanisme, c'est sa structure qu'il faut lire.
Contourner un proxy sans le voir
Dans Spring, en mode proxy (le mode par défaut), seuls les appels qui arrivent de l'extérieur passent par le proxy. Une méthode qui appelle une autre méthode @Transactional de la même classe ne déclenche donc aucune transaction, et rien ne le signale. Vue présente un effet voisin : l'objet renvoyé par reactive() n'a pas la même identité que l'original, et une comparaison avec === renvoie faux. Les deux cas viennent du même mécanisme, l'intermédiaire placé entre l'appelant et l'objet.
Comparer des poids-mouches par identité
Avec Java, == entre deux Integer valant 100 renvoie vrai, puisque le cache fournit le même objet. Avec 1000, le résultat n'est plus garanti, la Javadoc ne promettant le cache que de -128 à 127. Un test unitaire qui passe avec de petites valeurs peut donc échouer en production. La comparaison de valeurs passe par equals().
Ajouter un pattern pour cocher une case
Une Stratégie avec une seule implémentation, une Fabrique qui ne fabrique qu'une classe : ces structures coûtent de la lecture sans rien apporter. KISS et YAGNI s'appliquent aux patterns comme au reste du code, et la Rule of Three donne un repère utile : attendre la troisième variante avant d'introduire l'abstraction. Les principes SOLID expliquent pourquoi certains patterns existent, ils ne demandent pas de les placer partout.
Côté PHP, la formation PHP orienté objet : concevoir des applications structurées pose ces structures sur des cas réels, et Symfony 7 : initiation au framework PHP professionnel montre où le framework les a déjà prévues. Pour Python, Python : POO et traitement de données couvre les classes et les protocoles sur lesquels reposent Itérateur et Visiteur.
Questions fréquentes
Faut-il connaître les 23 design patterns pour décrocher un premier poste ?
Les design patterns servent-ils encore avec les assistants de code ?
Quelle différence entre un design pattern et un principe comme SOLID ?
Les patterns ne concernent-ils que la programmation orientée objet ?
key de sorted().