Design Patterns

Solutions
réutilisables

Les 5 patrons de conception essentiels du GoF, implémentés en Python moderne.

Pourquoi les design patterns ?

Un design pattern est une solution générale et réutilisable à un problème récurrent de conception. Ce n'est pas du code prêt à copier, mais un plan de conception.

Vocabulaire commun — dire "c'est un Observer" communique instantanément une architecture complète.
Solutions éprouvées — éviter de réinventer des solutions à des problèmes bien documentés.
Découplage — la plupart des patterns réduisent les dépendances entre classes.
Extensibilité — faciliter l'ajout de fonctionnalités sans modifier le code existant (principe Ouvert/Fermé).
⚠️

Sur-ingénierie : ne pas appliquer un pattern si le problème ne l'exige pas. Un pattern mal appliqué complexifie le code inutilement. Partir simple, refactorer vers un pattern quand le besoin devient réel.

Les 3 catégories GoF

CatégoriePréoccupationPatterns (5 essentiels)
Créationnel Comment créer des objets Singleton, Factory Method, Abstract Factory, Builder, Prototype
Structurel Comment assembler les classes Decorator, Adapter, Facade, Composite, Proxy, Bridge, Flyweight
Comportemental Comment les objets collaborent Observer, Strategy, Command, Iterator, Template Method…

Singleton

Singleton Créationnel

Garantir qu'une classe n'a qu'une seule instance et fournir un point d'accès global à cette instance.

┌─────────────────────────┐ │ <<Singleton>> │ │ ConfigApp │ ├─────────────────────────┤ │ - _instance: ConfigApp │ ├─────────────────────────┤ │ + get_instance() │ │ + get(key) │ └─────────────────────────┘
Python — __new__
class ConfigApp:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._data = {}
        return cls._instance

    def set(self, clé, val):
        self._data[clé] = val

    def get(self, clé):
        return self._data.get(clé)

# Utilisation
a = ConfigApp()
b = ConfigApp()
a is b   # True — même objet
a.set("debug", True)
b.get("debug")   # True
Python — module (plus pythonique)
# config.py
# Un module Python EST un singleton naturel
# Importé une seule fois, partagé partout

_data = {}

def set(clé, val):
    _data[clé] = val

def get(clé, défaut=None):
    return _data.get(clé, défaut)

# main.py
import config
config.set("debug", True)

# autre.py
import config
config.get("debug")   # True
💡

En Python, préférer l'approche module. C'est plus simple, thread-safe par défaut, et idiomatique.

Quand l'utiliser
  • Configuration globale de l'application
  • Connexion à une base de données (pool)
  • Gestionnaire de logs centralisé
  • Cache partagé
✓ Avantages
  • Instance unique garantie
  • Point d'accès global
  • Initialisé à la première utilisation
✗ Inconvénients
  • Difficile à tester (état global)
  • Couplage fort si abusé
  • Problèmes de thread-safety en Python classique

Factory Method

Factory Method Créationnel

Définir une interface pour créer un objet, mais laisser les sous-classes décider quelle classe instancier. Déléguer la création à des sous-classes.

Animal (ABC) Créateur (ABC) ┌──────────────┐ ┌───────────────────────┐ │ + parler() │◄────── │ + créer_animal() [FM] │ └──────┬───────┘ │ + déclencher_son() │ │ └───────────┬───────────┘ ┌─────┴──────┐ ┌──────┴──────┐ ▼ ▼ ▼ ▼ Chien Chat ChatCreateur ChienCreateur
Python
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def parler(self) -> str: ...

class Chien(Animal):
    def parler(self): return "Woof!"

class Chat(Animal):
    def parler(self): return "Miaou!"

# Créateur avec Factory Method
class CréateurAnimal(ABC):
    @abstractmethod
    def créer_animal(self) -> Animal: ...

    def déclencher_son(self):
        a = self.créer_animal()
        print(a.parler())

class CréateurChien(CréateurAnimal):
    def créer_animal(self): return Chien()

