Aller au contenu

Design Patterns en Java — Cours pratique

Spring Boot 3 · Java 17 · Thymeleaf · Windows


Sommaire


1. Introduction aux Design Patterns

1.1. Qu’est-ce qu’un Design Pattern ?

Imaginez travailler dans une banque depuis 20 ans, certains problèmes reviennent régulièrement : comment gérer l’accès aux dossiers clients, comment calculer les intérêts selon plusieurs méthodes différentes, comment notifier les clients lors d’un virement… Au fil des années, vous avez développé des façons de résoudre ces problèmes efficacement. Ces solutions éprouvées, ce sont des patterns — des modèles.

En développement logiciel, un Design Pattern (patron de conception) est exactement cela : une solution réutilisable à un problème récurrent dans la conception de logiciels. Ce ne sont pas des bibliothèques à importer, ni du code tout fait — ce sont des recettes, des plans de construction que vous adaptez à votre situation.

💡 Un Design Pattern, c’est comme une recette de cuisine : la recette de la tarte tatin ne vous donne pas une tarte, mais elle vous explique comment en faire une. Vous adaptez les ingrédients (les classes) à votre contexte.

1.2. Origine — Le Gang of Four

En 1994, quatre ingénieurs — Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides — ont publié le livre “Design Patterns: Elements of Reusable Object-Oriented Software”. Ce livre recense 23 patterns fondamentaux. Ses auteurs sont surnommés le Gang of Four (GoF).

Ces 23 patterns sont regroupés en trois familles :

Famille Rôle Exemples couverts dans ce cours
Créationnels Comment créer les objets Singleton, Factory, Builder
Structurels Comment assembler les objets DAO, Decorator, Proxy
Comportementaux Comment les objets collaborent Strategy, Observer, Template Method, Command

1.3. Pourquoi apprendre les Design Patterns ?

Dans le secteur bancaire, les enjeux sont particulièrement élevés :

Les Design Patterns répondent directement à ces enjeux. Ils permettent de :

1.4. Prérequis et environnement

Ce cours suppose que vous maîtrisez les bases de Java (classes, héritage, interfaces) et que vous avez déjà utilisé Spring Boot. Voici l’environnement de travail :

Environnement recommandé — Windows
├── JDK 17 (Oracle ou OpenJDK)
├── Spring Boot 3.x
├── Maven ou Gradle
├── IntelliJ IDEA Community ou VS Code + Extension Pack for Java
├── MySQL 8 (via XAMPP ou Docker Desktop)
└── Thymeleaf (intégré à Spring Boot)

Vérification de l’environnement :

# Dans le terminal Windows (CMD ou PowerShell)
java -version
# java version "17.x.x"

mvn -version
# Apache Maven 3.x.x

1.5. Structure d’un projet Spring Boot de référence

Pour les exemples de ce cours, notre projet s’appelle BanqueApp :

# Créer le projet avec Spring Initializr
# https://start.spring.io
# Paramètres :
# - Project : Maven
# - Language : Java
# - Spring Boot : 3.x
# - Java : 17
# - Dependencies : Spring Web, Thymeleaf, Spring Data JPA, MySQL Driver, Lombok
banque-app/
├── src/main/java/com/banque/
│   ├── BanqueAppApplication.java   ← Point d'entrée Spring Boot
│   ├── config/                     ← Configuration (Beans, Singletons)
│   ├── controller/                 ← Contrôleurs Spring MVC
│   ├── dao/                        ← Couche d'accès aux données (DAO)
│   ├── entity/                     ← Entités JPA (modèles)
│   ├── factory/                    ← Factories (fabriques)
│   ├── service/                    ← Services métier
│   └── strategy/                   ← Stratégies de calcul
├── src/main/resources/
│   ├── application.properties      ← Configuration Spring
│   └── templates/                  ← Vues Thymeleaf
└── pom.xml

2. Patron Singleton

2.1. Le problème que résout le Singleton

Dans votre banque, il ne peut y avoir qu’un seul gouverneur de la Banque de France. De même, dans votre application, certains objets ne doivent exister qu’en une seule instance dans toute la JVM :

Sans Singleton, chaque partie du code pourrait créer sa propre instance de ces objets — gaspillage mémoire, incohérences, risques de race conditions.

2.2. Définition

Singleton : patron créationnel qui garantit qu’une classe n’a qu’une seule instance et fournit un point d’accès global à cette instance.

2.3. Implémentation classique Java

// ❌ Implémentation naïve — problème en environnement multi-thread
public class GestionnaireAudit {

    private static GestionnaireAudit instance; // L'unique instance

    // ✅ Constructeur privé : personne ne peut faire new GestionnaireAudit()
    private GestionnaireAudit() {
        System.out.println("Gestionnaire d'audit initialisé.");
    }

    // ❌ Non thread-safe : deux threads peuvent créer deux instances
    public static GestionnaireAudit getInstance() {
        if (instance == null) {
            instance = new GestionnaireAudit();
        }
        return instance;
    }
}

Problème dans un contexte bancaire avec plusieurs threads (ce qui est toujours le cas) : deux requêtes simultanées peuvent créer deux instances différentes !

2.4. Implémentation thread-safe avec double vérification

// ✅ Singleton thread-safe — La version recommandée en Java moderne
public class GestionnaireAudit {

    // volatile garantit la visibilité entre threads
    private static volatile GestionnaireAudit instance;

    private final List<String> journalAudit = new ArrayList<>();

    // ✅ Constructeur privé
    private GestionnaireAudit() {}

    // ✅ Double-checked locking
    public static GestionnaireAudit getInstance() {
        if (instance == null) {                    // Premier test (sans synchronisation)
            synchronized (GestionnaireAudit.class) {
                if (instance == null) {            // Deuxième test (avec verrou)
                    instance = new GestionnaireAudit();
                }
            }
        }
        return instance;
    }

    public synchronized void enregistrer(String action, String utilisateur) {
        String entree = String.format("[%s] %s — %s",
            LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
            utilisateur,
            action
        );
        journalAudit.add(entree);
        System.out.println("AUDIT : " + entree);
    }

    public List<String> obtenirJournal() {
        return Collections.unmodifiableList(journalAudit);
    }
}

2.5. Implémentation par enum — La plus robuste

// ✅ La meilleure implémentation Singleton en Java : l'enum
// Avantages : thread-safe de facto, sérialisation gérée, résistant à la réflexion
public enum GestionnaireConfiguration {

    INSTANCE; // L'unique instance

    private final Properties config = new Properties();

    GestionnaireConfiguration() {
        // Chargement de la configuration au démarrage
        try (InputStream is = getClass().getResourceAsStream("/application.properties")) {
            if (is != null) config.load(is);
        } catch (IOException e) {
            System.err.println("Erreur chargement config : " + e.getMessage());
        }
    }

    public String obtenirPropriete(String cle) {
        return config.getProperty(cle, "");
    }

    public String obtenirPropriete(String cle, String valeurDefaut) {
        return config.getProperty(cle, valeurDefaut);
    }
}

// ✅ Utilisation
public class ExempleUtilisation {
    public void afficherConfig() {
        String tauxTva = GestionnaireConfiguration.INSTANCE.obtenirPropriete("banque.taux.tva", "20");
        System.out.println("Taux TVA : " + tauxTva + "%");
    }
}

2.6. Singleton dans Spring Boot — Le cas le plus courant

Bonne nouvelle : Spring Boot implémente le Singleton pour vous ! Par défaut, chaque Bean Spring est un Singleton dans le conteneur IoC (Inversion of Control).

// ✅ Dans Spring Boot, tout @Service, @Repository, @Component est Singleton par défaut
@Service
public class AuditService {

    private static final Logger log = LoggerFactory.getLogger(AuditService.class);

    // Spring crée UNE SEULE instance de ce service pour toute l'application
    public void logAction(String utilisateur, String action) {
        log.info("[AUDIT] {} : {}", utilisateur, action);
        // En production : persister dans la base, envoyer à un SIEM...
    }
}

// Vérification que Spring respecte bien le pattern Singleton :
@RestController
public class TestController {

    @Autowired
    private AuditService auditService1;

    @Autowired
    private AuditService auditService2;

    @GetMapping("/test-singleton")
    public String testerSingleton() {
        // auditService1 == auditService2 : TOUJOURS true avec Spring
        return "Même instance ? " + (auditService1 == auditService2); // → true
    }
}

💡 En Spring Boot, si vous voulez explicitement une nouvelle instance à chaque injection, utilisez l’annotation @Scope("prototype"). Pour une instance par requête HTTP : @Scope("request"). Mais la grande majorité du temps, le Singleton par défaut est ce qu’il vous faut.

2.7. Cas concret bancaire — Configuration des taux

// ✅ Singleton Spring : gestionnaire de taux bancaires
@Component
public class GestionnaireTaux {

    // Chargé une seule fois au démarrage de l'application
    private final Map<String, BigDecimal> taux = new HashMap<>();

    @PostConstruct // Méthode appelée après l'injection des dépendances
    public void initialiser() {
        taux.put("LIVRET_A",        new BigDecimal("0.030"));  // 3,0%
        taux.put("ASSURANCE_VIE",   new BigDecimal("0.025"));  // 2,5%
        taux.put("CREDIT_IMMO",     new BigDecimal("0.040"));  // 4,0%
        taux.put("CREDIT_CONSO",    new BigDecimal("0.065"));  // 6,5%
        System.out.println("Taux bancaires chargés.");
    }

    public BigDecimal obtenirTaux(String typeProduit) {
        return taux.getOrDefault(typeProduit, BigDecimal.ZERO);
    }

    public Map<String, BigDecimal> obtenirTousTaux() {
        return Collections.unmodifiableMap(taux);
    }
}

2.8. Diagramme du patron Singleton

