← Retour au blog
Architecture

Les Design Patterns du GoF : guide complet avec schémas

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

Vous avez un projet en tête ?

Parlons de vos enjeux et voyons comment Gotan peut vous accompagner.

Contactez-nous