class CréateurChat(CréateurAnimal):
    def créer_animal(self): return Chat()
Python — version simple (fonction factory)
# Version plus légère et courante en Python
def créer_animal(type_animal: str) -> Animal:
    animaux = {
        "chien": Chien,
        "chat":  Chat,
    }
    if type_animal not in animaux:
        raise ValueError(
            f"Type inconnu : {type_animal}")
    return animaux[type_animal]()

# Le client ne connaît pas Chien ni Chat
a = créer_animal("chien")
a.parler()   # "Woof!"
ℹ️

La version avec dictionnaire {"clé": Classe} est la plus pythonique. Ajouter un nouveau type = une ligne dans le dict, sans modifier le reste.

Quand l'utiliser
  • Créer des objets selon un paramètre (type, config)
  • Isoler le code client de la création concrète
  • Faciliter l'ajout de nouveaux types sans modifier le code existant

Decorator

Decorator Structurel

Attacher dynamiquement des comportements supplémentaires à un objet, sans modifier sa classe. Alternative à l'héritage pour étendre les fonctionnalités.

⚠️

Attention : le Decorator GoF (patron de conception sur les objets) est différent du décorateur Python (@syntaxe). Les deux peuvent coexister — Python utilise le même principe.

Version GoF — décorateur d'objet

Python
from abc import ABC, abstractmethod

class Café(ABC):
    @abstractmethod
    def coût(self) -> float: ...
    @abstractmethod
    def description(self) -> str: ...

class EspressoSimple(Café):
    def coût(self): return 1.50
    def description(self): return "Espresso"

# Décorateur de base
class DécoCafé(Café):
    def __init__(self, café: Café):
        self._café = café
    def coût(self): return self._café.coût()
    def description(self): return self._café.description()

class AvecLait(DécoCafé):
    def coût(self): return super().coût() + 0.30
    def description(self): return super().description() + ", lait"

class AvecSirop(DécoCafé):
    def coût(self): return super().coût() + 0.50
    def description(self): return super().description() + ", sirop"

# Composition à la volée
commande = AvecSirop(AvecLait(EspressoSimple()))
commande.description()  # "Espresso, lait, sirop"
commande.coût()          # 2.30

Version Python — décorateur de fonction

Python
import time
from functools import wraps

# Mesurer le temps d'exécution
def chronométrer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        début = time.perf_counter()
        résultat = func(*args, **kwargs)
        durée = time.perf_counter() - début
        print(f"{func.__name__}: {durée:.4f}s")
        return résultat
    return wrapper

# Logger les appels
def logger(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"→ Appel : {func.__name__}")
        res = func(*args, **kwargs)
        print(f"← Retour : {res}")
        return res
    return wrapper

# Empilable !
@chronométrer
@logger
def traiter(x):
    return x * 2
Quand l'utiliser
  • Ajouter logging, cache, validation sans toucher à la classe
  • Combiner des comportements optionnels (café + lait + sirop)
  • Quand l'héritage créerait une explosion de sous-classes

Observer

Observer Comportemental

Définir une dépendance un-à-plusieurs entre objets : quand un objet (le sujet) change d'état, tous ses observateurs sont notifiés automatiquement.

Sujet Observateur ┌──────────────────────┐ ┌─────────────────────┐ │ - observateurs: list │ │ + actualiser(sujet) │ │ + abonner(obs) │──────►└─────────────────────┘ │ + désabonner(obs) │ △ │ + notifier() │ ┌─────────┴──────────┐ │ + état │ ▼ ▼ └──────────────────────┘ LogObservateur EmailObservateur
Python
from abc import ABC, abstractmethod
from typing import List

class Observateur(ABC):
    @abstractmethod
    def actualiser(self, sujet) -> None: ...