┌──────────────────────────────────────────┐
│           GestionnaireAudit              │
├──────────────────────────────────────────┤
│ - instance : GestionnaireAudit  [static] │
├──────────────────────────────────────────┤
│ - GestionnaireAudit()  [private]         │
│ + getInstance() : GestionnaireAudit      │
│ + enregistrer(action, utilisateur)       │
└──────────────────────────────────────────┘

  Client A ──▶ getInstance() ──▶ ┐
                                  ├── Même objet en mémoire
  Client B ──▶ getInstance() ──▶ ┘

3. Patron DAO — Data Access Object

3.1. Le problème que résout le DAO

Dans une application bancaire, vous avez besoin d’accéder aux données des comptes, des clients, des transactions. Sans organisation, vos méthodes de calcul métier se mélangent avec les requêtes SQL :

// ❌ SANS DAO — Code métier mélangé avec l'accès aux données
public class VirementService {
    public void effectuerVirement(long idCompteSource, long idCompteDest, BigDecimal montant) {
        // Logique métier mélangée avec SQL : illisible, non testable, non réutilisable
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/banque", "root", "");
        PreparedStatement ps = conn.prepareStatement("SELECT solde FROM compte WHERE id = ?");
        ps.setLong(1, idCompteSource);
        ResultSet rs = ps.executeQuery();
        // ... 50 lignes de code SQL enchevêtrées avec la logique métier
    }
}

Ce code est un cauchemar : si vous changez de base de données (Oracle → PostgreSQL), tout est à réécrire. Si vous voulez tester la logique de virement sans BDD, c’est impossible.

3.2. Définition

DAO (Data Access Object) : patron structurel qui sépare la logique d’accès aux données de la logique métier. Le DAO fournit une interface abstraite vers la base de données.

Le principe est simple : créer une interface qui définit les opérations possibles sur une entité (trouver, créer, modifier, supprimer), puis implémenter cette interface pour chaque technologie de persistance.

3.3. Implémentation — Entité Compte bancaire

// ✅ Entité JPA — modèle de données
@Entity
@Table(name = "compte")
public class Compte {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String numero;          // Ex : "FR7630001007941234567890185"

    @Column(nullable = false)
    private String typeCompte;      // "COURANT", "EPARGNE", "LIVRET_A"

    @Column(precision = 15, scale = 2)
    private BigDecimal solde;

    @Column(nullable = false)
    private Long idClient;

    @Column(nullable = false)
    private boolean actif;

    // ✅ Constructeur, getters, setters, toString...
    public Compte() {}

    public Compte(String numero, String typeCompte, BigDecimal solde, Long idClient) {
        this.numero     = numero;
        this.typeCompte = typeCompte;
        this.solde      = solde;
        this.idClient   = idClient;
        this.actif      = true;
    }

    // Getters et setters (ou @Data de Lombok)
    public Long getId() { return id; }
    public String getNumero() { return numero; }
    public BigDecimal getSolde() { return solde; }
    public void setSolde(BigDecimal solde) { this.solde = solde; }
    public boolean isActif() { return actif; }
    public String getTypeCompte() { return typeCompte; }
    public Long getIdClient() { return idClient; }
    public void setActif(boolean actif) { this.actif = actif; }
}

3.4. L’interface DAO

// ✅ Interface DAO — contrat abstrait, indépendant de la technologie
public interface CompteDAO {

    // Opérations CRUD de base
    Optional<Compte> trouverParId(Long id);
    Optional<Compte> trouverParNumero(String numero);
    List<Compte> trouverTous();
    List<Compte> trouverParClient(Long idClient);
    List<Compte> trouverParType(String typeCompte);

    Compte sauvegarder(Compte compte);
    Compte mettreAJour(Compte compte);
    void supprimer(Long id);

    // Opérations métier spécifiques
    List<Compte> trouverComptesSoldeInsuffisant(BigDecimal seuilMontant);
    BigDecimal calculerSoldeTotal(Long idClient);
    boolean existeParNumero(String numero);
}

3.5. Implémentation JDBC (sans Spring)

// ✅ Implémentation concrète avec JDBC — technologie 1
public class CompteDAOJdbc implements CompteDAO {

    private final DataSource dataSource;

    public CompteDAOJdbc(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Optional<Compte> trouverParId(Long id) {
        String sql = "SELECT * FROM compte WHERE id = ?";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql)) {

            ps.setLong(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return Optional.of(mapperResultSet(rs));
                }
            }
        } catch (SQLException e) {
            throw new RuntimeException("Erreur accès BDD", e);
        }
        return Optional.empty();
    }

    @Override
    public Compte sauvegarder(Compte compte) {
        String sql = "INSERT INTO compte (numero, type_compte, solde, id_client, actif) VALUES (?, ?, ?, ?, ?)";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {

            ps.setString(1,     compte.getNumero());
            ps.setString(2,     compte.getTypeCompte());
            ps.setBigDecimal(3, compte.getSolde());
            ps.setLong(4,       compte.getIdClient());
            ps.setBoolean(5,    compte.isActif());
            ps.executeUpdate();

            try (ResultSet keys = ps.getGeneratedKeys()) {
                if (keys.next()) {
                    // Retourner le compte avec son ID généré
                }
            }
        } catch (SQLException e) {
            throw new RuntimeException("Erreur sauvegarde compte", e);
        }
        return compte;
    }

    // ✅ Méthode utilitaire privée : mapping ResultSet → Compte
    private Compte mapperResultSet(ResultSet rs) throws SQLException {
        Compte c = new Compte();
        // mapping des colonnes... (voir implémentation complète)
        return c;
    }

    // ... autres méthodes de l'interface
    @Override public Optional<Compte> trouverParNumero(String numero) { return Optional.empty(); }
    @Override public List<Compte> trouverTous() { return List.of(); }
    @Override public List<Compte> trouverParClient(Long idClient) { return List.of(); }
    @Override public List<Compte> trouverParType(String typeCompte) { return List.of(); }
    @Override public Compte mettreAJour(Compte compte) { return compte; }
    @Override public void supprimer(Long id) {}
    @Override public List<Compte> trouverComptesSoldeInsuffisant(BigDecimal s) { return List.of(); }
    @Override public BigDecimal calculerSoldeTotal(Long idClient) { return BigDecimal.ZERO; }
    @Override public boolean existeParNumero(String numero) { return false; }
}

3.6. Implémentation Spring Data JPA (la plus utilisée)

// ✅ Implémentation avec Spring Data JPA — technologie 2
// Spring génère l'implémentation automatiquement !
@Repository
public interface CompteDAOJpa extends JpaRepository<Compte, Long>, CompteDAO {

    // Spring Data génère le SQL depuis le nom de la méthode !
    Optional<Compte> findByNumero(String numero);
    List<Compte> findByIdClient(Long idClient);
    List<Compte> findByTypeCompte(String typeCompte);
    boolean existsByNumero(String numero);

    // Requête JPQL personnalisée
    @Query("SELECT c FROM Compte c WHERE c.solde < :seuil AND c.actif = true")
    List<Compte> findComptesSoldeInsuffisant(@Param("seuil") BigDecimal seuil);

    @Query("SELECT SUM(c.solde) FROM Compte c WHERE c.idClient = :idClient AND c.actif = true")
    BigDecimal calculerSoldeTotal(@Param("idClient") Long idClient);
}

3.7. Couche Service utilisant le DAO

// ✅ La couche Service utilise le DAO via son interface — jamais l'implémentation concrète
@Service
@Transactional
public class CompteService {

    private final CompteDAO compteDAO; // ← Interface, pas l'implémentation !
    private final AuditService auditService;

    // Spring injecte automatiquement l'implémentation disponible (JPA, JDBC, Mock...)
    public CompteService(CompteDAO compteDAO, AuditService auditService) {
        this.compteDAO    = compteDAO;
        this.auditService = auditService;
    }

    public Compte ouvrirCompte(String typeCompte, Long idClient, BigDecimal soldeInitial) {
        String numero = genererNumeroCompte();
        Compte compte = new Compte(numero, typeCompte, soldeInitial, idClient);
        Compte sauvegarde = compteDAO.sauvegarder(compte);
        auditService.logAction("SYSTEME", "Ouverture compte " + numero + " type " + typeCompte);
        return sauvegarde;
    }

    public void effectuerVirement(String numeroSource, String numeroDest, BigDecimal montant) {
        Compte source = compteDAO.trouverParNumero(numeroSource)
            .orElseThrow(() -> new RuntimeException("Compte source introuvable : " + numeroSource));

        Compte destination = compteDAO.trouverParNumero(numeroDest)
            .orElseThrow(() -> new RuntimeException("Compte destination introuvable : " + numeroDest));

        if (source.getSolde().compareTo(montant) < 0) {
            throw new RuntimeException("Solde insuffisant sur " + numeroSource);
        }

        source.setSolde(source.getSolde().subtract(montant));
        destination.setSolde(destination.getSolde().add(montant));

        compteDAO.mettreAJour(source);
        compteDAO.mettreAJour(destination);

        auditService.logAction("VIREMENT",
            String.format("Virement %.2f€ de %s vers %s", montant, numeroSource, numeroDest));
    }

    private String genererNumeroCompte() {
        return "FR76" + System.currentTimeMillis();
    }
}

3.8. Avantage clé : la testabilité

// ✅ Test unitaire SANS base de données — possible grâce au DAO
class CompteServiceTest {

