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.

​
type Tri = (a: Article, b: Article) => number;

const parPrix: Tri = (a, b) => a.prix - b.prix;
const parNom: Tri  = (a, b) => a.nom.localeCompare(b.nom);

class Catalogue {
  constructor(private readonly articles: Article[]) {}

  trier(strategie: Tri): Article[] {
    return [...this.articles].sort(strategie);
  }
}
from operator import attrgetter
from typing import Callable

Tri = Callable[[Article], object]        # Python trie par clé, pas par comparaison

par_prix = attrgetter("prix")
par_nom = attrgetter("nom")


class Catalogue:
    def __init__(self, articles: list[Article]) -> None:
        self._articles = articles

    def trier(self, strategie: Tri) -> list[Article]:
        return sorted(self._articles, key=strategie)
use std::cmp::Ordering;

type Tri = fn(&Article, &Article) -> Ordering;

fn par_prix(a: &Article, b: &Article) -> Ordering { a.prix.cmp(&b.prix) }
fn par_nom(a: &Article, b: &Article) -> Ordering { a.nom.cmp(&b.nom) }

struct Catalogue {
    articles: Vec<Article>,
}

impl Catalogue {
    fn trier(&self, strategie: Tri) -> Vec<Article> {
        let mut copie = self.articles.clone();
        copie.sort_by(strategie);
        copie
    }
}
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.

​
class Sujet<T> {
  private observateurs: ((valeur: T) => void)[] = [];

  souscrire(observateur: (valeur: T) => void): () => void {
    this.observateurs.push(observateur);
    return () => {                       // Désinscription
      this.observateurs = this.observateurs.filter((o) => o !== observateur);
    };
  }

  emettre(valeur: T): void {
    this.observateurs.forEach((o) => o(valeur));
  }
}
from typing import Callable, Generic, TypeVar

T = TypeVar("T")


class Sujet(Generic[T]):
    def __init__(self) -> None:
        self._observateurs: list[Callable[[T], None]] = []

    def souscrire(self, observateur: Callable[[T], None]) -> Callable[[], None]:
        self._observateurs.append(observateur)
        return lambda: self._observateurs.remove(observateur)   # Désinscription

    def emettre(self, valeur: T) -> None:
        for observateur in list(self._observateurs):            # Copie : la liste
            observateur(valeur)                                 # peut changer
type Abonnement = u64;

struct Sujet<T> {
    observateurs: Vec<(Abonnement, Box<dyn Fn(&T)>)>,
    prochain_id: Abonnement,
}

impl<T> Sujet<T> {
    fn souscrire(&mut self, observateur: impl Fn(&T) + 'static) -> Abonnement {
        let id = self.prochain_id;
        self.prochain_id += 1;
        self.observateurs.push((id, Box::new(observateur)));
        id                              // Deux closures n'étant pas comparables,
    }                                   // la désinscription passe par un jeton

    fn desinscrire(&mut self, id: Abonnement) {
        self.observateurs.retain(|(autre, _)| *autre != id);
    }

    fn emettre(&self, valeur: &T) {
        for (_, observateur) in &self.observateurs {
            observateur(valeur);
        }
    }
}
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.

​
function creerParseur(fichier: string): Parseur {
  const extension = fichier.split('.').pop();
  switch (extension) {
    case 'json': return new ParseurJson();
    case 'csv':  return new ParseurCsv();
    case 'xml':  return new ParseurXml();
    default: throw new Error(`Format inconnu : ${extension}`);
  }
}
from pathlib import Path


def creer_parseur(fichier: str) -> Parseur:
    match Path(fichier).suffix:
        case ".json": return ParseurJson()
        case ".csv":  return ParseurCsv()
        case ".xml":  return ParseurXml()
        case autre:   raise ValueError(f"Format inconnu : {autre}")
fn creer_parseur(fichier: &str) -> Result<Box<dyn Parseur>, FormatInconnu> {
    match fichier.rsplit_once('.') {
        Some((_, "json")) => Ok(Box::new(ParseurJson)),
        Some((_, "csv"))  => Ok(Box::new(ParseurCsv)),
        Some((_, "xml"))  => Ok(Box::new(ParseurXml)),
        _ => Err(FormatInconnu(fichier.to_owned())),
    }
}

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.

​
// Le domaine veut ceci.
interface PasserelleDePaiement {
  debiter(montantEnCentimes: number, carte: Carte): Promise<Recu>;
}

