Aller au contenu

Semaine 3 : JOUR 2 - Design Patterns (Singleton, Factory, DAO,…)

Formation Java / Spring Boot pour développeurs COBOL

Démonstration DAO / Hibernate

Lien vers les cours détaillés sur les Patterns

Exemple de code avec l’utilisation du modèle DAO avec implémentation (version Hibernate)

Exemple complet DAO avec JDBC sans Hibernate ni Spring Boot avec PostgreSQL

C’est là que l’on découvre l’intérêt d’utiliser des interfaces ! A ce stade, vous comprenez cette notion d’interface et la nécessité d’implémenter (c’est-a-dire, écrire des méthodes de l’interface dans une ou plusieurs classes spécifiques).

Bonus - Pattern Visitor


Objectifs

À l’issue de cette journée, vous serez capable de :

Objectif clé : Reconnaître et les utiliser si besoin.


Contexte

En COBOL, vous avez déjà utilisé des patterns sans les nommer :

Les Design Patterns sont : des solutions récurrentes à des problèmes récurrents.

En général, ils sont nommées, documentées et partagées. Vous trouverez beaucoup d’information sur le Web et en librairie spécialisée.

En Java, on nomme ce que l’on fait, pour mieux communiquer en équipe.


Qu’est-ce qu’un Design Pattern ?

Définition simple

Un Design Pattern est :

Ce n’est :

C’est une façon d’organiser le code. D’ailleurs la plupart des Frameworks utilisent de nombreux modèles de conception.


Pourquoi les Design Patterns sont importants ?

En réunion, dans un Sprint de méthodes agiles, vous entendrez souvent :


PATTERN 1 – SINGLETON

Problème à résoudre

Comment garantir qu’une classe ne permet d’avoir qu’une seule instance dans toute l’application ?

En Java, sans précaution, chaque new crée un objet.


Exemple concret (métier / technique)


Implémentation du Singleton

public class Configuration {

    private static Configuration instance;

    private Configuration() {
        // constructeur privé
    }

    // méthode spécifique publique static pour retourner une instance existente ou à créer
    public static Configuration getInstance() {
        if (instance == null) {
            instance = new Configuration();
        }
        return instance;
    }
}

Utilisation :

Configuration config = Configuration.getInstance();

Points clés à comprendre

En Spring, ce pattern sera géré automatiquement (plus tard avec une annotation).


Erreurs fréquentes (Singleton)

  1. Rendre le constructeur public
  2. Créer plusieurs instances par erreur
  3. Utiliser un Singleton pour tout
  4. Confondre Singleton et variable globale

PATTERN 2 – FACTORY

Problème à résoudre

Comment créer des objets sans exposer la logique de création ?


Analogie COBOL

un programme qui décide quel type de traitement lancer selon des paramètres.

En Java, on évite les if / else partout dans le code car c’est difficile à maintenir.


Exemple métier simple


Mauvaise approche (à éviter)

if (type.equals("COURANT"))
{
    return new CompteCourant(...);
}
else if (type.equals("EPARGNE"))
{
    return new CompteEpargne(...);
}

Factory – implémentation simple (une Factory de comptes)

On implémente une méthode statique et publique chargée de faire le travail d’instanciation en fonction du type.

public class CompteFactory {

    public static Compte creerCompte(String type, String numero, BigDecimal solde) {

        if (type.equals("COURANT")) {
            return new CompteCourant(numero, solde, new BigDecimal("500"));
        }

        if (type.equals("EPARGNE")) {
            return new CompteEpargne(numero, solde);
        }

        throw new IllegalArgumentException("Type inconnu");
    }
}

Utilisation :

Compte c = CompteFactory.creerCompte("COURANT", "FR001", new BigDecimal("1000"));

Vous voyez que l’on ajoute le “type” de Compte dans le constructeur de creerCompte qui se charge de faire le travail d’instancier les bons types de compte.


Avantages de la Factory

Une Factory utilise aussi des if (ou switch), donc ce n’est PAS une révolution syntaxique. La différence n’est pas dans le “comment écrire”, mais dans :

QUI décide de créer l’objet et OÙ est située cette logique ?

Le vrai problème du “if” classique dans notre exemple, est que le code est généralement répété partout dans l’application :

Exemple :

// service A
if (type.equals("COURANT")) ...

// service B
if (type.equals("EPARGNE")) ...

// controller
if (type.equals("COURANT")) ...

Et cela engendre une duplication, un couplage fort, une maintenance pénible et des bugs assurés à long terme.

On récapitule avec des bouts de code :

public class CompteFactory {
    public static Compte creerCompte(...) {
        // UN SEUL endroit !
    }
}

Le code appelant : Compte c = CompteFactory.creerCompte(...); ne connaît PAS :

Mais seulement : Compte !