    @Test
    void testVirementInsuffisant() {
        // On crée un FAUX DAO en mémoire (Mock)
        CompteDAO fakeDao = new CompteDAO() {
            private final Map<String, Compte> comptes = new HashMap<>();

            @Override
            public Optional<Compte> trouverParNumero(String num) {
                return Optional.ofNullable(comptes.get(num));
            }

            @Override
            public Compte mettreAJour(Compte c) { comptes.put(c.getNumero(), c); return c; }

            // ... autres méthodes avec implémentation vide ou minimale
            @Override public Optional<Compte> trouverParId(Long id) { return Optional.empty(); }
            @Override public List<Compte> trouverTous() { return List.of(); }
            @Override public Compte sauvegarder(Compte c) { comptes.put(c.getNumero(), c); return c; }
            @Override public List<Compte> trouverParClient(Long id) { return List.of(); }
            @Override public List<Compte> trouverParType(String t) { return List.of(); }
            @Override public void supprimer(Long id) {}
            @Override public List<Compte> trouverComptesSoldeInsuffisant(BigDecimal s) { return List.of(); }
            @Override public BigDecimal calculerSoldeTotal(Long id) { return BigDecimal.ZERO; }
            @Override public boolean existeParNumero(String n) { return false; }
        };

        // Créer des comptes de test
        Compte source = new Compte("FR7600001", "COURANT", new BigDecimal("100.00"), 1L);
        fakeDao.sauvegarder(source);

        CompteService service = new CompteService(fakeDao, new AuditService());

        // Tenter un virement de 200€ avec seulement 100€ disponibles
        assertThrows(RuntimeException.class,
            () -> service.effectuerVirement("FR7600001", "FR7600002", new BigDecimal("200.00")));
    }
}

3.9. Diagramme du patron DAO

┌──────────────┐    utilise     ┌──────────────┐
│ CompteService│ ─────────────▶ │  CompteDAO   │  ← Interface
└──────────────┘                └──────┬───────┘
                                       │ implémente
                      ┌────────────────┼──────────────────┐
                      ▼                ▼                   ▼
              ┌───────────────┐ ┌─────────────┐ ┌──────────────┐
              │ CompteDAOJdbc │ │CompteDAOJpa │ │CompteDAOMock │
              │  (production) │ │(production) │ │   (test)     │
              └───────────────┘ └─────────────┘ └──────────────┘

4. Patron Factory — Fabrique

4.1. Le problème que résout la Factory

Dans une banque, il existe plusieurs types de comptes : courant, épargne, livret A, PEL, PEA… Chaque type a ses propres règles : plafond de dépôt, taux, conditions d’ouverture. Sans Factory, la création de ces objets serait dispersée partout dans le code :

// ❌ SANS FACTORY — création dispersée et rigide
if (type.equals("COURANT")) {
    compte = new CompteCourant(client, 0);
} else if (type.equals("EPARGNE")) {
    compte = new CompteEpargne(client, 0.025);
} else if (type.equals("LIVRET_A")) {
    compte = new CompteLivretA(client, 0.03, 22950); // plafond réglementaire
}
// Ce bloc se répète dans 10 endroits du code → maintenance cauchemardesque

Quand un nouveau type de compte arrive (compte professionnel, compte jeune…), il faut modifier tous ces blocs if-else dans tout le code.

4.2. Définition

Factory (Fabrique) : patron créationnel qui délègue la création d’objets à une classe spécialisée. Le code client demande un objet sans savoir exactement quelle classe concrète sera instanciée.

Il existe plusieurs variantes : Simple Factory, Factory Method, Abstract Factory. Nous allons les explorer progressivement.

4.3. Simple Factory — Première étape

// ✅ Classe abstraite commune à tous les types de comptes
public abstract class CompteBancaire {

    protected String numero;
    protected BigDecimal solde;
    protected Long idClient;
    protected String typeCompte;

    public CompteBancaire(Long idClient) {
        this.idClient  = idClient;
        this.solde     = BigDecimal.ZERO;
        this.numero    = genererNumero();
    }

    // Méthodes communes à tous les comptes
    public abstract BigDecimal calculerInterets();
    public abstract BigDecimal getMontantMaxVirement();
    public abstract String getDescription();

    public void deposer(BigDecimal montant) {
        if (montant.compareTo(BigDecimal.ZERO) <= 0)
            throw new IllegalArgumentException("Le montant doit être positif.");
        this.solde = this.solde.add(montant);
    }

    public void retirer(BigDecimal montant) {
        if (montant.compareTo(this.solde) > 0)
            throw new RuntimeException("Solde insuffisant.");
        this.solde = this.solde.subtract(montant);
    }

    private String genererNumero() {
        return "FR76" + typeCompte + System.nanoTime() % 1000000;
    }

    // Getters
    public String getNumero() { return numero; }
    public BigDecimal getSolde() { return solde; }
    public Long getIdClient() { return idClient; }
    public String getTypeCompte() { return typeCompte; }
}
// ✅ Compte courant
public class CompteCourant extends CompteBancaire {

    private static final BigDecimal PLAFOND_VIREMENT = new BigDecimal("50000.00");

    public CompteCourant(Long idClient) {
        super(idClient);
        this.typeCompte = "COURANT";
    }

    @Override
    public BigDecimal calculerInterets() {
        return BigDecimal.ZERO; // Pas d'intérêts sur le courant
    }

    @Override
    public BigDecimal getMontantMaxVirement() {
        return PLAFOND_VIREMENT;
    }

    @Override
    public String getDescription() {
        return "Compte Courant — Virements jusqu'à " + PLAFOND_VIREMENT + "€";
    }
}

// ✅ Livret A
public class CompteLivretA extends CompteBancaire {

    private static final BigDecimal TAUX_ANNUEL    = new BigDecimal("0.030");  // 3,0%
    private static final BigDecimal PLAFOND_DEPOT  = new BigDecimal("22950.00"); // Plafond légal

    public CompteLivretA(Long idClient) {
        super(idClient);
        this.typeCompte = "LIVRET_A";
    }

    @Override
    public BigDecimal calculerInterets() {
        // Intérêts mensuels = solde * taux / 12
        return solde.multiply(TAUX_ANNUEL).divide(new BigDecimal("12"), 2, RoundingMode.HALF_UP);
    }

    @Override
    public BigDecimal getMontantMaxVirement() {
        return solde; // On ne peut virer que ce qu'on a
    }

    @Override
    public String getDescription() {
        return String.format("Livret A — Taux : %.1f%% — Plafond dépôt : %.0f€",
            TAUX_ANNUEL.multiply(BigDecimal.valueOf(100)), PLAFOND_DEPOT);
    }
}

// ✅ Compte Épargne
public class CompteEpargne extends CompteBancaire {

    private final BigDecimal tauxAnnuel;

    public CompteEpargne(Long idClient, BigDecimal tauxAnnuel) {
        super(idClient);
        this.typeCompte = "EPARGNE";
        this.tauxAnnuel = tauxAnnuel;
    }

    @Override
    public BigDecimal calculerInterets() {
        return solde.multiply(tauxAnnuel).divide(new BigDecimal("12"), 2, RoundingMode.HALF_UP);
    }

    @Override
    public BigDecimal getMontantMaxVirement() {
        return solde;
    }

    @Override
    public String getDescription() {
        return String.format("Compte Épargne — Taux : %.2f%%", tauxAnnuel.multiply(BigDecimal.valueOf(100)));
    }
}
// ✅ Simple Factory — centralise toute la création
public class CompteFactory {

    // Méthode de création centrale
    public static CompteBancaire creer(String typeCompte, Long idClient) {
        return switch (typeCompte.toUpperCase()) {
            case "COURANT"  -> new CompteCourant(idClient);
            case "LIVRET_A" -> new CompteLivretA(idClient);
            case "EPARGNE"  -> new CompteEpargne(idClient, new BigDecimal("0.025"));
            case "PEL"      -> new ComptePEL(idClient);
            case "PEA"      -> new ComptePEA(idClient);
            default -> throw new IllegalArgumentException("Type de compte inconnu : " + typeCompte);
        };
    }
}
// ✅ Utilisation dans le service — le code client ne connaît pas les classes concrètes
@Service
public class CompteService {

    public CompteBancaire ouvrirCompte(String typeCompte, Long idClient) {
        // Une seule ligne — peu importe le type, la factory s'en charge
        CompteBancaire compte = CompteFactory.creer(typeCompte, idClient);
        System.out.println("Compte créé : " + compte.getDescription());
        return compte;
    }
}

4.4. Factory Method — La variante extensible

// ✅ Factory Method : chaque sous-classe définit sa propre logique de création
public abstract class GestionnaireCompteFactory {

    // Factory Method — abstraite : les sous-classes définissent QUOI créer
    protected abstract CompteBancaire creerCompte(Long idClient);

    // Méthode template — commune à tous : définit COMMENT initialiser
    public final CompteBancaire ouvrirEtInitialiser(Long idClient, BigDecimal depotInitial) {
        CompteBancaire compte = creerCompte(idClient); // Délégué à la sous-classe

        if (depotInitial != null && depotInitial.compareTo(BigDecimal.ZERO) > 0) {
            compte.deposer(depotInitial);
        }

        System.out.println("Compte ouvert : " + compte.getNumero() +
                           " — Solde initial : " + compte.getSolde() + "€");
        return compte;
    }
}

// ✅ Fabrique concrète pour les particuliers
public class GestionnaireParticulier extends GestionnaireCompteFactory {

    @Override
    protected CompteBancaire creerCompte(Long idClient) {
        return new CompteCourant(idClient); // Les particuliers ont un compte courant
    }
}

// ✅ Fabrique concrète pour les jeunes (18-25 ans)
public class GestionnaireJeune extends GestionnaireCompteFactory {

    @Override
    protected CompteBancaire creerCompte(Long idClient) {
        // Compte courant avec avantages spéciaux pour les jeunes
        CompteCourant compte = new CompteCourant(idClient);
        // Appliquer des avantages tarifaires spéciaux...
        return compte;
    }
}

// ✅ Fabrique concrète pour les professionnels
public class GestionnaireProfessionnel extends GestionnaireCompteFactory {

    @Override
    protected CompteBancaire creerCompte(Long idClient) {
        return new CompteProfessionnel(idClient); // Type de compte pro avec fonctionnalités étendues
    }
}

4.5. Factory Spring Boot — Intégration pratique

// ✅ Factory Spring : utiliser le contexte Spring pour créer les objets
@Component
public class CompteSpringFactory {

