viccom/design-patterns

Patrons de conception courants avec des exemples Java (Factory, Builder, Strategy, Observer, Decorator, etc.). À utiliser lorsque l'utilisateur demande « implémenter un patron », « utiliser une fabrique », « patron stratégie », ou lors de la conception de composants extensibles.

Qu'est-ce que design-patterns ?

Cette compétence fournit une référence complète et pratique des patrons de conception (design patterns) en Java, couvrant les catégories : création (Builder, Factory Method, Singleton), comportement (Strategy, Observer, Template Method) et structure (Decorator, Adapter). Chaque patron est illustré par des exemples modernes (Java 8+, enums, records) et des alternatives préférées avec Spring (injection de dépendances, ApplicationEventPublisher, @EventListener). Elle inclut des conseils sur quand utiliser chaque patron, des anti-patrons à éviter (constructeurs télescopiques, singletons globaux), et des versions fonctionnelles avec lambdas et @FunctionalInterface. Idéale pour les développeurs Java, elle facilite la conception extensible, le refactoring de code rigide, et les revues de code en suggérant des solutions adaptées aux problèmes de conception.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/viccom/viccom-agent-skills/tree/HEAD/skills/design-patterns

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

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èmePatron
Construction d'objet complexeBuilder
Créer des objets sans spécifier la classeFactory
Plusieurs algorithmes, échange à l'exécutionStrategy
Ajouter un comportement sans modifier la classeDecorator
Notifier plusieurs objets de changementsObserver
Garantir une instance uniqueSingleton
Convertir des interfaces incompatiblesAdapter
Définir un squelette d'algorithmeTemplate 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

  1. Dois-je créer des objets complexes ? → Builder
  2. Dois-je créer des objets sans en connaître le type ? → Factory
  3. Ai-je besoin d'une instance unique ? → Singleton (utiliser Spring si possible)
  4. Dois-je changer d'algorithme à l'exécution ? → Strategy
  5. Dois-je notifier plusieurs parties ? → Observer (utiliser Spring Events si possible)
  6. Dois-je ajouter des responsabilités à un objet ? → Decorator
  7. Dois-je définir une structure d'algorithme réutilisable ? → Template Method
  8. 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

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

Skills associés