Design patterns
Ou "patrons de conception", "modèles de conception". Façons types, éprouvées, d'organiser le code pour le rendre plus évolutif, maintenable etc. Ce sont des idées de conceptions générales.
Factory Method
Patron basé de création sur l'héritage :
-une factory (usineDAnimaux) et des factories qui héritent de son type (usine de Chien, de Chat...).
-un produit (Animal) et des produits qui en hérite (Chien, Chat).
-dans l'usine mère, des méthodes qui utilisent le produit (sans se soucier de son sous type).
Quand utiliser ?
-Quand on pense que notre type risque d'être étendu et qu'on ne veut pas avoir à tout modifier.
-Un peu pareil, quand on veut laisser les utilisateurs de notre programme étendre (typiquement un framework) ET utiliser ce qu'il nous donne. Typiquement le framework appelle la méthode presserBouton(Bouton) dans BoutonFactory et on peut lui donner un MonBoutonTresJoliBoutonFactory.
-Comme toutes les factories, quand l'objet est un peu reloud à initialiser (connexion en BDD etc) mais il faut que ça se justifie par les 2 points au dessus aussi.
Comment l'utiliser ?
-On utilise la classe parent.
class Main {
public static void main(String[] args) {
run(new UsineDeChien()); //Supppeer pratique, on a juste à remplacer par usineDeChat et tout marche encore.
}
public static void run(UsineDAnimaux usine) {
usine.petAnimalInShelter();
}
}
abstract class UsineDAnimaux {
protected abstract Animal creerAnimal(); // La factory method
public void petAnimalInShelter() { // Elle utilise des produits
(creerAnimal()).caresser();
}
}
class UsineDeChien extends UsineDAnimaux {
@Override
protected Animal creerAnimal() { return new Chien(); }
}
class Animal {
public void caresser() {
System.out.println("Grr...");
}
}
class Chien extends Animal{
public void caresser() {
System.out.println("Ouaf !");
}
}
Ne pas confondre avec :
-Simple Factory (une méthode qui génère un objet mais sans ces notions d'héritage etc).
-Abstract Factory (une interface/classe UsineDAnimaux abstraite qui contient plusieurs méthodes gens creerChien(), creerChat() puis serait implémenté par des factories UsineDAnimauxSansPoils, UsineDAnimauxMecaniques...).
Abstract Factory
Pattern de création noté comme basé sur la composition (même si l'héritage est nécessaire pour construire ce pattern).
-Une factory interface/class abstraite UsineDAnimaux qui sera implémenté par des factories concrète UsineDAnimauxSansPoils, UsineDAnimauxMecaniques...).
-Dans les factories concrètes, plusieurs méthodes gens creerChien(), creerChat().
-Les factories n'ont PAS de code métier.
Quand utiliser ?
-Quand on a des collections d'objets de la même "famille".
-Quand on veut éviter de mélanger des objets.
Gros défaut :
Compliquer d'ajouter un produit, il faut l'ajouter aprtout (oiseauMecanique, oiseauSansPoil...).
Pourquoi "composition" ?
-C'est le classement qu'on trouve souvent mais c'est un peu foireux car ça ressemble beaucoup à la Factory Method avec une usine en polymorphe avec laquelle on créé des objets polymorphe. La grosse différence avec la Factory Method c'est qu'on appelle nous même la méthode de création (fabrique.createCat()) et une méthode dans l'objet qu'on vient de fabrique, donc le client en sait un peu plus que dans la Factory Method. Souvent la classe qui utilise la fabrique va garder la fabrique en attribut donc bon voilà c'est classé comme ça.
class Main {
public static void main(String[] args) {
caresserTouteLaFamille(new FabriqueAnimauxMecaniques()); //on pourrait imaginer qu'on garde la fabrique en attribut
caresserTouteLaFamille(new FabriqueAnimauxSansPoils());
}
static void caresserTouteLaFamille(FabriqueAnimaux fabrique) { // Ici on caresse la même "famille" de chats et de chiens
fabrique.createCat().caresser(); //On appelle le code métier direct dans la classe.
fabrique.createDog().caresser();
}
}
abstract class Animal { abstract void caresser(); }
abstract class Chat extends Animal {}
abstract class Chien extends Animal {}
interface FabriqueAnimaux { Chat createCat(); Chien createDog(); }
class FabriqueAnimauxMecaniques implements FabriqueAnimaux {
public Chat createCat() { return new ChatMecanique(); }
public Chien createDog() { return new ChienMecanique(); }
}
class FabriqueAnimauxSansPoils implements FabriqueAnimaux {
public Chat createCat() { return new ChatSansPoils(); }
public Chien createDog() { return new ChienSansPoils(); }
}
class ChatMecanique extends Chat { void caresser() { System.out.println("La peau du chat est métallique."); } }
class ChienMecanique extends Chien { void caresser() { System.out.println("La peau du chien est métallique"); } }
class ChatSansPoils extends Chat { void caresser() { System.out.println("La peau du chat est sans poils"); } }
class ChienSansPoils extends Chien { void caresser() { System.out.println("La peau du chien est sans poils"); } }
Builder
Un pattern de création assez souple.
L'idée est de construire les objets étape par étape.
En Java on le voit souvent avec Lombok :
MaClasse monObjet = MaClasse.builder().sonPrenom("Bob").sonNom("Morane").build();, c'est le builder du livre Effective Java, simplifié par rapport au design pattern du Gang of Four.
On peut ajouter de l'héritage si on a des objets se construisant de la même façon.
On peut aussi ajouter un directeur qui va assembler un objet pour nous.
Quand utiliser ?
-Quand notre objet demande genre 8 param, des nulls etc. Remplaçant des named parameters un peu.
-Ca permet d'instancier un objet "final".
-Quand on a des objets qui se construisent de façon identiques (avec un "directeur"). Typiquement pour générer un csv et un json par exemple.
-Permet de faire des validations à la volée.
class BuilderPattern {
public static void main(String[] arg) {
//On peut appeler direct...
ConstructeurDeChien constructeurDeChien = new ConstructeurDeChien();
constructeurDeChien.construitNom("Fido");
Chien monBeauChien = constructeurDeChien.getResultat();
System.out.println(monBeauChien);
//...ou demander au directeur de construire pour nous.
Chien monBeauChienAvecDirecteur = (new Directeur()).construireSelonLeDirecteur(new ConstructeurDeChien());
System.out.println(monBeauChienAvecDirecteur);
}
}
class Chien { //on pourrait rendre ça impossible de créer sans builder en le passant en privé
boolean aDesPoils;
String nom;
int tailleAuGarotEnCm;
@Override
public String toString() {
return String.format("Ouaf ! Mon nom est %s", nom);
}
}
class Directeur { // Il est souple, il utilise le constructeur qu'on lui donne, donc c'est bien pour ...
public <T> T construireSelonLeDirecteur(Constructeur<T> c) { //...construire des choses de façon identiques. Générique pour tout accepter.
c.reset();
c.construitNom("Fifi"); // Le directeur appelle tout ce qu'on lui donne "Fifi"
c.construitTaille(20);
c.construitPoils();
return c.getResultat();
}
}
interface Constructeur<T> { // Pour construire, en général.
void reset();
void construitPoils();
void construitNom(String nom);
void construitTaille(int cm);
T getResultat();
}
class ConstructeurDeChien implements Constructeur<Chien> {
private Chien chienEnConstruction;
public void reset() {
chienEnConstruction = new Chien();
}
private void resetChienSiBesoin() {
if (chienEnConstruction == null) {
reset();
}
}
public void construitPoils() { // Ici on pourrait renvoyer l'instance de l'objet en cours (this) pour chaîner les appels.
resetChienSiBesoin();
chienEnConstruction.aDesPoils = true; // Ici il pourrait faire une copie et la renvoyer si l'objet doit être final.
}
public void construitNom(String nom) {
resetChienSiBesoin();
chienEnConstruction.nom = nom;
}
public void construitTaille(int cm) {
resetChienSiBesoin();
chienEnConstruction.tailleAuGarotEnCm = cm;
}
public Chien getResultat() {
resetChienSiBesoin();
Chien chienFinal = chienEnConstruction;
chienEnConstruction = null;
return chienFinal;
}
}
Prototype
Un pattern de création, pour dupliquer un objet.
C'est assez simple : au lieu de lire les champs de l'objet depuis l'extérieur (ce qui peut être impossible) pour en instancier un identique, l'objet implémente une interface Copiable par exemple, avec une méthode copie() ou clone() à implémenter. Comme un objet peut lire ses propres champs privés, il peut se cloner et renvoyer le clone.
Quand utiliser ?
-C'est pratique aussi quand on ne connaît pas le type de l'objet précisément, on a juste à savoir que c'est un Copiable.
-Quand on a besoin d'une multitude d'objets identiques, genre 20 champs pareil et un seul différent qu'on veut changer.
Attention :
-Quand vous implémenterez copie(), attention à ne pas faire de "shallow copy", par exemple un objet pointe vers une array list, et sa copie qui pointe vers la même array list au lieu de la nouvelle. Concrètement pour faire une deep copy on peut serialiser/déserialiser avec SerializationUtils.clone() mais c'est lent.
-Ne pas utiliser l'interface déjà existante Cloneable car l'interface ne force pas ses enfants à implémenter clone(). Elle est vide, mauvais design de Java pour le coup.
Alternative :
-Mettre en place un constructeur spécial ("copy constructor") qui prend l'instance à copier en entrée genre MonObjet(monObjet). Ca marchera aussi car on est aussi dans la classe et on peut donc voir ses champs privés. Par contre on doit connaître la classe concrète donc c'est moins bien.
import java.util.List;
import java.util.ArrayList;
interface Copiable<T> { T copie(); }
class Document implements Copiable<Document> {
private String titre;
private List<String> tags;
Document(String titre, List<String> tags) {
this.titre = titre;
this.tags = tags;
}
public Document copie() { return new Document(this); }
Document(Document autre) { // Là on utilise copie() ET le copy constructor
this.titre = autre.titre;
this.tags = new ArrayList<>(autre.tags); // deep copy de la liste
}
@Override
public String toString() {
return titre + tags;
}
}
class Main{
public static void main(String[] args) {
Document doc1 = new Document("Danse avec les loups", List.of("aventure"));
System.out.println(doc1);
Document doc2 = doc1.copie();
System.out.println(doc2);
}
}
Singleton
Pattern de création.
Super simple, pas de constructeur public et création via une méthode statique qui checke que l'instance n'existe pas déjà.
class Singleton {
private static Singleton instance;
private Singleton(){};
public static Singleton creerOuRecupererSingleton() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
class Main{
public static void main(String[] args) {
System.out.println(Singleton.creerOuRecupererSingleton()); //Singleton@2f92e0f4
System.out.println(Singleton.creerOuRecupererSingleton()); //aussi Singleton@2f92e0f4
}
}
Maiiiiiiiiis si 2 threads font creerOuRecupererSingleton exactement au même moment il ne sera plus singleton. On peut donc le créer directement dès le lancement du programme (dans sa propre class quand on initialise la variable ou un bloc initializer).
On peut aussi mettre greffer le Singleton à un enum car les enums sont garantis d'être Singleton en Java :
public enum MonSingleton {
INSTANCE;
public void faireUnTruc() { ... } // Utilisation : MonSingleton.INSTANCE.faireUnTruc();
}
Mais de manière général les Singletons ne sont pas trop utilisés car relouds à tester et ce sont un peu des variables globales. Dans les applis modernes on fera de l'injection de dépendance : une classe annotée avec @Bean (singleton par défaut) qui se fera @Autowired dans une autre.