    // On injecte toutes les implémentations de CompteBancaire disponibles dans Spring
    // Spring les trouve automatiquement grâce au @Component sur chaque type
    private final Map<String, CompteCreateur> creatorsParType;

    public CompteSpringFactory(List<CompteCreateur> createursList) {
        this.creatorsParType = createursList.stream()
            .collect(Collectors.toMap(
                CompteCreateur::getTypeSupporte,
                Function.identity()
            ));
    }

    public CompteBancaire creer(String type, Long idClient) {
        CompteCreateur createur = creatorsParType.get(type.toUpperCase());
        if (createur == null) {
            throw new IllegalArgumentException("Type de compte non supporté : " + type);
        }
        return createur.creer(idClient);
    }
}

// Interface pour chaque créateur
public interface CompteCreateur {
    String getTypeSupporte();
    CompteBancaire creer(Long idClient);
}

// ✅ Chaque type de compte a son propre créateur Spring
@Component
public class CompteCreateurLivretA implements CompteCreateur {
    @Override public String getTypeSupporte() { return "LIVRET_A"; }
    @Override public CompteBancaire creer(Long idClient) { return new CompteLivretA(idClient); }
}

@Component
public class CompteCreateurCourant implements CompteCreateur {
    @Override public String getTypeSupporte() { return "COURANT"; }
    @Override public CompteBancaire creer(Long idClient) { return new CompteCourant(idClient); }
}

4.6. Diagramme du patron Factory

         ┌────────────────┐
         │  CompteFactory │
         └───────┬────────┘
    creer()      │
    ─────────────┤
                 │
      ┌──────────┼──────────┐
      ▼          ▼          ▼
┌───────────┐ ┌──────────┐ ┌──────────┐
│  Compte   │ │CompteLiv.│ │ Compte   │
│  Courant  │ │    A     │ │ Epargne  │
└───────────┘ └──────────┘ └──────────┘
      ▲          ▲          ▲
      └──────────┴──────────┘
              implémentent
         CompteBancaire (abstract)

5. Patron Strategy — Stratégie

5.1. Définition et analogie bancaire

Imaginez le calcul des frais bancaires : pour un client standard, on applique une règle ; pour un client premium, une autre ; pour un client professionnel, encore une autre. Si ce calcul est codé avec des if-else, ajouter un nouveau type de client implique de modifier le code existant — risqué en production bancaire.

Strategy : patron comportemental qui définit une famille d’algorithmes interchangeables. Le client utilise l’algorithme via une interface, sans savoir lequel est actif.

5.2. Implémentation — Calcul des frais bancaires

// ✅ Interface Strategy — contrat de tous les algorithmes de calcul
public interface StratégieCalculFrais {
    BigDecimal calculerFraisVirement(BigDecimal montant);
    BigDecimal calculerFraisRetrait(BigDecimal montant);
    BigDecimal calculerFraisAnnuels();
    String getNomTarification();
}

// ✅ Stratégie 1 : Client Standard
@Component("fraisStandard")
public class StrategieClientStandard implements StratégieCalculFrais {

    @Override
    public BigDecimal calculerFraisVirement(BigDecimal montant) {
        // 0,5% du montant, minimum 0,50€, maximum 15€
        BigDecimal frais = montant.multiply(new BigDecimal("0.005"));
        return frais.max(new BigDecimal("0.50")).min(new BigDecimal("15.00"));
    }

    @Override
    public BigDecimal calculerFraisRetrait(BigDecimal montant) {
        return new BigDecimal("0.50"); // Forfait fixe
    }

    @Override
    public BigDecimal calculerFraisAnnuels() {
        return new BigDecimal("24.00"); // 24€ / an
    }

    @Override
    public String getNomTarification() { return "Standard"; }
}

// ✅ Stratégie 2 : Client Premium
@Component("fraisPremium")
public class StrategieClientPremium implements StratégieCalculFrais {

    @Override
    public BigDecimal calculerFraisVirement(BigDecimal montant) {
        return BigDecimal.ZERO; // Virements gratuits !
    }

    @Override
    public BigDecimal calculerFraisRetrait(BigDecimal montant) {
        return BigDecimal.ZERO; // Retraits gratuits
    }

    @Override
    public BigDecimal calculerFraisAnnuels() {
        return new BigDecimal("120.00"); // Forfait premium 120€/an
    }

    @Override
    public String getNomTarification() { return "Premium"; }
}

// ✅ Stratégie 3 : Client Professionnel
@Component("fraisProfessionnel")
public class StrategieClientProfessionnel implements StratégieCalculFrais {

    @Override
    public BigDecimal calculerFraisVirement(BigDecimal montant) {
        // 0,1% du montant, sans plafond
        return montant.multiply(new BigDecimal("0.001"));
    }

    @Override
    public BigDecimal calculerFraisRetrait(BigDecimal montant) {
        return BigDecimal.ZERO;
    }

    @Override
    public BigDecimal calculerFraisAnnuels() {
        return new BigDecimal("360.00"); // Forfait pro 360€/an
    }

    @Override
    public String getNomTarification() { return "Professionnel"; }
}
// ✅ Contexte : le service qui utilise la stratégie
@Service
public class CalculFraisService {

    // Map de toutes les stratégies injectées par Spring
    private final Map<String, StratégieCalculFrais> strategies;

    public CalculFraisService(Map<String, StratégieCalculFrais> strategies) {
        this.strategies = strategies;
    }

    public BigDecimal calculerFraisVirement(String typeClient, BigDecimal montant) {
        StratégieCalculFrais strategie = obtenirStrategie(typeClient);
        BigDecimal frais = strategie.calculerFraisVirement(montant);
        System.out.printf("Frais virement [%s] pour %.2f€ : %.2f€%n",
            strategie.getNomTarification(), montant, frais);
        return frais;
    }

    private StratégieCalculFrais obtenirStrategie(String typeClient) {
        String cle = switch (typeClient.toUpperCase()) {
            case "PREMIUM"        -> "fraisPremium";
            case "PROFESSIONNEL"  -> "fraisProfessionnel";
            default               -> "fraisStandard";
        };
        return strategies.getOrDefault(cle, strategies.get("fraisStandard"));
    }
}

6. Patron Observer — Observateur

6.1. Définition et analogie bancaire

Une banque doit notifier ses clients lors d’événements : dépassement de découvert, virement reçu, tentative de connexion suspecte… Ces notifications peuvent prendre plusieurs formes : email, SMS, notification push, alerte interne de sécurité.

Observer : patron comportemental qui définit une relation 1-N entre objets. Quand l’objet observé change d’état, tous ses observateurs sont notifiés automatiquement.

6.2. Implémentation avec les événements Spring

// ✅ Événement Spring — l'objet qui déclenche la notification
public class EvenementTransaction extends ApplicationEvent {

    public enum TypeEvenement {
        VIREMENT_EMIS, VIREMENT_RECU, DEPOT, RETRAIT,
        DECOUVERTE_SUSPECTE, SOLDE_FAIBLE
    }

    private final String  numeroCompte;
    private final BigDecimal montant;
    private final TypeEvenement typeEvenement;
    private final Long idClient;

    public EvenementTransaction(Object source, String numeroCompte,
                                 BigDecimal montant, TypeEvenement type, Long idClient) {
        super(source);
        this.numeroCompte  = numeroCompte;
        this.montant       = montant;
        this.typeEvenement = type;
        this.idClient      = idClient;
    }

    // Getters
    public String getNumeroCompte() { return numeroCompte; }
    public BigDecimal getMontant() { return montant; }
    public TypeEvenement getTypeEvenement() { return typeEvenement; }
    public Long getIdClient() { return idClient; }
}
// ✅ Observateur 1 : notification par email
@Component
public class ObservateurEmail {

    @EventListener
    public void surEvenementTransaction(EvenementTransaction evenement) {
        String message = switch (evenement.getTypeEvenement()) {
            case VIREMENT_RECU ->
                String.format("Vous avez reçu un virement de %.2f€ sur votre compte %s.",
                    evenement.getMontant(), evenement.getNumeroCompte());
            case SOLDE_FAIBLE ->
                String.format("Alerte : votre solde est bas (%.2f€ restants).",
                    evenement.getMontant());
            case DECOUVERTE_SUSPECTE ->
                "Alerte sécurité : activité suspecte détectée sur votre compte.";
            default -> null;
        };

        if (message != null) {
            System.out.println("[EMAIL] → Client " + evenement.getIdClient() + " : " + message);
            // En production : emailService.envoyer(clientEmail, "Alerte compte", message);
        }
    }
}

// ✅ Observateur 2 : audit interne (toutes les transactions)
@Component
public class ObservateurAudit {

    @EventListener
    @Async // Asynchrone : ne bloque pas la transaction
    public void surEvenementTransaction(EvenementTransaction evenement) {
        System.out.printf("[AUDIT] %s — Compte %s — Montant : %.2f€ — Client : %d%n",
            evenement.getTypeEvenement(),
            evenement.getNumeroCompte(),
            evenement.getMontant(),
            evenement.getIdClient()
        );
        // En production : persister dans la table audit
    }
}

// ✅ Observateur 3 : détection de fraude
@Component
public class ObservateurAntiFraude {

    private static final BigDecimal SEUIL_ALERTE = new BigDecimal("10000.00");

    @EventListener
    public void surEvenementTransaction(EvenementTransaction evenement) {
        if (evenement.getMontant().compareTo(SEUIL_ALERTE) > 0
                && evenement.getTypeEvenement() == EvenementTransaction.TypeEvenement.VIREMENT_EMIS) {
            System.out.printf("[ANTI-FRAUDE] ⚠️  Virement important détecté : %.2f€ sur %s%n",
                evenement.getMontant(), evenement.getNumeroCompte());
            // En production : alerter le service de conformité
        }
    }
}
// ✅ Publication de l'événement depuis le service
@Service
public class TransactionService {

    private final ApplicationEventPublisher eventPublisher;
    private final CompteDAO compteDAO;

