Compétence : Patrons de conception
Référence pratique des patrons de conception en Java avec des exemples modernes.
Quand utiliser
- L'utilisateur demande d'implémenter un patron spécifique
- Conception de composants extensibles/flexibles
- Refactoring de structures de code rigides
- Revue de code suggérant l'utilisation d'un patron
Référence rapide : Quand utiliser quoi
| Problème | Patron |
|---|---|
| Construction d'objet complexe | Builder |
| Créer des objets sans spécifier la classe | Factory |
| Plusieurs algorithmes, échange à l'exécution | Strategy |
| Ajouter un comportement sans modifier la classe | Decorator |
| Notifier plusieurs objets de changements | Observer |
| Garantir une instance unique | Singleton |
| Convertir des interfaces incompatibles | Adapter |
| Définir un squelette d'algorithme | Template Method |
Patrons de création
Builder
À utiliser quand : L'objet a plusieurs paramètres, dont certains optionnels.
// ❌ Anti-patron du constructeur télescopique
public class User {
public User(String name) { }
public User(String name, String email) { }
public User(String name, String email, int age) { }
public User(String name, String email, int age, String phone) { }
// ... explosion de constructeurs
}
// ✅ Patron Builder
public class User {
private final String name; // requis
private final String email; // requis
private final int age; // optionnel
private final String phone; // optionnel
private final String address; // optionnel
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public static Builder builder(String name, String email) {
return new Builder(name, email);
}
public static class Builder {
// Requis
private final String name;
private final String email;
// Optionnel avec valeurs par défaut
private int age = 0;
private String phone = "";
private String address = "";
private Builder(String name, String email) {
this.name = name;
this.email = email;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
// Utilisation
User user = User.builder("John", "[email protected]")
.age(30)
.phone("+1234567890")
.build();
Avec Lombok :
@Builder
@Getter
public class User {
private final String name;
private final String email;
@Builder.Default private int age = 0;
private String phone;
}
Factory Method
À utiliser quand : Besoin de créer des objets sans spécifier la classe exacte.
// ✅ Patron Factory Method
public interface Notification {
void send(String message);
}
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Email : " + message);
}
}
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("SMS : " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Push : " + message);
}
}
// Factory
public class NotificationFactory {
public static Notification create(String type) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification();
case "SMS" -> new SmsNotification();
case "PUSH" -> new PushNotification();
default -> throw new IllegalArgumentException("Type inconnu : " + type);
};
}
}
// Utilisation
Notification notification = NotificationFactory.create("EMAIL");
notification.send("Bonjour !");
Avec Spring (préféré) :
public interface NotificationSender {
void send(String message);
String getType();
}
@Component
public class EmailSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "EMAIL"; }
}
@Component
public class SmsSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "SMS"; }
}
@Component
public class NotificationFactory {
private final Map<String, NotificationSender> senders;
public NotificationFactory(List<NotificationSender> senderList) {
this.senders = senderList.stream()
.collect(Collectors.toMap(
NotificationSender::getType,
Function.identity()
));
}
public NotificationSender getSender(String type) {
return Optional.ofNullable(senders.get(type))
.orElseThrow(() -> new IllegalArgumentException("Inconnu : " + type));
}
}
Singleton
À utiliser quand : Exactement une instance nécessaire (à utiliser avec parcimonie !).
// ✅ Singleton moderne (basé sur énumération, thread-safe)
public enum DatabaseConnection {
INSTANCE;
private Connection connection;
DatabaseConnection() {
// Initialiser la connexion
}
public Connection getConnection() {
return connection;
}
}
// Utilisation
Connection conn = DatabaseConnection.INSTANCE.getConnection();
Avec Spring (préféré) :
@Component // Le scope par défaut est singleton
public class DatabaseConnection {
// Spring gère l'instance unique
}
Attention : Les singletons peuvent poser problème :
- Difficiles à tester (état global)
- Dépendances cachées
- Envisager l'injection de dépendances à la place
Patrons comportementaux
Strategy
À utiliser quand : Plusieurs algorithmes pour la même opération, besoin de les échanger à l'exécution.
// ✅ Patron Strategy
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Paiement de " + amount + " avec la carte " + cardNumber);
}
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Paiement de " + amount + " via PayPal : " + email);
}
}
public class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Paiement de " + amount + " vers le portefeuille : " + walletAddress);
}
}
// Contexte
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(BigDecimal total) {
paymentStrategy.pay(total);
}
}
// Utilisation
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("4111-1111-1111-1111"));
cart.checkout(new BigDecimal("99.99"));
// Changer la stratégie à l'exécution
cart.setPaymentStrategy(new PayPalPayment("[email protected]"));
cart.checkout(new BigDecimal("49.99"));
Avec Java 8+ (fonctionnel) :
// Stratégie en tant qu'interface fonctionnelle
@FunctionalInterface
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
// Utilisation avec des lambdas
PaymentStrategy creditCard = amount ->
System.out.println("Paiement par carte : " + amount);
PaymentStrategy paypal = amount ->
System.out.println("Paiement PayPal : " + amount);
cart.setPaymentStrategy(creditCard);
Observer
À utiliser quand : Des objets doivent être notifiés de changements dans un autre objet.
// ✅ Patron Observer (Java moderne)
public interface OrderObserver {
void onOrderPlaced(Order order);
}
public class OrderService {
private final List<OrderObserver> observers = new ArrayList<>();
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void removeObserver(OrderObserver observer) {
observers.remove(observer);
}
public void placeOrder(Order order) {
// Traiter la commande
saveOrder(order);
// Notifier tous les observateurs
observers.forEach(observer -> observer.onOrderPlaced(order));
}
}
// Observateurs
public class InventoryService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
// Réduire le stock
order.getItems().forEach(item ->
reduceStock(item.getProductId(), item.getQuantity())
);
}
}
public class EmailNotificationService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
sendConfirmationEmail(order.getCustomerEmail(), order);
}
}
public class AnalyticsService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
trackOrderEvent(order);
}
}
// Configuration
OrderService orderService = new OrderService();
orderService.addObserver(new InventoryService());
orderService.addObserver(new EmailNotificationService());
orderService.addObserver(new AnalyticsService());
Avec Spring Events (préféré) :
// Événement
public record OrderPlacedEvent(Order order) {}
// Éditeur
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public void placeOrder(Order order) {
saveOrder(order);
eventPublisher.publishEvent(new OrderPlacedEvent(order));
}
}
// Écouteurs (observateurs)
@Component
public class InventoryListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// Réduire le stock
}
}
@Component
public class EmailListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// Envoyer un e-mail
}
@EventListener
@Async // Traitement asynchrone
public void handleOrderPlacedAsync(OrderPlacedEvent event) {
// Envoyer un e-mail de manière asynchrone
}
}
Template Method
À utiliser quand : Définir un squelette d'algorithme, laisser les sous-classes remplir les étapes.
// ✅ Patron Template Method
public abstract class DataProcessor {
// Méthode modèle - définit l'algorithme
public final void process() {
readData();
processData();
writeData();
if (shouldNotify()) {
notifyCompletion();
}
}
// Étapes à implémenter par les sous-classes
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
// Point d'accroche - surcharge optionnelle
protected boolean shouldNotify() {
return true;
}
protected void notifyCompletion() {
System.out.println("Traitement terminé !");
}
}
public class CsvDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Lecture du fichier CSV...");
}
@Override
protected void processData() {
System.out.println("Traitement des données CSV...");
}
@Override
protected void writeData() {
System.out.println("Écriture dans la base de données...");
}
}
public class ApiDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Récupération depuis l'API...");
}
@Override
protected void processData() {
System.out.println("Transformation de la réponse API...");
}
@Override
protected void writeData() {
System.out.println("Écriture dans le cache...");
}
@Override
protected boolean shouldNotify() {
return false; // Surcharge du point d'accroche
}
}
// Utilisation
DataProcessor csvProcessor = new CsvDataProcessor();
csvProcessor.process();
DataProcessor apiProcessor = new ApiDataProcessor();
apiProcessor.process();
Patrons structurels
Decorator
À utiliser quand : Ajouter un comportement dynamiquement sans modifier les classes existantes.
// ✅ Patron Decorator
public interface Coffee {
String getDescription();
BigDecimal getCost();
}
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Café";
}
@Override
public BigDecimal getCost() {
return new BigDecimal("2.00");
}
}
// Décorateur de base
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public BigDecimal getCost() {
return coffee.getCost();
}
}
// Décorateurs concrets
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Lait";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.50"));
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Sucre";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.20"));
}
}
public class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Crème fouettée";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.70"));
}
}
// Utilisation
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " -> " + coffee.getCost());
// Ajouter du lait
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " -> " + coffee.getCost());
// Ajouter du sucre
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " -> " + coffee.getCost());
Anti-patrons à éviter
Utilisation excessive de if-else ou switch
// ❌ À éviter : commutateur manuel
public void processPayment(String type) {
switch(type) {
case "card": /* ... */ break;
case "paypal": /* ... */ break;
// ... s'allonge
}
}
// ✅ Mieux : Patron Strategy + Factory
Héritage profond
// ❌ À éviter : héritage profond
class A extends B { }
class B extends C { }
class C extends D { }
// ✅ Mieux : Composition (Decorator, Strategy)
Classe Dieu (God Class)
// ❌ À éviter : une classe qui fait tout
public class OrderManager {
public void processOrder() { /* ... */ }
public void sendEmail() { /* ... */ }
public void updateInventory() { /* ... */ }
public void generateInvoice() { /* ... */ }
public void trackAnalytics() { /* ... */ }
}
// ✅ Mieux : Séparer les responsabilités (Observer, SRP)
Guide de sélection rapide
- Dois-je créer des objets complexes ? → Builder
- Dois-je créer des objets sans en connaître le type ? → Factory
- Ai-je besoin d'une instance unique ? → Singleton (utiliser Spring si possible)
- Dois-je changer d'algorithme à l'exécution ? → Strategy
- Dois-je notifier plusieurs parties ? → Observer (utiliser Spring Events si possible)
- Dois-je ajouter des responsabilités à un objet ? → Decorator
- Dois-je définir une structure d'algorithme réutilisable ? → Template Method
- Ai-je une interface incompatible ? → Adapter
Résumé
- Préférer l'injection de dépendances (Spring) aux factories ou singletons personnalisés
- Utiliser les événements Spring (@EventListener) pour les scénarios Observer
- Exploiter les lambdas et interfaces fonctionnelles de Java 8+ pour les stratégies
- La composition sur l'héritage (Decorator, Strategy)
- Garder les choses simples — ne pas utiliser un patron si une simple interface suffit