Aller au contenu

Design Patterns

Solutions eprouvees a des problèmes recurrents de conception — a connaître pour communiquer et pour ne pas reinventer la roue.


Les trois familles

graph TD
    P["Design Patterns"]
    P --> C["Creational\nComment creer des objets"]
    P --> S["Structural\nComment assembler des objets"]
    P --> B["Behavioral\nComment les objets collaborent"]

Les patterns ne sont pas du code a copier-coller. Ce sont des vocabulaires de conception : quand vous dites "j'ai utilisé un Strategy ici", votre collegue comprend immédiatement la structure.


Creational — comment créer des objets

Factory Method

Problème : le code client ne devrait pas connaître la classe concrète a instancier — cela dépend d'une configuration ou d'un contexte.

classDiagram
    class Notifier {
        <<abstract>>
        +send(message)
    }
    class EmailNotifier
    class SmsNotifier
    class NotifierFactory {
        +create(type) Notifier
    }
    Notifier <|-- EmailNotifier
    Notifier <|-- SmsNotifier
    NotifierFactory ..> Notifier
from abc import ABC, abstractmethod

class Notifier(ABC):
    @abstractmethod
    def send(self, message: str) -> None: pass

class EmailNotifier(Notifier):
    def send(self, message: str) -> None:
        print(f"Email: {message}")

class SmsNotifier(Notifier):
    def send(self, message: str) -> None:
        print(f"SMS: {message}")

def create_notifier(channel: str) -> Notifier:
    factories = {"email": EmailNotifier, "sms": SmsNotifier}
    if channel not in factories:
        raise ValueError(f"Canal inconnu: {channel}")
    return factories[channel]()

# le client ne connait pas EmailNotifier ou SmsNotifier
notifier = create_notifier("email")
notifier.send("Votre commande est confirmee")

Singleton

Problème : certaines ressources (connexion DB, config) ne doivent exister qu'en une seule instance.

class Config:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._load()
        return cls._instance

    def _load(self):
        self.db_url = "postgresql://localhost/mydb"

Singleton = couplage global

Le Singleton est souvent un anti-pattern deguise. Il rend le code difficile à tester (état global partagé) et crée un couplage implicite. Preferez l'injection de dépendances dans la majorité des cas.


Structural — comment assembler des objets

Adapter

Problème : vous devez intégrer une librairie tierce dont l'interface ne correspond pas a celle attendue par votre code.

# interface attendue par votre systeme
class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: float, currency: str) -> str: pass

# librairie tierce avec une interface differente
class StripeClient:
    def create_charge(self, amount_cents: int, cur: str) -> dict:
        return {"id": "ch_123", "status": "succeeded"}

# adapter : convertit l'interface Stripe en PaymentGateway
class StripeAdapter(PaymentGateway):
    def __init__(self, client: StripeClient):
        self._client = client

    def charge(self, amount: float, currency: str) -> str:
        result = self._client.create_charge(int(amount * 100), currency)
        return result["id"]

Decorator

Problème : ajouter des comportements a un objet sans modifier sa classe ni créer une explosion de sous-classes.

class DataProcessor(ABC):
    @abstractmethod
    def process(self, data: str) -> str: pass

class CsvProcessor(DataProcessor):
    def process(self, data: str) -> str:
        return data.strip()

class LoggingDecorator(DataProcessor):
    def __init__(self, wrapped: DataProcessor):
        self._wrapped = wrapped

    def process(self, data: str) -> str:
        print(f"Processing {len(data)} chars")
        result = self._wrapped.process(data)
        print("Done")
        return result

class CompressionDecorator(DataProcessor):
    def __init__(self, wrapped: DataProcessor):
        self._wrapped = wrapped

    def process(self, data: str) -> str:
        result = self._wrapped.process(data)
        return result.replace("  ", " ")  # compression simplifiee

# composition a la volee
processor = LoggingDecorator(CompressionDecorator(CsvProcessor()))
processor.process("  a,b,c  ")

Behavioral — comment les objets collaborent

Strategy

Problème : un algorithme doit être interchangeable à l'exécution sans modifier l'objet qui l'utilisé.

from typing import Protocol

class SortStrategy(Protocol):
    def sort(self, data: list) -> list: ...

class QuickSort:
    def sort(self, data: list) -> list:
        return sorted(data)  # simplifie

class MergeSort:
    def sort(self, data: list) -> list:
        return sorted(data, key=lambda x: x)  # simplifie

class DataSorter:
    def __init__(self, strategy: SortStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: SortStrategy):
        self._strategy = strategy

    def sort(self, data: list) -> list:
        return self._strategy.sort(data)

sorter = DataSorter(QuickSort())
sorter.sort([3, 1, 2])
sorter.set_strategy(MergeSort())  # change d'algo sans modifier DataSorter

Observer

Problème : un objet doit notifier d'autres objets de ses changements d'état sans les connaître directement.

sequenceDiagram
    participant Subject
    participant Observer1
    participant Observer2
    Subject->>Observer1: notify(event)
    Subject->>Observer2: notify(event)
from typing import Callable

class EventBus:
    def __init__(self):
        self._listeners: dict[str, list[Callable]] = {}

    def subscribe(self, event: str, handler: Callable) -> None:
        self._listeners.setdefault(event, []).append(handler)

    def publish(self, event: str, payload: dict) -> None:
        for handler in self._listeners.get(event, []):
            handler(payload)

bus = EventBus()
bus.subscribe("order.created", lambda e: print(f"Email pour {e['id']}"))
bus.subscribe("order.created", lambda e: print(f"Stock mis a jour pour {e['id']}"))
bus.publish("order.created", {"id": "ORD-42"})

Tableau de référence

Pattern Famille Quand l'utiliser Quand l'éviter
Factory Creational Création variable selon config ou contexte Si une seule implémentation existe
Singleton Creational Ressource unique et partagee (rare) Presque toujours — preferez l'injection
Adapter Structural Intégration de librairie tierce a interface différente Si vous controllez les deux interfaces
Decorator Structural Ajout de comportements orthogonaux (log, cache, auth) Si la hiérarchie devient trop profonde
Strategy Behavioral Algorithme interchangeable à l'exécution Si une seule variante existe
Observer Behavioral Découpler producteur et consommateurs d'événements Si l'ordre d'exécution est critique

Pattern mania

Nommer quelque chose un "pattern" ne le rend pas meilleur. Un if/else lisible bat un Strategy Pattern inutile. Introduisez un pattern quand la douleur est réelle : duplication, rigidite, couplage — pas pour faire "propre".