    public TransactionService(ApplicationEventPublisher eventPublisher, CompteDAO compteDAO) {
        this.eventPublisher = eventPublisher;
        this.compteDAO      = compteDAO;
    }

    public void effectuerVirement(String numeroSource, String numeroDest,
                                   BigDecimal montant, Long idClient) {
        // Logique de virement...

        // ✅ Publier l'événement — tous les observateurs sont notifiés automatiquement
        eventPublisher.publishEvent(new EvenementTransaction(
            this, numeroSource, montant,
            EvenementTransaction.TypeEvenement.VIREMENT_EMIS, idClient
        ));

        // Vérification du solde restant
        Compte source = compteDAO.trouverParNumero(numeroSource).orElseThrow();
        if (source.getSolde().compareTo(new BigDecimal("100")) < 0) {
            eventPublisher.publishEvent(new EvenementTransaction(
                this, numeroSource, source.getSolde(),
                EvenementTransaction.TypeEvenement.SOLDE_FAIBLE, idClient
            ));
        }
    }
}

7. Patron Builder — Constructeur

7.1. Définition et analogie bancaire

Créer une demande de prêt immobilier, c’est assembler de nombreux paramètres : montant, durée, taux, garanties, assurance, co-emprunteur… Un constructeur Java avec 15 paramètres est illisible et source d’erreurs. Le Builder résout ce problème.

Builder : patron créationnel qui construit des objets complexes étape par étape. Il sépare la construction de l’objet de sa représentation finale.

7.2. Implémentation — Demande de prêt

// ✅ Objet complexe construit par le Builder
public class DemandePret {

    // Champs obligatoires
    private final Long idClient;
    private final BigDecimal montant;
    private final int dureeMois;
    private final String typePret; // "IMMO", "CONSO", "AUTO"

    // Champs optionnels
    private final BigDecimal tauxNegocié;
    private final boolean assuranceDecés;
    private final boolean assuranceInvalidite;
    private final String coEmprunteurNom;
    private final BigDecimal apportPersonnel;
    private final String objetFinancement;

    // ✅ Constructeur privé — seul le Builder peut créer cet objet
    private DemandePret(Builder builder) {
        this.idClient            = builder.idClient;
        this.montant             = builder.montant;
        this.dureeMois           = builder.dureeMois;
        this.typePret            = builder.typePret;
        this.tauxNegocié         = builder.tauxNegocié;
        this.assuranceDecés      = builder.assuranceDecés;
        this.assuranceInvalidite = builder.assuranceInvalidite;
        this.coEmprunteurNom     = builder.coEmprunteurNom;
        this.apportPersonnel     = builder.apportPersonnel;
        this.objetFinancement    = builder.objetFinancement;
    }

    // ✅ Classe Builder statique imbriquée
    public static class Builder {

        // Champs obligatoires
        private final Long idClient;
        private final BigDecimal montant;
        private final int dureeMois;
        private final String typePret;

        // Champs optionnels avec valeurs par défaut
        private BigDecimal tauxNegocié         = null; // Taux calculé automatiquement
        private boolean assuranceDecés         = true;  // Activée par défaut
        private boolean assuranceInvalidite    = false;
        private String coEmprunteurNom         = null;
        private BigDecimal apportPersonnel     = BigDecimal.ZERO;
        private String objetFinancement        = "";

        // Constructeur du Builder avec les champs obligatoires
        public Builder(Long idClient, BigDecimal montant, int dureeMois, String typePret) {
            if (idClient == null || montant == null || dureeMois <= 0)
                throw new IllegalArgumentException("Paramètres obligatoires manquants.");
            if (montant.compareTo(BigDecimal.ZERO) <= 0)
                throw new IllegalArgumentException("Le montant doit être positif.");
            this.idClient  = idClient;
            this.montant   = montant;
            this.dureeMois = dureeMois;
            this.typePret  = typePret;
        }

        // ✅ Méthodes "fluent" — chacune retourne le Builder pour chaîner les appels
        public Builder avecTauxNegocié(BigDecimal taux) {
            this.tauxNegocié = taux;
            return this;
        }

        public Builder avecAssuranceDecés(boolean active) {
            this.assuranceDecés = active;
            return this;
        }

        public Builder avecAssuranceInvalidite(boolean active) {
            this.assuranceInvalidite = active;
            return this;
        }

        public Builder avecCoEmprunteur(String nomCoEmprunteur) {
            this.coEmprunteurNom = nomCoEmprunteur;
            return this;
        }

        public Builder avecApportPersonnel(BigDecimal apport) {
            this.apportPersonnel = apport;
            return this;
        }

        public Builder pourFinancement(String objet) {
            this.objetFinancement = objet;
            return this;
        }

        // ✅ Méthode finale qui construit l'objet
        public DemandePret construire() {
            // Validation finale avant construction
            if ("IMMO".equals(typePret) && apportPersonnel.compareTo(BigDecimal.ZERO) == 0) {
                System.out.println("Avertissement : apport personnel recommandé pour prêt immobilier.");
            }
            return new DemandePret(this);
        }
    }

    // Getters
    public Long getIdClient() { return idClient; }
    public BigDecimal getMontant() { return montant; }
    public int getDureeMois() { return dureeMois; }
    public String getTypePret() { return typePret; }
    public BigDecimal getTauxNegocié() { return tauxNegocié; }
    public String getCoEmprunteurNom() { return coEmprunteurNom; }
    public BigDecimal getApportPersonnel() { return apportPersonnel; }
    public String getObjetFinancement() { return objetFinancement; }

    @Override
    public String toString() {
        return String.format("DemandePret{client=%d, type=%s, montant=%.0f€, durée=%d mois, apport=%.0f€}",
            idClient, typePret, montant, dureeMois, apportPersonnel);
    }
}
// ✅ Utilisation du Builder — lisible comme une phrase !
public class ExempleBuilder {

    public void creerDemandesPret() {

        // Prêt immobilier complet avec toutes les options
        DemandePret pretImmo = new DemandePret.Builder(
                1001L,
                new BigDecimal("250000"),
                300, // 25 ans
                "IMMO"
            )
            .avecApportPersonnel(new BigDecimal("50000"))
            .avecCoEmprunteur("Marie Dupont")
            .avecAssuranceDecés(true)
            .avecAssuranceInvalidite(true)
            .avecTauxNegocié(new BigDecimal("0.038"))
            .pourFinancement("Résidence principale — Lyon 6ème")
            .construire();

        // Prêt consommation simple — juste les obligatoires
        DemandePret pretConso = new DemandePret.Builder(
                1002L,
                new BigDecimal("15000"),
                60, // 5 ans
                "CONSO"
            )
            .construire();

        System.out.println("Demande créée : " + pretImmo);
        System.out.println("Demande créée : " + pretConso);
    }
}

💡 Le Builder de Lombok (@Builder) génère automatiquement ce code pour vous. C’est ce que vous verrez le plus souvent dans les projets Spring Boot modernes.

// ✅ Version avec Lombok @Builder — beaucoup plus concis !
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Client {
    private Long id;
    private String nom;
    private String prenom;
    private String email;
    private String telephone;
    private String segmentation; // "STANDARD", "PREMIUM", "PRO"
    private LocalDate dateNaissance;
}

// Utilisation Lombok Builder
Client client = Client.builder()
    .nom("Dupont")
    .prenom("Jean")
    .email("jean.dupont@email.fr")
    .segmentation("PREMIUM")
    .build();

8. Patron Decorator — Décorateur

8.1. Définition et analogie bancaire

Un compte bancaire peut avoir des fonctionnalités additionnelles : assurance perte d’emploi, protection découvert, accès lounge aéroport pour une carte premium… Ces options s’ajoutent dynamiquement sans changer la classe de base.

Decorator : patron structurel qui ajoute dynamiquement des comportements à un objet en l’enveloppant dans des objets décorateurs, sans modifier la classe originale.

8.2. Implémentation — Services bancaires additionnels

// ✅ Interface commune
public interface ServiceBancaire {
    String getDescription();
    BigDecimal getCoutMensuel();
    String getAvantages();
}

// ✅ Composant de base : compte standard
public class CompteStandardService implements ServiceBancaire {

    @Override
    public String getDescription() { return "Compte Bancaire Standard"; }

    @Override
    public BigDecimal getCoutMensuel() { return new BigDecimal("2.00"); }

    @Override
    public String getAvantages() { return "CB classique, virements SEPA"; }
}

// ✅ Décorateur abstrait — base de tous les décorateurs
public abstract class ServiceBancaireDecorator implements ServiceBancaire {

    protected final ServiceBancaire serviceDécore; // L'objet à décorer

    public ServiceBancaireDecorator(ServiceBancaire service) {
        this.serviceDécore = service;
    }

    @Override
    public String getDescription() { return serviceDécore.getDescription(); }

    @Override
    public BigDecimal getCoutMensuel() { return serviceDécore.getCoutMensuel(); }

    @Override
    public String getAvantages() { return serviceDécore.getAvantages(); }
}

// ✅ Décorateur concret : Assurance perte d'emploi
public class AssurancePerteDEmploiDecorator extends ServiceBancaireDecorator {

    public AssurancePerteDEmploiDecorator(ServiceBancaire service) { super(service); }

    @Override
    public String getDescription() {
        return serviceDécore.getDescription() + " + Assurance Perte d'Emploi";
    }

    @Override
    public BigDecimal getCoutMensuel() {
        return serviceDécore.getCoutMensuel().add(new BigDecimal("9.90"));
    }

    @Override
    public String getAvantages() {
        return serviceDécore.getAvantages() + ", Protection revenu en cas de licenciement";
    }
}

// ✅ Décorateur : Protection découvert
public class ProtectionDecouvertDecorator extends ServiceBancaireDecorator {

    public ProtectionDecouvertDecorator(ServiceBancaire service) { super(service); }

