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".