// Le prestataire fournit cela.
class StripeAdapter implements PasserelleDePaiement {
  constructor(private readonly stripe: StripeSdk) {}

  async debiter(montantEnCentimes: number, carte: Carte): Promise<Recu> {
    const intent = await this.stripe.paymentIntents.create({
      amount: montantEnCentimes,
      currency: 'eur',
      payment_method: carte.token,
    });
    return { reference: intent.id, statut: intent.status === 'succeeded' ? 'ok' : 'ko' };
  }
}
from typing import Protocol

# Le domaine veut ceci.


class PasserelleDePaiement(Protocol):
    def debiter(self, montant_en_centimes: int, carte: Carte) -> Recu: ...


# Le prestataire fournit cela.


class StripeAdapter:
    def __init__(self, stripe: StripeSdk) -> None:
        self._stripe = stripe

    def debiter(self, montant_en_centimes: int, carte: Carte) -> Recu:
        intent = self._stripe.PaymentIntent.create(
            amount=montant_en_centimes,
            currency="eur",
            payment_method=carte.token,
        )
        return Recu(
            reference=intent.id,
            statut="ok" if intent.status == "succeeded" else "ko",
        )
// Le domaine veut ceci.
trait PasserelleDePaiement {
    fn debiter(&self, montant_en_centimes: u64, carte: &Carte) -> Result<Recu, Erreur>;
}

// Le prestataire fournit cela.
struct StripeAdapter {
    stripe: StripeSdk,
}

impl PasserelleDePaiement for StripeAdapter {
    fn debiter(&self, montant_en_centimes: u64, carte: &Carte) -> Result<Recu, Erreur> {
        let intent = self.stripe.creer_payment_intent(PaymentIntentParams {
            amount: montant_en_centimes,
            currency: "eur",
            payment_method: &carte.token,
        })?;

        Ok(Recu {
            reference: intent.id,
            statut: match intent.status.as_str() {
                "succeeded" => Statut::Ok,
                _ => Statut::Ko,
            },
        })
    }
}

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.

​
interface Depot { lire(cle: string): Promise<string>; }

class DepotAvecCache implements Depot {
  private cache = new Map<string, string>();

  constructor(private readonly source: Depot) {}

  async lire(cle: string): Promise<string> {
    if (!this.cache.has(cle)) this.cache.set(cle, await this.source.lire(cle));
    return this.cache.get(cle)!;
  }
}

class DepotAvecJournal implements Depot {
  constructor(private readonly source: Depot) {}

  async lire(cle: string): Promise<string> {
    console.time(cle);
    const valeur = await this.source.lire(cle);
    console.timeEnd(cle);
    return valeur;
  }
}

const depot = new DepotAvecJournal(new DepotAvecCache(new DepotS3()));
from time import perf_counter
from typing import Protocol


class Depot(Protocol):
    def lire(self, cle: str) -> str: ...


class DepotAvecCache:
    def __init__(self, source: Depot) -> None:
        self._source = source
        self._cache: dict[str, str] = {}

    def lire(self, cle: str) -> str:
        if cle not in self._cache:
            self._cache[cle] = self._source.lire(cle)
        return self._cache[cle]


class DepotAvecJournal:
    def __init__(self, source: Depot) -> None:
        self._source = source

    def lire(self, cle: str) -> str:
        debut = perf_counter()
        valeur = self._source.lire(cle)
        print(f"{cle} : {perf_counter() - debut:.3f}s")
        return valeur


depot = DepotAvecJournal(DepotAvecCache(DepotS3()))
use std::cell::RefCell;
use std::collections::HashMap;
use std::time::Instant;

trait Depot {
    fn lire(&self, cle: &str) -> String;
}

struct DepotAvecCache<D: Depot> {
    source: D,
    cache: RefCell<HashMap<String, String>>,   // &self impose la mutabilité interne
}

impl<D: Depot> Depot for DepotAvecCache<D> {
    fn lire(&self, cle: &str) -> String {
        if let Some(valeur) = self.cache.borrow().get(cle) {
            return valeur.clone();
        }
        let valeur = self.source.lire(cle);
        self.cache.borrow_mut().insert(cle.to_owned(), valeur.clone());
        valeur
    }
}

struct DepotAvecJournal<D: Depot> {
    source: D,
}