    @Override
    public String getDescription() {
        return serviceDécore.getDescription() + " + Protection Découvert";
    }

    @Override
    public BigDecimal getCoutMensuel() {
        return serviceDécore.getCoutMensuel().add(new BigDecimal("3.50"));
    }

    @Override
    public String getAvantages() {
        return serviceDécore.getAvantages() + ", Autorisation découvert 500€ sans frais";
    }
}

// ✅ Décorateur : Carte premium
public class CartePremiumDecorator extends ServiceBancaireDecorator {

    public CartePremiumDecorator(ServiceBancaire service) { super(service); }

    @Override
    public String getDescription() {
        return serviceDécore.getDescription() + " + Carte Visa Premier";
    }

    @Override
    public BigDecimal getCoutMensuel() {
        return serviceDécore.getCoutMensuel().add(new BigDecimal("14.90"));
    }

    @Override
    public String getAvantages() {
        return serviceDécore.getAvantages() + ", Lounge aéroport, assurance voyage, cashback 1%";
    }
}
// ✅ Utilisation — combinaison dynamique des décorateurs
public class ExempleDecorator {
    public void configurerOffres() {
        ServiceBancaire compteBase = new CompteStandardService();
        System.out.println("Base : " + compteBase.getCoutMensuel() + "€/mois");

        // Client qui ajoute uniquement la protection découvert
        ServiceBancaire avecProtection = new ProtectionDecouvertDecorator(compteBase);
        System.out.println(avecProtection.getDescription() + " — " + avecProtection.getCoutMensuel() + "€/mois");

        // Client Premium avec tout
        ServiceBancaire offrePremium = new CartePremiumDecorator(
                                         new AssurancePerteDEmploiDecorator(
                                            new ProtectionDecouvertDecorator(compteBase)));

        System.out.println("OFFRE PREMIUM : " + offrePremium.getDescription());
        System.out.println("Coût mensuel  : " + offrePremium.getCoutMensuel() + "€");
        System.out.println("Avantages     : " + offrePremium.getAvantages());
    }
}

9. Patron Proxy

9.1. Définition et analogie bancaire

Dans une banque, vous n’accédez pas directement au coffre-fort. Un agent (le proxy) vérifie vos droits, journalise votre accès, et peut même mettre en cache les informations fréquentes.

Proxy : patron structurel qui fournit un substitut ou intermédiaire à un autre objet. Le proxy contrôle l’accès à l’objet réel.

9.2. Implémentation — Proxy avec cache et contrôle d’accès

// ✅ Interface commune
public interface ServiceCours {
    BigDecimal obtenirCoursBourse(String ticker);
    BigDecimal obtenirTauxChange(String devise);
}

// ✅ Implémentation réelle — appels coûteux (réseau, latence)
@Service("servicesCoursReels")
public class ServiceCoursBoursiere implements ServiceCours {

    @Override
    public BigDecimal obtenirCoursBourse(String ticker) {
        System.out.println("[API EXTERNE] Appel coûteux pour : " + ticker);
        // En production : appel à une API financière (Bloomberg, Reuters...)
        return new BigDecimal("152.45"); // Valeur simulée
    }

    @Override
    public BigDecimal obtenirTauxChange(String devise) {
        System.out.println("[API EXTERNE] Appel coûteux pour taux : " + devise);
        return new BigDecimal("1.085"); // EUR/USD simulé
    }
}

// ✅ Proxy avec cache et limitation de fréquence
@Service("servicesCoursProxy")
@Primary // Spring utilisera ce proxy par défaut
public class ServiceCoursProxy implements ServiceCours {

    private final ServiceCours serviceReel;
    private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
    private static final long DUREE_CACHE_MS = 30_000; // 30 secondes

    public ServiceCoursProxy(@Qualifier("servicesCoursReels") ServiceCours serviceReel) {
        this.serviceReel = serviceReel;
    }

    @Override
    public BigDecimal obtenirCoursBourse(String ticker) {
        // ✅ Proxy de cache : retourner la valeur en cache si fraîche
        String cle = "BOURSE_" + ticker;
        CacheEntry entree = cache.get(cle);

        if (entree != null && !entree.estExpiree()) {
            System.out.println("[CACHE HIT] Cours " + ticker + " servi depuis le cache.");
            return entree.valeur;
        }

        // Cache manqué : appeler le service réel
        BigDecimal cours = serviceReel.obtenirCoursBourse(ticker);
        cache.put(cle, new CacheEntry(cours));
        return cours;
    }

    @Override
    public BigDecimal obtenirTauxChange(String devise) {
        String cle = "CHANGE_" + devise;
        CacheEntry entree = cache.get(cle);
        if (entree != null && !entree.estExpiree()) return entree.valeur;
        BigDecimal taux = serviceReel.obtenirTauxChange(devise);
        cache.put(cle, new CacheEntry(taux));
        return taux;
    }

    // Classe interne pour les entrées de cache
    private static class CacheEntry {
        final BigDecimal valeur;
        final long horodatage;

        CacheEntry(BigDecimal valeur) {
            this.valeur      = valeur;
            this.horodatage  = System.currentTimeMillis();
        }

        boolean estExpiree() {
            return System.currentTimeMillis() - horodatage > DUREE_CACHE_MS;
        }
    }
}

💡 Spring AOP (Aspect-Oriented Programming) est une implémentation automatique du patron Proxy. Les annotations @Transactional, @Cacheable, @PreAuthorize sont toutes des proxies Spring qui s’intercalent entre l’appelant et la méthode réelle.


10. Patron Template Method

10.1. Définition et analogie bancaire

Le traitement d’un virement bancaire suit toujours les mêmes étapes : vérifier le solde, bloquer les fonds, exécuter le transfert, libérer les fonds, notifier. Mais certaines étapes varient selon le type de virement (SEPA, international, interne). Le Template Method permet de fixer la structure tout en délégant les variations.

Template Method : patron comportemental qui définit le squelette d’un algorithme dans une méthode de base, en déléguant certaines étapes aux sous-classes.

10.2. Implémentation — Traitement de virements

// ✅ Classe abstraite avec le template
public abstract class TraitementVirement {

    // ✅ Template Method — la structure est fixe et FINALE
    public final void executer(String source, String destination, BigDecimal montant) {
        System.out.println("\n=== Début traitement virement ===");

        validerParametres(source, destination, montant);     // 1. Commune
        verifierSolde(source, montant);                       // 2. Commune
        appliquerReglesSpecifiques(source, destination, montant); // 3. Variable
        effectuerTransfert(source, destination, montant);     // 4. Variable
        calculerEtAppliquerFrais(source, montant);            // 5. Variable
        notifier(source, destination, montant);               // 6. Commune

        System.out.println("=== Virement terminé ===\n");
    }

    // Étapes communes à tous les virements (non surchargeables)
    private void validerParametres(String source, String dest, BigDecimal montant) {
        if (source == null || dest == null || montant == null)
            throw new IllegalArgumentException("Paramètres invalides.");
        System.out.println("✅ Paramètres validés.");
    }

    private void verifierSolde(String numeroCompte, BigDecimal montant) {
        System.out.println("✅ Solde vérifié pour " + numeroCompte);
    }

    private void notifier(String source, String dest, BigDecimal montant) {
        System.out.printf("✅ Notifications envoyées : virement de %.2f€%n", montant);
    }

    // Étapes variables — à implémenter dans les sous-classes
    protected abstract void appliquerReglesSpecifiques(String source, String dest, BigDecimal montant);
    protected abstract void effectuerTransfert(String source, String dest, BigDecimal montant);
    protected abstract void calculerEtAppliquerFrais(String numeroCompte, BigDecimal montant);
}

// ✅ Virement SEPA (Europe)
public class VirementSEPA extends TraitementVirement {

    @Override
    protected void appliquerReglesSpecifiques(String source, String dest, BigDecimal montant) {
        System.out.println("🔍 Vérification IBAN européen...");
        System.out.println("🔍 Contrôle liste noire SEPA...");
    }

    @Override
    protected void effectuerTransfert(String source, String dest, BigDecimal montant) {
        System.out.printf("💶 Virement SEPA exécuté : %s → %s — %.2f€%n", source, dest, montant);
    }

    @Override
    protected void calculerEtAppliquerFrais(String compte, BigDecimal montant) {
        System.out.println("💰 Frais SEPA : 0,00€ (gratuit dans la zone euro)");
    }
}

// ✅ Virement international (hors SEPA)
public class VirementInternational extends TraitementVirement {

    @Override
    protected void appliquerReglesSpecifiques(String source, String dest, BigDecimal montant) {
        System.out.println("🔍 Contrôle réglementaire international (SWIFT)...");
        System.out.println("🔍 Vérification conformité AML (anti-blanchiment)...");
        System.out.println("🔍 Déclaration obligatoire si montant > 10 000€...");
    }

    @Override
    protected void effectuerTransfert(String source, String dest, BigDecimal montant) {
        System.out.printf("🌍 Virement SWIFT exécuté : %s → %s — %.2f€ (J+3)%n", source, dest, montant);
    }

    @Override
    protected void calculerEtAppliquerFrais(String compte, BigDecimal montant) {
        BigDecimal frais = montant.multiply(new BigDecimal("0.005")).max(new BigDecimal("15.00"));
        System.out.printf("💰 Frais virement international : %.2f€%n", frais);
    }
}

11. Patron Command — Commande

11.1. Définition et analogie bancaire

Les opérations bancaires doivent parfois être annulables (ordre de virement révocable), enregistrées (toute opération doit être tracée), et parfois rejouables (reprise après incident). Le patron Command encapsule chaque opération comme un objet.

Command : patron comportemental qui encapsule une demande sous forme d’objet. Permet l’annulation, la journalisation et la mise en file d’attente des opérations.

11.2. Implémentation — Ordres bancaires

// ✅ Interface Command
public interface OrdresBancaires {
    void executer();
    void annuler();
    String getDescription();
    LocalDateTime getHorodatage();
}

