Aller au contenu

Design Patterns en Java — Cours pratique pour développeur.euse.s

Spring Boot 3 · Java 17 · Thymeleaf · Windows


Sommaire


1. Introduction aux Design Patterns

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

Imaginez que vous travaillez 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 et hop, ça marche.

1.2. Origine — Le Gang of Four

En 1994, 4 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 3 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 et pas seulement, 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 ou pas.

Voici l’environnement de travail :

Normalement, c’est le cas sur nos machines.

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 ou Eclipse
├── MySQL 8 (via XAMPP ou Docker Desktop) ou PostgreSQL
└── Thymeleaf (intégré à Spring Boot) ultérieurement

Vérification de votre 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 semaine 4)

Pour les patterns que nous allons étudier en premier, un projet simple Java suffira pour le moment.

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. Singleton

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

Imaginons que vous travailliez à la banque de France, 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 et générer du gaspillage mémoire, des incohérences.

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 : 2 threads peuvent créer 2 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) : 2 requêtes simultanées peuvent créer 2 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 (nous verrons cela ultérieurement sin on a le temps)
    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; // 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 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. DAO — Data Access Object

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

Dans une application bancaire et autre, 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 en 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) : Design pattern 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. C’est ce dont nous avons parlé lors de la semaine précédente.

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" (on pourrait avoir des sous-classes mais on simplifie ici)

    @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

Ici je n’ai pas intégré des Excptions personnalisées pour simplifier mais il faudrait le faire en prod.

//  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 Design pattern DAO

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

4. Factory — Fabrique

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

Dans la gestion des comptes client, 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 et la maintenance est 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) : Design pattern 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)));
    }
}

Classe CompteFactory : C’est la méthode créer() de CompteFactory qui va instancier les différents types de Compte grâce au Type, donc tout se fait dans cette classe !

//  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 Design pattern Factory

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

5. 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 et 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 et c’est risqué !

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

Exemple très simple

Ce Pattern c’est comme choisir un super-pouvoir pour un personnage dans un jeu vidéo. Par exemple, notre Robot peut se déplacer. Parfois, il marche, parfois il vole, parfois il nage. Au lieu de changer tout le code du robot, on lui donne juste un nouveau super-pouvoir (une nouvelle stratégie) :

Cela donnerait le code suivant :

interface Deplacement {
    void deplacer();
}

class Marcher implements Deplacement {
    public void deplacer() { System.out.println("Je marche !"); }
}

class Voler implements Deplacement {
    public void deplacer() { System.out.println("Je vole !"); }
}

class Robot {
    private Deplacement strategie;

    public void setStrategie(Deplacement strategie) {
        this.strategie = strategie;
    }

    public void bouger() {
        strategie.deplacer(); // "Je marche !" ou "Je vole !"
    }
}

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, StrategieCalculFrais> strategies;

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

    public BigDecimal calculerFraisVirement(String typeClient, BigDecimal montant) {
        StrategieCalculFrais 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 StrategieCalculFrais obtenirStrategie(String typeClient) {
        String cle = switch (typeClient.toUpperCase()) {
            case "PREMIUM"        -> "fraisPremium";
            case "PROFESSIONNEL"  -> "fraisProfessionnel";
            default               -> "fraisStandard";
        };
        return strategies.getOrDefault(cle, strategies.get("fraisStandard"));
    }
}

6. 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 : Design pattern comportemental qui définit une relation 1-N entre objets. Quand l’objet observé change d’état, tous ses observateurs sont notifiés automatiquement.

Même si ce pattern est assez facile à comprendre, voici un exemple simple :

Quand vous êtes abonné.e à une chaîne YouTube ou autre : Vous êtes prévenu à chaque nouvelle vidéo !

En version code :

// Encore une interface ;)
interface Observateur {
    void mettreAJour(String message);
}

class ChaîneYouTube {
    private List<Observateur> abonnés = new ArrayList<>();

    public void abonner(Observateur observateur) {
        abonnés.add(observateur);
    }

    public void notifier(String video) {
        for (Observateur internaute : abonnés) {
            internaute.mettreAJour("Nouvelle vidéo : " + video);
        }
    }
}

class Internaute implements Observateur {
    private String nom;

    public Internaute(String nom) { this.nom = nom; }

    public void mettreAJour(String message) {
        System.out.println(nom + " a reçu : " + message);
    }
}

// Utilisation :
ChaîneYouTube chaines = new ChaîneYouTube();
Internaute maeva = new Internaute("Maeva");
Internaute leopold = new Internaute("Leopold");
chaines.abonner(maeva);
chaines.abonner(leopold);
chaines.notifier("Comment coder le pattern Observer en Java ?");
// maeva et leopold reçoivent la notification !

