Les Design Patterns Essentiels : Guide du Développeur
Les design patterns sont des solutions éprouvées à des problèmes récurrents en programmation. Ils représentent les meilleures pratiques développées par des générations de programmeurs.
Qu'est-ce qu'un Design Pattern ?
Un design pattern décrit :
- Un problème récurrent
- Une solution générale réutilisable
- Les conséquences de cette solution
Les Patterns Essentiels
1. Singleton Pattern
Problème : Garantir qu'une classe n'a qu'une seule instance.
class DatabaseConnection:
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not self._initialized:
self.connection = self._create_connection()
self._initialized = True
def _create_connection(self):
# Simulation de connexion
return "Connection établie"
def query(self, sql):
return f"Exécution de : {sql}"
# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True
Version thread-safe :
import threading
class ThreadSafeSingleton:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
2. Factory Pattern
Problème : Créer des objets sans spécifier leur classe exacte.
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def make_sound(self):
pass
class Dog(Animal):
def make_sound(self):
return "Woof!"
class Cat(Animal):
def make_sound(self):
return "Meow!"
class Bird(Animal):
def make_sound(self):
return "Tweet!"
class AnimalFactory:
@staticmethod
def create_animal(animal_type: str) -> Animal:
animals = {
'dog': Dog,
'cat': Cat,
'bird': Bird
}
animal_class = animals.get(animal_type.lower())
if animal_class:
return animal_class()
else:
raise ValueError(f"Type d'animal inconnu: {animal_type}")
# Usage
factory = AnimalFactory()
dog = factory.create_animal('dog')
cat = factory.create_animal('cat')
print(dog.make_sound()) # Woof!
print(cat.make_sound()) # Meow!
3. Observer Pattern
Problème : Notifier plusieurs objets lors d'un changement d'état.
from abc import ABC, abstractmethod
from typing import List
class Observer(ABC):
@abstractmethod
def update(self, message: str):
pass
class Subject:
def __init__(self):
self._observers: List[Observer] = []
def attach(self, observer: Observer):
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer: Observer):
if observer in self._observers:
self._observers.remove(observer)
def notify(self, message: str):
for observer in self._observers:
observer.update(message)
class EmailNotifier(Observer):
def __init__(self, email: str):
self.email = email
def update(self, message: str):
print(f"Email envoyé à {self.email}: {message}")
class SMSNotifier(Observer):
def __init__(self, phone: str):
self.phone = phone
def update(self, message: str):
print(f"SMS envoyé au {self.phone}: {message}")
class NewsPublisher(Subject):
def __init__(self):
super().__init__()
self._latest_news = ""
def publish_news(self, news: str):
self._latest_news = news
self.notify(f"Nouvelle actualité: {news}")
# Usage
publisher = NewsPublisher()
email_notifier = EmailNotifier("user@example.com")
sms_notifier = SMSNotifier("+33612345678")
publisher.attach(email_notifier)
publisher.attach(sms_notifier)
publisher.publish_news("Nouvelle fonctionnalité lancée!")
4. Strategy Pattern
Problème : Choisir un algorithme à l'exécution.
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> str:
pass
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str):
self.card_number = card_number
def pay(self, amount: float) -> str:
return f"Paiement de {amount}€ par carte {self.card_number[-4:]}"
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> str:
return f"Paiement de {amount}€ via PayPal ({self.email})"
class BankTransferPayment(PaymentStrategy):
def __init__(self, account_number: str):
self.account_number = account_number
def pay(self, amount: float) -> str:
return f"Virement de {amount}€ vers {self.account_number}"
class ShoppingCart:
def __init__(self):
self.items = []
self.payment_strategy = None
def add_item(self, item: str, price: float):
self.items.append((item, price))
def set_payment_strategy(self, strategy: PaymentStrategy):
self.payment_strategy = strategy
def checkout(self):
total = sum(price for _, price in self.items)
if self.payment_strategy:
return self.payment_strategy.pay(total)
else:
raise ValueError("Aucune stratégie de paiement définie")
# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Souris", 29.99)
# Paiement par carte
cart.set_payment_strategy(CreditCardPayment("1234567812345678"))
print(cart.checkout())
# Changement de stratégie
cart.set_payment_strategy(PayPalPayment("user@example.com"))
print(cart.checkout())
5. Decorator Pattern
Problème : Ajouter des fonctionnalités à un objet dynamiquement.
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
class SimpleCoffee(Coffee):
def cost(self) -> float:
return 2.0
def description(self) -> str:
return "Café simple"
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.5
def description(self) -> str:
return self._coffee.description() + " + lait"
class SugarDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.2
def description(self) -> str:
return self._coffee.description() + " + sucre"
class VanillaDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.8
def description(self) -> str:
return self._coffee.description() + " + vanille"
# Usage
coffee = SimpleCoffee()
print(f"{coffee.description()}: {coffee.cost()}€")
# Ajout de lait
coffee_with_milk = MilkDecorator(coffee)
print(f"{coffee_with_milk.description()}: {coffee_with_milk.cost()}€")
# Ajout de sucre et vanille
fancy_coffee = VanillaDecorator(SugarDecorator(coffee_with_milk))
print(f"{fancy_coffee.description()}: {fancy_coffee.cost()}€")
6. Command Pattern
Problème : Encapsuler une requête comme un objet.
from abc import ABC, abstractmethod
from typing import List
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class Light:
def __init__(self, location: str):
self.location = location
self.is_on = False
def turn_on(self):
self.is_on = True
print(f"Lumière {self.location} allumée")
def turn_off(self):
self.is_on = False
print(f"Lumière {self.location} éteinte")
class LightOnCommand(Command):
def __init__(self, light: Light):
self.light = light
def execute(self):
self.light.turn_on()
def undo(self):
self.light.turn_off()
class LightOffCommand(Command):
def __init__(self, light: Light):
self.light = light
def execute(self):
self.light.turn_off()
def undo(self):
self.light.turn_on()
class RemoteControl:
def __init__(self):
self.commands: List[Command] = []
self.last_command: Command = None
def set_command(self, command: Command):
self.commands.append(command)
def press_button(self, slot: int):
if 0 <= slot < len(self.commands):
command = self.commands[slot]
command.execute()
self.last_command = command
def press_undo(self):
if self.last_command:
self.last_command.undo()
# Usage
living_room_light = Light("salon")
bedroom_light = Light("chambre")
remote = RemoteControl()
remote.set_command(LightOnCommand(living_room_light))
remote.set_command(LightOffCommand(living_room_light))
remote.set_command(LightOnCommand(bedroom_light))
remote.press_button(0) # Allume le salon
remote.press_button(2) # Allume la chambre
remote.press_undo() # Annule la dernière action
Patterns Avancés
7. Builder Pattern
class Pizza:
def __init__(self):
self.size = ""
self.dough = ""
self.toppings = []
def __str__(self):
return f"Pizza {self.size}, pâte {self.dough}, garnie de: {', '.join(self.toppings)}"
class PizzaBuilder:
def __init__(self):
self.pizza = Pizza()
def set_size(self, size: str):
self.pizza.size = size
return self
def set_dough(self, dough: str):
self.pizza.dough = dough
return self
def add_topping(self, topping: str):
self.pizza.toppings.append(topping)
return self
def build(self) -> Pizza:
return self.pizza
# Usage
pizza = (PizzaBuilder()
.set_size("grande")
.set_dough("fine")
.add_topping("mozzarella")
.add_topping("tomates")
.add_topping("basilic")
.build())
print(pizza)
Quand Utiliser Chaque Pattern
| Pattern | Utilisation | Avantages |
|---|---|---|
| Singleton | Configuration globale | Une seule instance |
| Factory | Création d'objets complexes | Flexibilité |
| Observer | Notifications | Couplage faible |
| Strategy | Algorithmes interchangeables | Extensibilité |
| Decorator | Fonctionnalités additionnelles | Composition |
| Command | Actions réversibles | Historique |
Anti-Patterns à Éviter
1. Singletons Partout
# Mauvais
class Logger(Singleton):
pass
class Config(Singleton):
pass
# Mieux : Injection de dépendance
class Service:
def __init__(self, logger: Logger, config: Config):
self.logger = logger
self.config = config
2. Factory Trop Complexe
# Mauvais
class ComplexFactory:
def create(self, type, size, color, material, style, ...):
# Trop de paramètres
pass
# Mieux : Builder Pattern
class ProductBuilder:
def with_size(self, size): return self
def with_color(self, color): return self
# ...
Conclusion
Les design patterns sont des outils puissants qui :
- Améliorent la communication entre développeurs
- Fournissent des solutions éprouvées
- Rendent le code plus maintenable
- Facilitent les tests et la refactorisation
L'important n'est pas de tous les connaître, mais de comprendre quand et comment les appliquer. Un bon développeur sait reconnaître les situations où un pattern apporte une réelle valeur ajoutée.
Top comments (0)