class Sujet:
    def __init__(self):
        self._observateurs: List[Observateur] = []
        self._état = None

    def abonner(self, obs: Observateur):
        self._observateurs.append(obs)

    def désabonner(self, obs: Observateur):
        self._observateurs.remove(obs)

    def notifier(self):
        for obs in self._observateurs:
            obs.actualiser(self)

    @property
    def état(self): return self._état

    @état.setter
    def état(self, val):
        self._état = val
        self.notifier()   # notification automatique

# Observateurs concrets
class LogObservateur(Observateur):
    def actualiser(self, sujet):
        print(f"[LOG] État changé : {sujet.état}")

class EmailObservateur(Observateur):
    def actualiser(self, sujet):
        print(f"[EMAIL] Alerte : nouvel état = {sujet.état}")

# Utilisation
capteur = Sujet()
capteur.abonner(LogObservateur())
capteur.abonner(EmailObservateur())
capteur.état = "ALERTE"
# [LOG] État changé : ALERTE
# [EMAIL] Alerte : nouvel état = ALERTE
Quand l'utiliser
  • Interface graphique : bouton → plusieurs composants réagissent
  • Système d'événements / callbacks
  • Modèle MVC : le Modèle notifie la Vue
  • Capteurs IoT, flux de données en temps réel
✓ Avantages
  • Découplage sujet / observateurs
  • Ajout d'observateurs sans modifier le sujet
  • Notification automatique
✗ Inconvénients
  • Ordre de notification non garanti
  • Fuites mémoire si oubli de désabonner
  • Cascades d'événements difficiles à déboguer

Strategy

Strategy Comportemental

Définir une famille d'algorithmes, les encapsuler chacun dans une classe, et les rendre interchangeables. Permet de changer d'algorithme à l'exécution.

Python — version classe
from abc import ABC, abstractmethod

class StratégieTri(ABC):
    @abstractmethod
    def trier(self, données: list) -> list: ...

class TriRapide(StratégieTri):
    def trier(self, données):
        return sorted(données)   # Timsort

class TriInverse(StratégieTri):
    def trier(self, données):
        return sorted(données, reverse=True)

# Contexte — utilise la stratégie
class Trieur:
    def __init__(self, stratégie: StratégieTri):
        self._stratégie = stratégie

    def changer_stratégie(self, s: StratégieTri):
        self._stratégie = s

    def exécuter(self, données):
        return self._stratégie.trier(données)

t = Trieur(TriRapide())
t.exécuter([3,1,2])   # [1, 2, 3]
t.changer_stratégie(TriInverse())
t.exécuter([3,1,2])   # [3, 2, 1]
Python — version fonctionnelle (pythonique)
# En Python, les fonctions sont des objets
# → Strategy avec Callable, sans classes
from typing import Callable

def tri_asc(d): return sorted(d)
def tri_desc(d): return sorted(d, reverse=True)
def tri_longueur(d): return sorted(d, key=len)

class Trieur:
    def __init__(self,
                  stratégie: Callable = tri_asc):
        self.stratégie = stratégie

    def exécuter(self, données):
        return self.stratégie(données)

# Changement à l'exécution
t = Trieur()
t.exécuter(["banane", "kiwi", "pomme"])
t.stratégie = tri_longueur
t.exécuter(["banane", "kiwi", "pomme"])
# ["kiwi", "pomme", "banane"]
💡

Python supporte nativement ce pattern via les fonctions de première classe. La version fonctionnelle est plus concise et souvent préférable.

Quand l'utiliser
  • Plusieurs algorithmes pour une même tâche (tri, compression, validation)
  • Remplacer des if/elif longs pour sélectionner un comportement
  • Permettre à l'utilisateur de configurer un algorithme

Cheat sheet

PatternCatégorieProblème résoluMot-clé
SingletonCréationnelInstance unique globaleInstance unique
Factory MethodCréationnelCréer sans connaître la classe concrèteDéléguer la création
DecoratorStructurelAjouter des comportements sans héritageEmballage dynamique
ObserverComportementalNotifier automatiquement des dépendantsÉvénement / callback
StrategyComportementalAlgorithmes interchangeablesComportement variable