Du coup, tout le monde est automatiquement prévenu quand quelque chose change !

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. 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 : Design pattern créationnel qui construit des objets complexes étape par étape. Il sépare la construction de l’objet de sa représentation finale !

Avant d’aborder des exemples complexes, un exemple simple…

Imaginez que vous devez construire un burger chez McDonald’s.

On ajoute des ingrédients un par un.

Exemple : Vous voulez un burger avec pain, steak, fromage, salade, sauce. Au lieu de dire “Donne-moi un burger avec tout”, on dis :

Comme ça, vous pouvez éviter la salade si vous n’aimez pas ça ! (Je déconseille)

En code :

package fr.mer.gestion.semaine3.builder;

class Burger {

	private String pain, steak, fromage, salade, sauce;
	
	
	@Override
	public String toString() {
		return "Burger avec : \n" + getAvecOuSans(pain, "pain") + "\n" + getAvecOuSans(steak, "Steak") + "\n" + getAvecOuSans(fromage, "Fromage") + "\n" + getAvecOuSans(salade, "Salade") + "\n"
				+ getAvecOuSans(sauce, "Sauce");
	}
    // Méthode optionnelle pour l'affichage propre en console
	private String getAvecOuSans(String ingredient, String categorie) {
		
		if (ingredient == null) return "Sans "+ categorie;
		return ingredient;
	}
	
	// constructeur privé
	private Burger() {} 

	// Noter que cette classe est "static"
	public static class Builder {

		private Burger burger = new Burger();

		// La méthode retourne la classe Builder
		public Builder ajouterPain(String pain) {
			burger.pain = pain;
			return this;
		}
		// La méthode retourne la classe Builder
		public Builder ajouterSteak(String steak) {
			burger.steak = steak;
			return this;
		}

		// La méthode retourne la classe Builder
		public Builder ajouterFromage(String fromage) {
			burger.fromage = fromage;
			return this;
		}

		// La méthode retourne un objet de type Burger
		public Burger construire() {
			return burger;
		}
	}
}

Utilisation :

Burger monBurger = new Burger.Builder()
    .ajouterPain("Pain aux graines")
    .ajouterSteak("Steak haché")
    .ajouterFromage("Cheddar")
    .construire();

    System.out.println(monBurger); 

burger attendu

On peut donc créer plein de burgers différents sans se tromper et avec les ingrédients souhaités.

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
// ici, vous constatez que les setters comme setNom(), setPrenom(), etc. sont simplement remplacés par le nom de l'attribut.
Client client = Client.builder()
    .nom("Dupont")
    .prenom("Jean")
    .email("jean.dupont@email.fr")
    .segmentation("PREMIUM")
    .build();

8. 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 : Design pattern structurel qui ajoute dynamiquement des comportements à un objet en l’enveloppant dans des objets décorateurs, sans modifier la classe originale. C’est bien pratique !

Exemple simple

C’est comme habiller un bonhomme de neige avec des divers accessoires !

Exemple :

En code cela donnerait ceci :

// Bien pratique les interfaces
interface BonhommeNeige {
    String habiller();
}

class BonhommeNu implements BonhommeNeige {
    public String habiller() { return "Bonhomme de neige nu"; }
}

class DecorateurAccessoire implements BonhommeNeige {
    protected BonhommeNeige bonhomme;

    public DecorateurAccessoire(BonhommeNeige bonhomme) {
        this.bonhomme = bonhomme;
    }

    public String habiller() {
        return bonhomme.habiller();
    }
}

class Chapeau extends DecorateurAccessoire {
    public Chapeau(BonhommeNeige bonhomme) { super(bonhomme); }

    public String habiller() {
        return super.habiller() + " + chapeau";
    }
}

class Echarpe extends DecorateurAccessoire {
    public Echarpe(BonhommeNeige bonhomme) { super(bonhomme); }

    public String habiller() {
        return super.habiller() + " + écharpe";
    }
}

// Utilisation :
BonhommeNeige monBonhomme = new BonhommeNu();
monBonhomme = new Chapeau(monBonhomme);
monBonhomme = new Echarpe(monBonhomme);
System.out.println(monBonhomme.habiller());
// affichera : Bonhomme de neige nu + chapeau + écharpe

Pas toujours évident au début, mais bien pratique. On peut enlever ou ajouter d’autres choses sans tout refaire !

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. 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 : Design pattern 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. 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 : Design pattern 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. 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 : Design pattern 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. MVC avec Thymeleaf et Spring Boot semaine 5

12.1. MVC — Le Design pattern structurant de Spring Boot

Le MVC (Modèle-Vue-Contrôleur) est le design pattern 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 complète semaine 4

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");

        // 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)) + "%");

        // 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());

        // 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);

        // 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 + "€");

        // 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
        ));

        // 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 !");
    }
}

Complément sur les Patterns

Lien vers d’autres Design Pattern


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>