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.
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égorie | Préoccupation | Patterns (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
Garantir qu'une classe n'a qu'une seule instance et fournir un point d'accès global à cette instance.
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
# 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
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.
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()
# 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
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
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
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
Définir une dépendance un-à-plusieurs entre objets : quand un objet (le sujet) change d'état, tous ses observateurs sont notifiés automatiquement.
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
Définir une famille d'algorithmes, les encapsuler chacun dans une classe, et les rendre interchangeables. Permet de changer d'algorithme à l'exécution.
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]
# 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/eliflongs pour sélectionner un comportement - Permettre à l'utilisateur de configurer un algorithme
Cheat sheet
| Pattern | Catégorie | Problème résolu | Mot-clé |
|---|---|---|---|
| Singleton | Créationnel | Instance unique globale | Instance unique |
| Factory Method | Créationnel | Créer sans connaître la classe concrète | Déléguer la création |
| Decorator | Structurel | Ajouter des comportements sans héritage | Emballage dynamique |
| Observer | Comportemental | Notifier automatiquement des dépendants | Événement / callback |
| Strategy | Comportemental | Algorithmes interchangeables | Comportement variable |