Formation Java / Spring Boot pour développeurs COBOL
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).
interfaces
Bonus - Pattern Visitor
À l’issue de cette journée, vous serez capable de :
Objectif clé : Reconnaître et les utiliser si besoin.
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.
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.
En réunion, dans un Sprint de méthodes agiles, vous entendrez souvent :
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.
new
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();
En Spring, ce pattern sera géré automatiquement (plus tard avec une annotation).
Comment créer des objets sans exposer la logique de création ?
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.
if / else
if (type.equals("COURANT")) { return new CompteCourant(...); } else if (type.equals("EPARGNE")) { return new CompteEpargne(...); }
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"); } }
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.
creerCompte
if
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 :
Compte c = CompteFactory.creerCompte(...);
Mais seulement : Compte !
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.
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);
Compte c = CompteFactory.creerCompte("COURANT", "FR001", solde);
Dans la factory : Function<ParamCompte, Compte> creator = registry.get("COURANT");, creator contient :
Function<ParamCompte, Compte> creator = registry.get("COURANT");
p -> new CompteCourant(...) puis : return creator.apply(new ParamCompte(...)); donc l’équivalent de new CompteCourant(...)
p -> new CompteCourant(...)
return creator.apply(new ParamCompte(...));
new CompteCourant(...)
Comment isoler l’accès aux données du reste du code ?
public interface CompteDAO { void sauvegarder(Compte compte); Compte trouverParNumero(String numero); }
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 :
Assembler tous les patterns vus aujourd’hui dans un mini-système cohérent.
Compte
CompteCourant
CompteEpargne
CompteFactory
CompteDAO
CompteDaoMemoire
Configuration
Main
Consignes :
new CompteCourant
main
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"));
Vous savez maintenant :
La prochaine fois on abordera :