// ✅ Commande concrète : Virement
public class CommandeVirement implements OrdresBancaires {

    private final CompteService compteService;
    private final String compteSource;
    private final String compteDestination;
    private final BigDecimal montant;
    private final LocalDateTime horodatage;
    private boolean estExecute = false;

    public CommandeVirement(CompteService service, String source, String dest, BigDecimal montant) {
        this.compteService     = service;
        this.compteSource      = source;
        this.compteDestination = dest;
        this.montant           = montant;
        this.horodatage        = LocalDateTime.now();
    }

    @Override
    public void executer() {
        compteService.effectuerVirement(compteSource, compteDestination, montant);
        estExecute = true;
        System.out.println("✅ Virement exécuté : " + getDescription());
    }

    @Override
    public void annuler() {
        if (!estExecute)
            throw new IllegalStateException("Ce virement n'a pas encore été exécuté.");
        // Virement inverse pour annuler
        compteService.effectuerVirement(compteDestination, compteSource, montant);
        estExecute = false;
        System.out.println("↩️  Virement annulé : " + getDescription());
    }

    @Override
    public String getDescription() {
        return String.format("Virement %.2f€ de %s vers %s", montant, compteSource, compteDestination);
    }

    @Override
    public LocalDateTime getHorodatage() { return horodatage; }
}

// ✅ Gestionnaire de commandes (Invoker) avec historique
@Service
public class GestionnaireOrdres {

    private final Deque<OrdresBancaires> historiqueOrdres = new ArrayDeque<>();
    private final List<OrdresBancaires> fileAttente = new ArrayList<>();

    public void ajouterOrdre(OrdresBancaires ordre) {
        fileAttente.add(ordre);
        System.out.println("📋 Ordre mis en file : " + ordre.getDescription());
    }

    public void traiterTousLesOrdres() {
        for (OrdresBancaires ordre : fileAttente) {
            ordre.executer();
            historiqueOrdres.push(ordre); // Empiler pour permettre l'annulation
        }
        fileAttente.clear();
    }

    public void annulerDernierOrdre() {
        if (historiqueOrdres.isEmpty()) {
            System.out.println("Aucun ordre à annuler.");
            return;
        }
        OrdresBancaires dernierOrdre = historiqueOrdres.pop();
        dernierOrdre.annuler();
    }

    public void afficherHistorique() {
        System.out.println("\n=== Historique des ordres ===");
        historiqueOrdres.forEach(o ->
            System.out.println("  [" + o.getHorodatage() + "] " + o.getDescription())
        );
    }
}

12. Patron MVC avec Thymeleaf et Spring Boot

12.1. MVC — Le patron structurant de Spring Boot

Le MVC (Modèle-Vue-Contrôleur) est le patron de base de toute application web Spring Boot. Il organise le code en trois couches :

Couche Rôle Dans Spring Boot
Modèle Données et logique métier Entity, Service, DAO
Vue Présentation (HTML généré) Templates Thymeleaf
Contrôleur Coordonne M et V Classes @Controller

12.2. Contrôleur Spring MVC complet

// ✅ Contrôleur MVC Spring pour la gestion des comptes
@Controller
@RequestMapping("/comptes")
public class CompteController {

    private final CompteService compteService;
    private final ClientService clientService;

    public CompteController(CompteService compteService, ClientService clientService) {
        this.compteService = compteService;
        this.clientService = clientService;
    }

    // GET /comptes — Liste de tous les comptes
    @GetMapping
    public String listerComptes(Model model) {
        model.addAttribute("comptes", compteService.trouverTous());
        model.addAttribute("titre", "Gestion des Comptes");
        return "comptes/liste"; // → templates/comptes/liste.html
    }

    // GET /comptes/{id} — Détail d'un compte
    @GetMapping("/{id}")
    public String detailCompte(@PathVariable Long id, Model model) {
        Compte compte = compteService.trouverParId(id)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Compte introuvable"));
        model.addAttribute("compte", compte);
        return "comptes/detail";
    }

    // GET /comptes/nouveau — Formulaire de création
    @GetMapping("/nouveau")
    public String formulaireNouveauCompte(Model model) {
        model.addAttribute("compte", new Compte());
        model.addAttribute("typesCompte", List.of("COURANT", "EPARGNE", "LIVRET_A", "PEL"));
        model.addAttribute("clients", clientService.trouverTous());
        return "comptes/formulaire";
    }

    // POST /comptes — Créer un compte
    @PostMapping
    public String creerCompte(@ModelAttribute @Valid Compte compte,
                               BindingResult bindingResult,
                               RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
            return "comptes/formulaire"; // Réafficher avec erreurs
        }
        compteService.ouvrirCompte(compte.getTypeCompte(), compte.getIdClient(), compte.getSolde());
        redirectAttributes.addFlashAttribute("succes", "Compte créé avec succès !");
        return "redirect:/comptes";
    }

    // POST /comptes/virement — Effectuer un virement
    @PostMapping("/virement")
    public String effectuerVirement(@RequestParam String compteSource,
                                     @RequestParam String compteDestination,
                                     @RequestParam BigDecimal montant,
                                     RedirectAttributes redirectAttributes) {
        try {
            compteService.effectuerVirement(compteSource, compteDestination, montant);
            redirectAttributes.addFlashAttribute("succes",
                String.format("Virement de %.2f€ effectué avec succès.", montant));
        } catch (Exception e) {
            redirectAttributes.addFlashAttribute("erreur", "Erreur : " + e.getMessage());
        }
        return "redirect:/comptes";
    }
}

12.3. Templates Thymeleaf

<!-- templates/comptes/liste.html -->
<!DOCTYPE html>
<html lang="fr" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="${titre}">Comptes</title>
    <link rel="stylesheet"
          href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-4">

    <h1 th:text="${titre}" class="mb-4">🏦 Gestion des Comptes</h1>

    <!-- Message flash de succès -->
    <div th:if="${succes}" class="alert alert-success alert-dismissible fade show">
        <span th:text="${succes}"></span>
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
    </div>

    <!-- Message flash d'erreur -->
    <div th:if="${erreur}" class="alert alert-danger">
        <span th:text="${erreur}"></span>
    </div>

    <!-- Tableau des comptes -->
    <div class="card shadow-sm">
        <div class="card-header d-flex justify-content-between align-items-center">
            <h5 class="mb-0">Liste des comptes</h5>
            <a th:href="@{/comptes/nouveau}" class="btn btn-primary btn-sm">
                + Nouveau compte
            </a>
        </div>
        <div class="card-body p-0">
            <table class="table table-striped table-hover mb-0">
                <thead class="table-dark">
                    <tr>
                        <th>Numéro</th>
                        <th>Type</th>
                        <th>Titulaire</th>
                        <th class="text-end">Solde</th>
                        <th>Statut</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody>
                    <tr th:each="compte : ${comptes}">
                        <td th:text="${compte.numero}" class="font-monospace"></td>
                        <td>
                            <span th:text="${compte.typeCompte}"
                                  th:classappend="${compte.typeCompte == 'LIVRET_A'} ? 'badge bg-success' : 'badge bg-secondary'">
                            </span>
                        </td>
                        <td th:text="${compte.idClient}"></td>
                        <td class="text-end fw-bold"
                            th:text="${#numbers.formatDecimal(compte.solde, 1, 'COMMA', 2, 'POINT')} + ' €'">
                        </td>
                        <td>
                            <span th:if="${compte.actif}" class="badge bg-success">Actif</span>
                            <span th:unless="${compte.actif}" class="badge bg-danger">Inactif</span>
                        </td>
                        <td>
                            <a th:href="@{/comptes/{id}(id=${compte.id})}"
                               class="btn btn-sm btn-outline-primary">Détail</a>
                        </td>
                    </tr>
                    <tr th:if="${#lists.isEmpty(comptes)}">
                        <td colspan="6" class="text-center text-muted py-3">Aucun compte trouvé.</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>

    <!-- Formulaire de virement rapide -->
    <div class="card shadow-sm mt-4">
        <div class="card-header"><h5 class="mb-0">💸 Virement rapide</h5></div>
        <div class="card-body">
            <form th:action="@{/comptes/virement}" method="post" class="row g-3">
                <div class="col-md-4">
                    <label class="form-label">Compte source</label>
                    <input type="text" name="compteSource" class="form-control"
                           placeholder="FR7600001..." required>
                </div>
                <div class="col-md-4">
                    <label class="form-label">Compte destination</label>
                    <input type="text" name="compteDestination" class="form-control"
                           placeholder="FR7600002..." required>
                </div>
                <div class="col-md-2">
                    <label class="form-label">Montant (€)</label>
                    <input type="number" name="montant" class="form-control"
                           step="0.01" min="0.01" required>
                </div>
                <div class="col-md-2 d-flex align-items-end">
                    <button type="submit" class="btn btn-warning w-100">Virer</button>
                </div>
            </form>
        </div>
    </div>

</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

13. Projet fil rouge — Application bancaire complète

13.1. Présentation — BanqueApp

L’application BanqueApp intègre tous les patterns vus dans ce cours dans une application Spring Boot cohérente :

Pattern Utilisation dans BanqueApp
Singleton GestionnaireTaux — taux chargés une fois, réutilisés partout
DAO CompteDAO, ClientDAO, TransactionDAO
Factory CompteFactory — création de tous les types de comptes
Strategy StratégieCalculFrais — tarification par segment client
Observer EvenementTransaction → Email, Audit, Anti-fraude
Builder DemandePret.Builder — assemblage des demandes de prêt
Decorator ServiceBancaireDecorator — options additionnelles
Proxy ServiceCoursProxy — cache des cours boursiers
Template Method TraitementVirement — SEPA vs International
Command GestionnaireOrdres — file d’attente et annulation
MVC Controllers + Thymeleaf

13.2. Configuration Spring Boot