Autre Exemple concret d’évolution

Le cas sans Factory, imaginons que vous ajoutiez un nouveau type : ComptePremium !

Vous devez modifier le code dans de nombreuses classes :

if (…) // Service A if (…) // Service B if (…) // Controller if (…) // partout…

Le même cas Avec Factory, on modifie le code qu’à un seul endroit :

if (type.equals("PREMIUM")) {
    return new ComptePremium(...);
}

Le reste continue de fonctionner en toutes logique.

Résumé sous forme de tableau

Aspect IF classique Factory
Nombre d’endroits multiple 1 seul
Couplage fort faible
Lisibilité dispersée centralisée
Maintenance difficile simple
Évolutivité fragile propre

Autre exemple de code pour le fun

Dans cet exemple, on utilise plus de if mais une HashMap. C’est plus propre et joli !

public class CompteFactory {

    // on crée une Map qui associe un type (String) à une fonction qui fabrique un Compte
    // Le Map<String> correspond à la clef de notre Map qui est soit "COURANT" , soit EPARGNE".
    // "Function<ParamCompte, Compte>>" c'est une interface fonctionnelle qui dit "Donne-moi des paramètres et je te crée un Compte". La Fonction est un fabricant de Compte !

    private static final Map<String, Function<ParamCompte, Compte>> registry = new HashMap<>();
    // Ce code est particulier et permet qu'il ne soit exécuté qu'UNE SEULE FOIS au chargement de la classe. On le fait aussi souvent pour dans des classes de connection.
    //  "p -> new CompteCourant()" est une fonction anonyme qui signifie : Si on demande "COURANT", j’utilise cette fonction pour créer l’objet.
    static {
        registry.put("COURANT", p -> new CompteCourant(p.numero(), p.solde(), new BigDecimal("500")));
        registry.put("EPARGNE", p -> new CompteEpargne(p.numero(), p.solde()));
    }

    public static Compte creerCompte(String type, String numero, BigDecimal solde) {

        Function<ParamCompte, Compte> creator = registry.get(type);

        if (creator == null) {
            throw new IllegalArgumentException("Type de compte inconnu !");
        }
        // sinon, si tout est ok, on applique la création du Compte
        return creator.apply(new ParamCompte(numero, solde));
    }
}

Concretement quand on fait : Compte c = CompteFactory.creerCompte("COURANT", "FR001", solde);

Dans la factory : Function<ParamCompte, Compte> creator = registry.get("COURANT");, creator contient :

p -> new CompteCourant(...) puis : return creator.apply(new ParamCompte(...)); donc l’équivalent de new CompteCourant(...)

PATTERN 3 – DAO (DATA ACCESS OBJECT)

Problème à résoudre

Comment isoler l’accès aux données du reste du code ?

Principe du DAO


Exemple sans base de données

Interface DAO

public interface CompteDAO {
    void sauvegarder(Compte compte);
    Compte trouverParNumero(String numero);
}

Implémentation mémoire (pour l’instant)

public class CompteDaoMemoire implements CompteDAO {

    private Map<String, Compte> stockage = new HashMap<>();

    public void sauvegarder(Compte compte) {
        stockage.put(compte.getNumero(), compte);
    }

    public Compte trouverParNumero(String numero) {
        return stockage.get(numero);
    }
}

Plus tard :


Avantages du DAO


MINI-PROJET POO

Objectif du mini-projet

Assembler tous les patterns vus aujourd’hui dans un mini-système cohérent.


Architecture cible


TP 1 – Création via Factory

Consignes :

  1. Supprimer tous les new CompteCourant dans main
  2. Passer exclusivement par CompteFactory
  3. Tester avec plusieurs types

TP 2 – Persistance via DAO

Consignes :

  1. Utiliser CompteDAO
  2. Sauvegarder les comptes
  3. Les récupérer par numéro
  4. Aucune structure Map dans main

TP 3 – Singleton de configuration

Consignes :


Corrigé – Extrait global

CompteDAO dao = new CompteDaoMemoire();

Compte c1 = CompteFactory.creerCompte(
    "COURANT", "FR001", new BigDecimal("1000"));

dao.sauvegarder(c1);

Compte c = dao.trouverParNumero("FR001");
c.debiter(new BigDecimal("200"));

Erreurs fréquentes

  1. Utiliser un pattern “par principe”
  2. Multiplier les Singletons
  3. Mettre de la logique métier dans la Factory
  4. Ne pas utiliser d’interface pour le DAO
  5. Coupler fortement métier et stockage
  6. Utiliser un DAO comme un service métier
  7. Penser que Spring remplacera tout
  8. Trop anticiper
  9. Ne pas documenter l’intention du pattern

Synthèse de la journée

Vous savez maintenant :


Préparation

La prochaine fois on abordera :