impl<D: Depot> Depot for DepotAvecJournal<D> {
    fn lire(&self, cle: &str) -> String {
        let debut = Instant::now();
        let valeur = self.source.lire(cle);
        println!("{cle} : {:?}", debut.elapsed());
        valeur
    }
}

let depot = DepotAvecJournal {
    source: DepotAvecCache {
        source: DepotS3,
        cache: RefCell::default(),
    },
};

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.

​
class ServiceInscription {          // Façade
  constructor(
    private readonly utilisateurs: UtilisateurRepository,
    private readonly courriels: Mailer,
    private readonly analytique: Tracker,
  ) {}

  async inscrire(formulaire: Formulaire): Promise<Utilisateur> {
    const utilisateur = await this.utilisateurs.creer(formulaire);
    await this.courriels.envoyerBienvenue(utilisateur);
    this.analytique.evenement('inscription', utilisateur.id);
    return utilisateur;
  }
}
class ServiceInscription:                    # Façade
    def __init__(
        self,
        utilisateurs: UtilisateurRepository,
        courriels: Mailer,
        analytique: Tracker,
    ) -> None:
        self._utilisateurs = utilisateurs
        self._courriels = courriels
        self._analytique = analytique

    def inscrire(self, formulaire: Formulaire) -> Utilisateur:
        utilisateur = self._utilisateurs.creer(formulaire)
        self._courriels.envoyer_bienvenue(utilisateur)
        self._analytique.evenement("inscription", utilisateur.id)
        return utilisateur
struct ServiceInscription {          // Façade
    utilisateurs: Box<dyn UtilisateurRepository>,
    courriels: Box<dyn Mailer>,
    analytique: Box<dyn Tracker>,
}

impl ServiceInscription {
    fn inscrire(&self, formulaire: Formulaire) -> Result<Utilisateur, Erreur> {
        let utilisateur = self.utilisateurs.creer(formulaire)?;
        self.courriels.envoyer_bienvenue(&utilisateur)?;
        self.analytique.evenement("inscription", utilisateur.id);
        Ok(utilisateur)
    }
}

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.

​
class Configuration {
  private static instance: Configuration | null = null;

  private constructor(readonly valeurs: Record<string, string>) {}

  static obtenir(): Configuration {
    Configuration.instance ??= new Configuration(process.env as Record<string, string>);
    return Configuration.instance;
  }
}
import os
from dataclasses import dataclass
from functools import lru_cache


@dataclass(frozen=True)
class Configuration:
    valeurs: dict[str, str]


@lru_cache(maxsize=1)
def configuration() -> Configuration:
    return Configuration(dict(os.environ))


# Un module Python n'étant importé qu'une fois, une simple variable de module est
# déjà un singleton — le patron s'écrit rarement en toutes lettres.
use std::collections::HashMap;
use std::sync::OnceLock;

struct Configuration {
    valeurs: HashMap<String, String>,
}

fn configuration() -> &'static Configuration {
    static INSTANCE: OnceLock<Configuration> = OnceLock::new();
    INSTANCE.get_or_init(|| Configuration {
        valeurs: std::env::vars().collect(),
    })
}

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.

​
abstract class ImportCsv {
  importer(fichier: string): Rapport {       // La méthode « patron »
    const lignes = this.lire(fichier);
    const valides = lignes.filter((l) => this.valider(l));
    valides.forEach((l) => this.enregistrer(l));
    return { total: lignes.length, importes: valides.length };
  }

  protected lire(fichier: string): Ligne[] { /* commun à tous */ }

  protected abstract valider(ligne: Ligne): boolean;    // À fournir
  protected abstract enregistrer(ligne: Ligne): void;   // À fournir
}
from abc import ABC, abstractmethod


class ImportCsv(ABC):
    def importer(self, fichier: str) -> Rapport:        # La méthode « patron »
        lignes = self._lire(fichier)
        valides = [l for l in lignes if self._valider(l)]
        for ligne in valides:
            self._enregistrer(ligne)
        return Rapport(total=len(lignes), importes=len(valides))

    def _lire(self, fichier: str) -> list[Ligne]:
        ...                                             # Commun à tous

    @abstractmethod
    def _valider(self, ligne: Ligne) -> bool: ...       # À fournir

    @abstractmethod
    def _enregistrer(self, ligne: Ligne) -> None: ...   # À fournir
