Les 23 patterns du Gang of Four restent la grammaire commune de la conception orientée objet. Ce guide illustré couvre les patterns les plus utiles en pratique, avec leurs structures, leurs cas d'usage et les pièges à éviter.
Le livre Design Patterns: Elements of Reusable Object-Oriented Software publié en 1994 par Gamma, Helm, Johnson et Vlissides — le « Gang of Four » — reste une des références les plus citées en génie logiciel. Ses 23 patterns sont divisés en trois familles : créationnels (comment créer les objets), structuraux (comment les assembler) et comportementaux (comment ils communiquent). Ce guide se concentre sur les patterns les plus utiles en pratique, en évitant le catalogue exhaustif qui mène souvent au pattern-matching mécanique.
Patterns créationnels
Factory Method
Le Factory Method délègue la création d'objets à des sous-classes, permettant à une classe de travailler avec des types dont elle ignore l'implémentation concrète.
Creator
────────────────────
+ factoryMethod() : Product ← abstract
+ operation()
▲
┌─────────┴──────────┐
│ │
ConcreteCreatorA ConcreteCreatorB
+ factoryMethod() + factoryMethod()
│ │
▼ ▼
ProductA ProductB
(implements Product) (implements Product)
Cas d'usage : un framework de parsing qui supporte JSON, XML et CSV. La classe abstraite Parser définit
createReader() — chaque sous-classe retourne le reader adapté. L'algorithme de traitement reste identique, seule la
création du lecteur varie. À éviter quand il n'y a qu'un seul type concret : c'est alors de l'indirection inutile.
Builder
Le Builder sépare la construction d'un objet complexe de sa représentation, permettant de créer différentes représentations avec le même processus de construction.
Director
──────────────────────
- builder : Builder
+ construct() ──────► Builder (interface)
──────────────────────
+ buildPartA()
+ buildPartB()
+ getResult() : Product
▲
┌──────────┴───────────┐
ConcreteBuilderA ConcreteBuilderB
+ buildPartA() + buildPartA()
+ buildPartB() + buildPartB()
+ getResult() + getResult()
Cas d'usage pratique : la construction d'une requête SQL complexe, d'un email avec pièces jointes et destinataires
multiples, ou d'une configuration d'objet avec des dizaines de paramètres optionnels. Le Builder fluent (chaque méthode
retourne this) est sa forme moderne la plus répandue en Java :
Email email = new EmailBuilder()
.to("alice@example.com")
.subject("Rapport mensuel")
.body(bodyHtml)
.attachment(reportPdf)
.build();
Singleton
Le Singleton garantit qu'une classe n'a qu'une seule instance et fournit un point d'accès global à celle-ci.
Singleton
──────────────────────────
- instance : Singleton (static)
- Singleton() (private)
──────────────────────────
+ getInstance() : Singleton (static)
+ operation()
Le Singleton est le pattern le plus controversé du GoF. Il facilite les tests difficiles (couplage global implicite), complique la parallélisation, et masque les dépendances. En Java moderne, préférez l'injection de dépendances via Spring — le conteneur IoC gère le cycle de vie. Réservez le Singleton aux rares cas où une instance unique est une contrainte réelle du système (accès à une ressource matérielle unique, par exemple).
Patterns structuraux
Adapter
L'Adapter convertit l'interface d'une classe en une autre interface que le client attend. Il permet à des classes incompatibles de collaborer.
Client ──────► Target (interface)
+ request()
▲
Adapter
──────────────────
- adaptee : Adaptee
+ request() ──────► adaptee.specificRequest()
Adaptee
──────────────────
+ specificRequest()
Cas d'usage : intégrer une bibliothèque tierce dont l'interface ne correspond pas à votre domaine, adapter un ancien service legacy à une nouvelle interface, ou brancher une librairie de logging externe sur votre interface interne. L'Adapter par composition (ci-dessus) est préférable à l'Adapter par héritage — plus flexible et moins fragile.
Decorator
Le Decorator attache dynamiquement des responsabilités supplémentaires à un objet, en alternative à l'héritage pour l'extension de fonctionnalités.
Component (interface)
+ operation()
▲
┌──────────┴──────────────┐
│ │
ConcreteComponent Decorator
+ operation() - component : Component
+ operation() → component.operation()
▲
┌──────────┴──────────┐
DecoratorA DecoratorB
+ operation() + operation()
// + extra behavior // + extra behavior
Cas d'usage : les InputStream de Java sont l'exemple canonique — BufferedInputStream décore FileInputStream
qui décore InputStream. En pratique : ajouter du logging, du caching, ou de la validation à un service sans modifier
ses implémentations existantes. Le Decorator permet d'empiler les comportements à runtime, contrairement à l'héritage
qui fixe la combinaison à la compilation.
Facade
La Facade fournit une interface simplifiée à un sous-système complexe, sans modifier le sous-système lui-même.
Client
│
▼
Facade
──────────────────────────────────────────
+ operation()
│ │ │
▼ ▼ ▼
SubsystemA SubsystemB SubsystemC
(complexe) (complexe) (complexe)
Cas d'usage : un OrderService qui orchestre InventoryService, PaymentService, NotificationService et
ShippingService. Le client n'appelle qu'orderService.placeOrder(cart) — la complexité est encapsulée. La Facade ne
verrouille pas l'accès aux sous-systèmes : un client avancé peut toujours appeler PaymentService directement si
nécessaire.
Patterns comportementaux
Observer
L'Observer définit une relation un-à-plusieurs entre objets : quand un objet change d'état, tous ses dépendants en sont notifiés automatiquement.
Subject (interface) Observer (interface)
────────────────── ──────────────────
+ attach(Observer) + update()
+ detach(Observer) ▲
+ notify() │
▲ ConcreteObserverA
│ ConcreteObserverB
ConcreteSubject + update()
- state
- observers : List<Observer>
+ setState()
+ notify() → forEach(o → o.update())
Cas d'usage : systèmes d'événements, listeners UI, architectures réactives. En Java, c'est la base de
java.util.EventListener. En Angular, les EventEmitter et les Subject RxJS en sont des implémentations modernes.
Attention au lapsed listener problem : si les observers ne se désabonnent pas correctement, ils restent en mémoire
même si le composant est détruit — source classique de fuites mémoire.
Strategy
Le Strategy définit une famille d'algorithmes, encapsule chacun d'eux et les rend interchangeables. Il permet de faire varier l'algorithme indépendamment des clients qui l'utilisent.
Context
──────────────────────
- strategy : Strategy
+ setStrategy(Strategy)
+ executeStrategy() ──────► Strategy (interface)
+ execute(data)
▲
┌────────────┼────────────┐
StrategyA StrategyB StrategyC
+ execute() + execute() + execute()
Cas d'usage : différents algorithmes de tri selon la taille des données, différentes stratégies de pricing (prix
normal, soldes, abonnement premium), différents modes d'export (PDF, CSV, Excel). Le Strategy remplace une cascade de
if/else ou de switch dans le contexte, en déléguant le choix à la configuration ou au runtime. En Java moderne, les
lambdas et les interfaces fonctionnelles rendent le pattern très léger à implémenter.
Command
Le Command encapsule une requête sous forme d'objet, permettant de paramétrer des actions, les mettre en file d'attente, les journaliser et les annuler.
Invoker Command (interface)
────────────────── ──────────────────
- command : Command + execute()
+ setCommand(Command) + undo()
+ run() → command.execute() ▲
┌───────┴───────┐
CommandA CommandB
- receiver - receiver
+ execute() + execute()
+ undo() + undo()
│
Receiver
+ action()
Cas d'usage : systèmes d'undo/redo dans les éditeurs, files d'attente de tâches, journalisation d'audit. Le Command
est au cœur de l'architecture CQRS où chaque mutation du système est une commande explicite. La méthode undo()
optionnelle est puissante mais exige de capturer l'état avant exécution.
Template Method
Le Template Method définit le squelette d'un algorithme dans une méthode, en laissant les sous-classes redéfinir certaines étapes sans modifier sa structure.
AbstractClass
──────────────────────────────────
+ templateMethod() ← final
│
├─ step1() ← implémenté (invariant)
├─ step2() ← abstract (variant)
├─ step3() ← implémenté (invariant)
└─ hook() ← optionnel (peut être surchargé)
▲
┌────────┴────────┐
ConcreteA ConcreteB
+ step2() + step2()
+ hook()
Cas d'usage : le cycle de vie des tests JUnit (setUp, méthode de test, tearDown), le traitement de fichiers
(ouverture, parsing spécifique, fermeture), les pipelines ETL. Le Template Method est particulièrement efficace pour
imposer un protocole d'exécution tout en permettant la personnalisation des étapes variables.
Choisir le bon pattern
Le piège classique est de chercher à appliquer un pattern reconnu plutôt que de résoudre le problème réel. Quelques heuristiques :
- Variabilité à l'exécution → Strategy, State, Command
- Construction d'objets complexes → Builder, Factory Method, Abstract Factory
- Extension sans modification → Decorator, Visitor
- Simplification d'interface → Facade, Adapter
- Communication entre objets → Observer, Mediator
Un code qui n'utilise aucun pattern n'est pas nécessairement mauvais. Un code qui en utilise partout l'est presque certainement. Les patterns sont un vocabulaire partagé pour nommer des solutions connues — leur valeur réelle est la communication, pas l'implémentation mécanique.
→ À lire aussi : Les principes SOLID · Clean Architecture · Architecture hexagonale