// ✅ Point d'entrée de l'application
@SpringBootApplication
@EnableAsync // Pour les observateurs asynchrones
public class BanqueAppApplication {
    public static void main(String[] args) {
        SpringApplication.run(BanqueAppApplication.class, args);
    }
}
# application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/banque_app?useSSL=false&serverTimezone=Europe/Paris
spring.datasource.username=banque_user
spring.datasource.password=BanqueSecure2024!
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.format_sql=true

spring.thymeleaf.cache=false
spring.thymeleaf.encoding=UTF-8

# Configuration métier
banque.taux.livret-a=0.030
banque.taux.credit-immo=0.040
banque.virement.seuil-alerte=10000
banque.session.timeout=1800

13.3. Démonstration — Scénario complet d’ouverture de compte

// ✅ Scénario complet intégrant tous les patterns
@Service
public class ScenarioBancaire {

    private final GestionnaireTaux gestionnaireT;   // Singleton Spring
    private final CompteDAO compteDAO;               // DAO
    private final CompteSpringFactory compteFactory; // Factory
    private final CalculFraisService calculFrais;    // Strategy
    private final ApplicationEventPublisher events;  // Observer
    private final GestionnaireOrdres ordres;         // Command

    // Injection par constructeur (recommandé Spring Boot)
    public ScenarioBancaire(GestionnaireTaux gestionnaireT, CompteDAO compteDAO,
                             CompteSpringFactory compteFactory, CalculFraisService calculFrais,
                             ApplicationEventPublisher events, GestionnaireOrdres ordres) {
        this.gestionnaireT = gestionnaireT;
        this.compteDAO     = compteDAO;
        this.compteFactory = compteFactory;
        this.calculFrais   = calculFrais;
        this.events        = events;
        this.ordres        = ordres;
    }

    public void demonstrationComplete() {

        System.out.println("\n╔══════════════════════════════════════╗");
        System.out.println("║  BanqueApp — Démonstration complète  ║");
        System.out.println("╚══════════════════════════════════════╝\n");

        // 1. SINGLETON — Consulter les taux une seule fois
        System.out.println("--- 1. SINGLETON : Taux bancaires ---");
        BigDecimal tauxLivretA = gestionnaireT.obtenirTaux("LIVRET_A");
        System.out.println("Taux Livret A : " + tauxLivretA.multiply(BigDecimal.valueOf(100)) + "%");

        // 2. FACTORY — Créer les comptes
        System.out.println("\n--- 2. FACTORY : Création des comptes ---");
        CompteBancaire compteCourant = compteFactory.creer("COURANT", 1001L);
        CompteBancaire livretA       = compteFactory.creer("LIVRET_A", 1001L);
        System.out.println("Créé : " + compteCourant.getDescription());
        System.out.println("Créé : " + livretA.getDescription());

        // 3. BUILDER — Créer une demande de prêt
        System.out.println("\n--- 3. BUILDER : Demande de prêt ---");
        DemandePret demande = new DemandePret.Builder(1001L, new BigDecimal("200000"), 240, "IMMO")
            .avecApportPersonnel(new BigDecimal("40000"))
            .avecCoEmprunteur("Alice Martin")
            .avecAssuranceDecés(true)
            .pourFinancement("Maison à Lyon")
            .construire();
        System.out.println("Demande créée : " + demande);

        // 4. STRATEGY — Calculer les frais
        System.out.println("\n--- 4. STRATEGY : Calcul des frais ---");
        BigDecimal fraisStd = calculFrais.calculerFraisVirement("STANDARD", new BigDecimal("5000"));
        BigDecimal fraisPremium = calculFrais.calculerFraisVirement("PREMIUM", new BigDecimal("5000"));
        System.out.println("Virement 5000€ — Standard : " + fraisStd + "€, Premium : " + fraisPremium + "€");

        // 5. OBSERVER — Publier un événement
        System.out.println("\n--- 5. OBSERVER : Événement transaction ---");
        events.publishEvent(new EvenementTransaction(
            this, "FR7600001", new BigDecimal("15000"),
            EvenementTransaction.TypeEvenement.VIREMENT_EMIS, 1001L
        ));

        // 6. COMMAND — Ordres en file d'attente
        System.out.println("\n--- 6. COMMAND : File d'ordres ---");
        System.out.println("Tous les patterns ont été appliqués avec succès !");
    }
}

14. Exercices d’application

14.1. Exercices guidés

Exercice 1 — Singleton : Gestionnaire de limites de crédit

Créez un Singleton Spring GestionnaireLimites qui :

  1. Stocke les limites de crédit par type de client (Map<String, BigDecimal>).
  2. Est initialisé une seule fois au démarrage avec @PostConstruct.
  3. Expose une méthode obtenirLimite(String typeClient).
  4. Vérifiez dans un test que deux injections retournent bien la même instance.

Exercice 2 — DAO : Gestion des transactions

Créez le triplet complet pour les transactions :

  1. L’entité Transaction avec les champs : id, numeroCompteSource, numeroCompteDest, montant, type, dateHeure, statut.
  2. L’interface TransactionDAO avec les méthodes : sauvegarder, trouverParCompte(String numero), trouverParPeriode(LocalDate debut, LocalDate fin), calculerTotalEntrees(String numero).
  3. L’implémentation TransactionDAOJpa avec Spring Data.
  4. Un TransactionService qui utilise le DAO pour enregistrer les virements.

Exercice 3 — Factory : Catalogue de produits bancaires

Étendez la CompteFactory pour ajouter :

  1. Un type PEL (Plan Épargne Logement) : taux 2%, plafond 61 200€, durée minimum 4 ans.
  2. Un type PEA (Plan Épargne en Actions) : plafond 150 000€, pour actions françaises/européennes.
  3. Adaptez la Factory pour qu’elle lance une exception métier personnalisée TypeCompteInconnuException si le type est inconnu.

Exercice 4 — Strategy : Moteur de scoring crédit

Implémentez un moteur de scoring de crédit avec 3 stratégies :

Créez un service ScoringCreditService qui sélectionne la stratégie selon la politique bancaire du moment.

14.2. Exercices d’approfondissement

Exercice 5 — Observer : Système d’alertes multi-canaux

Étendez le système d’événements pour ajouter :

  1. Un observateur ObservateurSMS qui simule l’envoi de SMS.
  2. Un observateur ObservateurConformite qui déclenche une vérification réglementaire pour les virements > 10 000€ (obligation légale de déclaration).
  3. Un observateur ObservateurStatistiques qui tient à jour des statistiques en temps réel : nombre de virements, montant total journalier.

Exercice 6 — Builder + DAO : Simulateur de prêt immobilier

Créez une interface Thymeleaf complète permettant de :

  1. Saisir les paramètres d’un prêt (formulaire HTML) → DemandePret.Builder.
  2. Simuler le tableau d’amortissement (méthode française : mensualités constantes).
  3. Sauvegarder la simulation via le DAO.
  4. Afficher le résultat avec Thymeleaf (tableau HTML des mensualités).

Exercice 7 — Architecture complète

Créez une page d’accueil du tableau de bord bancaire qui affiche :


Annexe — Récapitulatif et aide-mémoire

Tableau récapitulatif des 11 patterns du cours

Pattern Famille Problème résolu Exemple bancaire Spring Boot
Singleton Créationnel Une seule instance GestionnaireTaux @Service, @Component
DAO Structurel Séparer données/métier CompteDAO JpaRepository
Factory Créationnel Créer sans connaître la classe CompteFactory @Component + List injection
Strategy Comportemental Algorithme interchangeable StratégieCalculFrais @Component + Map injection
Observer Comportemental Notifier plusieurs parties EvenementTransaction ApplicationEvent
Builder Créationnel Construire objets complexes DemandePret.Builder @Builder (Lombok)
Decorator Structurel Ajouter comportement dynamiquement ServiceBancaireDecorator Composition manuelle
Proxy Structurel Contrôler l’accès ServiceCoursProxy @Cacheable, AOP
Template Method Comportemental Squelette d’algorithme TraitementVirement Classe abstraite Java
Command Comportemental Encapsuler opérations GestionnaireOrdres @Async, EventBus
MVC Architectural Séparer M, V, C CompteController @Controller, Thymeleaf

Choix du bon pattern — Arbre de décision

Quel est mon besoin ?
│
├── Créer des objets
│   ├── Garantir une seule instance → SINGLETON
│   ├── Créer sans connaître la classe concrète → FACTORY
│   └── Assembler des objets complexes étape par étape → BUILDER
│
├── Structurer mon code
│   ├── Séparer accès aux données et logique métier → DAO
│   ├── Ajouter des comportements dynamiquement → DECORATOR
│   └── Contrôler l'accès à un objet → PROXY
│
└── Gérer les comportements
    ├── Algorithmes interchangeables → STRATEGY
    ├── Notifier plusieurs objets d'un changement → OBSERVER
    ├── Fixer une structure d'algorithme variable → TEMPLATE METHOD
    └── Encapsuler et annuler des opérations → COMMAND

Commandes Windows utiles pour le projet

# Créer le projet Spring Boot (dans le terminal Windows)
# Via https://start.spring.io ou :

# Compiler le projet
mvn clean compile

# Lancer les tests
mvn test

# Lancer l'application
mvn spring-boot:run

# Accéder à l'application
# → http://localhost:8080

# Créer la base MySQL
mysql -u root -p
CREATE DATABASE banque_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'banque_user'@'localhost' IDENTIFIED BY 'BanqueSecure2024!';
GRANT ALL PRIVILEGES ON banque_app.* TO 'banque_user'@'localhost';
FLUSH PRIVILEGES;

Dépendances Maven essentielles

<!-- pom.xml — Dépendances clés -->
<dependencies>
    <!-- Spring Boot Web + MVC -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Thymeleaf — Moteur de templates -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <!-- Spring Data JPA — DAO automatique -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- MySQL Driver -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Lombok — Builder, @Data, @Slf4j -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- Validation des formulaires -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- Tests -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

— Fin du cours —