trait ImportCsv {
    fn importer(&self, fichier: &str) -> Rapport {      // La méthode « patron »
        let lignes = self.lire(fichier);
        let valides: Vec<&Ligne> = lignes.iter().filter(|l| self.valider(l)).collect();
        for ligne in &valides {
            self.enregistrer(ligne);
        }
        Rapport { total: lignes.len(), importes: valides.len() }
    }

    fn lire(&self, fichier: &str) -> Vec<Ligne> {
        /* commun à tous */
    }

    fn valider(&self, ligne: &Ligne) -> bool;           // À fournir
    fn enregistrer(&self, ligne: &Ligne);               // À fournir
}

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.

​
interface Commande { executer(): void; annuler(): void; }

class Historique {
  private pile: Commande[] = [];

  lancer(commande: Commande): void {
    commande.executer();
    this.pile.push(commande);
  }

  defaire(): void { this.pile.pop()?.annuler(); }
}
from typing import Protocol


class Commande(Protocol):
    def executer(self) -> None: ...
    def annuler(self) -> None: ...


class Historique:
    def __init__(self) -> None:
        self._pile: list[Commande] = []

    def lancer(self, commande: Commande) -> None:
        commande.executer()
        self._pile.append(commande)

    def defaire(self) -> None:
        if self._pile:
            self._pile.pop().annuler()
trait Commande {
    fn executer(&self, doc: &mut Document);   // La cible est explicite : aucune
    fn annuler(&self, doc: &mut Document);    // commande ne détient le document
}

struct Historique {
    pile: Vec<Box<dyn Commande>>,
}

impl Historique {
    fn lancer(&mut self, commande: Box<dyn Commande>, doc: &mut Document) {
        commande.executer(doc);
        self.pile.push(commande);
    }

    fn defaire(&mut self, doc: &mut Document) {
        if let Some(commande) = self.pile.pop() {
            commande.annuler(doc);
        }
    }
}

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.

​
new RequeteBuilder()
  .table('commandes')
  .ou('statut', 'validee')
  .trierPar('date', 'desc')
  .limite(20)
  .construire();
(
    RequeteBuilder()
    .table("commandes")
    .ou("statut", "validee")
    .trier_par("date", "desc")
    .limite(20)
    .construire()
)

# Les arguments nommés à valeur par défaut couvrent déjà la plupart des cas :
requete(table="commandes", tri=("date", "desc"), limite=20)
RequeteBuilder::new()
    .table("commandes")
    .ou("statut", "validee")
    .trier_par("date", Ordre::Desc)
    .limite(20)
    .construire();

// Chaque méthode consomme et renvoie `self` : la valeur intermédiaire n'est
// plus utilisable, et oublier `construire()` déclenche un avertissement.
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.

​
interface Noeud { taille(): number; }

class Fichier implements Noeud {
  constructor(private readonly octets: number) {}
  taille(): number { return this.octets; }
}

class Dossier implements Noeud {
  constructor(private readonly enfants: Noeud[]) {}
  taille(): number { return this.enfants.reduce((t, e) => t + e.taille(), 0); }
}
from typing import Protocol


class Noeud(Protocol):
    def taille(self) -> int: ...


class Fichier:
    def __init__(self, octets: int) -> None:
        self._octets = octets

    def taille(self) -> int:
        return self._octets


class Dossier:
    def __init__(self, enfants: list[Noeud]) -> None:
        self._enfants = enfants

    def taille(self) -> int:
        return sum(enfant.taille() for enfant in self._enfants)
enum Noeud {
    Fichier(u64),
    Dossier(Vec<Noeud>),
}

impl Noeud {
    fn taille(&self) -> u64 {
        match self {
            Noeud::Fichier(octets) => *octets,
            Noeud::Dossier(enfants) => enfants.iter().map(Noeud::taille).sum(),
        }
    }
}

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.

​
class Arbre implements Iterable<number> {
  constructor(private readonly racine: Noeud) {}

  *[Symbol.iterator](): Iterator<number> {
    yield* this.parcourir(this.racine);       // Parcours paresseux
  }

  private *parcourir(n: Noeud): Generator<number> {
    yield n.valeur;
    for (const enfant of n.enfants) yield* this.parcourir(enfant);
  }
}

for (const valeur of new Arbre(racine)) { /* ... */ }
from typing import Iterator


class Arbre:
    def __init__(self, racine: Noeud) -> None:
        self._racine = racine

    def __iter__(self) -> Iterator[int]:
        yield from self._parcourir(self._racine)     # Parcours paresseux

    def _parcourir(self, n: Noeud) -> Iterator[int]:
        yield n.valeur
        for enfant in n.enfants:
            yield from self._parcourir(enfant)


for valeur in Arbre(racine):
    ...
struct Parcours<'a> {
    pile: Vec<&'a Noeud>,          // L'état du parcours, écrit à la main
}

impl<'a> Iterator for Parcours<'a> {
    type Item = u32;

    fn next(&mut self) -> Option<u32> {
        let noeud = self.pile.pop()?;
        self.pile.extend(noeud.enfants.iter().rev());
        Some(noeud.valeur)
    }
}

impl Noeud {
    fn iter(&self) -> Parcours<'_> {
        Parcours { pile: vec![self] }
    }
}

for valeur in racine.iter() { /* ... */ }

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.

​
interface EtatCommande {
  payer(c: Contexte): void;
  expedier(c: Contexte): void;
}

class EnAttente implements EtatCommande {
  payer(c: Contexte): void { c.etat = new Payee(); }
  expedier(): void { throw new Error('Commande non payée'); }
}
from typing import Protocol


class EtatCommande(Protocol):
    def payer(self, c: Contexte) -> None: ...
    def expedier(self, c: Contexte) -> None: ...


class EnAttente:
    def payer(self, c: Contexte) -> None:
        c.etat = Payee()

    def expedier(self, c: Contexte) -> None:
        raise RuntimeError("Commande non payée")
trait EtatCommande {
    fn payer(self: Box<Self>) -> Result<Box<dyn EtatCommande>, Interdit>;
    fn expedier(self: Box<Self>) -> Result<Box<dyn EtatCommande>, Interdit>;
}

struct EnAttente;
struct Payee;

impl EtatCommande for EnAttente {
    fn payer(self: Box<Self>) -> Result<Box<dyn EtatCommande>, Interdit> {
        Ok(Box::new(Payee))
    }

    fn expedier(self: Box<Self>) -> Result<Box<dyn EtatCommande>, Interdit> {
        Err(Interdit("Commande non payée"))
    }
}

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.

​
interface FabriqueSql {
  connexion(): Connexion;
  constructeurRequete(): RequeteBuilder;   // Le dialecte doit correspondre
}

class FabriquePostgres implements FabriqueSql { /* ... */ }
class FabriqueMySql implements FabriqueSql { /* ... */ }
from typing import Protocol


class FabriqueSql(Protocol):
    def connexion(self) -> Connexion: ...
    def constructeur_requete(self) -> RequeteBuilder: ...  # Le dialecte doit
                                                           # correspondre

class FabriquePostgres: ...
class FabriqueMySql: ...
trait FabriqueSql {
    type Connexion: Connexion;
    type Requete: RequeteBuilder;          // Types associés : la cohérence de la
                                           // famille est vérifiée à la compilation
    fn connexion(&self) -> Self::Connexion;
    fn constructeur_requete(&self) -> Self::Requete;
}

struct FabriquePostgres;
struct FabriqueMySql;

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

#PatronFamilleIntention en une ligne
1StrategyComportementalAlgorithmes interchangeables
2ObserverComportementalNotification des dépendants
3Factory MethodCréateurDéléguer le choix de la classe concrète
4AdapterStructurelTraduire une interface étrangère
5DecoratorStructurelEmpiler des responsabilités
6FacadeStructurelSimplifier l’accès à un sous-système
7SingletonCréateurInstance unique et globale
8Template MethodComportementalSquelette d’algorithme à trous
9CommandComportementalRéifier une action
10BuilderCréateurConstruction par étapes
11CompositeStructurelTraiter l’arbre comme la feuille
12IteratorComportementalParcours sans exposer la structure
13ProxyStructurelContrôler l’accès à un objet
14StateComportementalComportement piloté par l’état
15Abstract FactoryCréateurFamilles d’objets cohérentes
16Chain of ResponsibilityComportementalChaîne de gestionnaires
17MediatorComportementalCentraliser les interactions
18PrototypeCréateurCréer par clonage
19VisitorComportementalAlgorithme externe à la structure
20BridgeStructurelAbstraction et implémentation séparées
21MementoComportementalInstantané restaurable
22FlyweightStructurelPartage massif pour la mémoire
23InterpreterComportementalÉ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