Les design patterns (on dit aussi patrons en français) sont des solutions générales, flexibles, éprouvées et bien connues pour résoudre des problèmes récurrents en programmation. Ils sont indépendants du langage de programmation objet que vous utilisez. C’est pourquoi ils sont assez incontournables.
Le « Gang of Four » (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) a formalisé 23 patrons dans un livre best-seller :
On peut séparer ces design patterns en plusieurs catégories :
dont le but est de permettre de construire des objets complexes répondant à certaines spécifications ou contraintes.
dont le but est de structurer des objets complexes.
dont le but est d’étendre le comportement, les fonctionnalités, d’objets ou, tout simplement, de rendre certaines fonctionnalités possibles.
Une référence utile avec plein d'exemples et d'explications : Refactoring Guru.
Dans cette séance, on va s’intéresser à deux design patterns permettant de construire de manière efficace et flexible des objets :
Reprenons l’exemple de la construction des Magiciens, Chevaliers et Voleurs du jeu de rôle. On avait vu que leurs créations par ÉtatJeu posait des problèmes car une version « naïve » violait le principe Ouvert/Fermé de la classe ÉtatJeu. On avait trouvé une parade en exploitant le type générique Class de Java. Mais cette solution est un peu restrictive. En effet, elle se complexifie si les classes que l’on souhaite créer sont génériques (Magicien<T>). De plus, elle fonctionne bien sous Java car, dans ce langage, les noms des classes manipulés par la machine virtuelle sont les mêmes que ceux que vous avez donné à vos classes (Chevalier = Chevalier). En C++, c’est plus complexe : le RTTI (run-time type information) utilise des chaînes de caractères moins lisibles. Par exemple, le code suivant :
#include <iostream>
template <typename C>
struct Toto {
C x;
};
int main () {
std::cout << typeid(double).name() << std::endl;
std::cout << typeid(std::string).name() << std::endl;
std::cout << typeid(Toto<int>).name() << std::endl;
return 0;
}
pourra vous produire un affichage similaire à celui ci-dessous (cf. g++) :
d
NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
4TotoIiE
Et, comme souvent en C/C++, cet affichage est « implementation dependent ». Autrement dit, il varie d’un compilateur à l’autre. On aimerait donc trouver un moyen de s’en sortir tout en faisant en sorte que notre application reste maintenable (il est hors de question que la classe ÉtatJeu fasse des if/else pour déterminer quel constructeur appeler).
Définition
La problématique générale que le design pattern Factory essaie de résoudre est l’instanciation par une classe C d’objets dont les types ne sont pas connus à l’avance par C, sans que la classe C ne viole le principe Ouvert/Fermé.
Il existe de nombreux exemples de cette problématique. Par exemple, si l’on doit charger l’image des personnages, on peut avoir des fichiers jpg, png, gif, svg, etc., et on n’a pas envie que chaque type de personnage fasse des if/else pour sélectionner le constructeur adapté au type de l’image à charger. Si le jeu utilise un tableau pour sauvegarder des statistiques de performance des joueurs, il pourrait utiliser des fichiers xls, xlsx, csv, etc. On voit donc que c’est une problématique assez commune.
La solution apportée par le design pattern Factory peut être décrit grâce au diagramme UML suivant :
Au lieu que ce soit ÉtatJeu qui crée lui-même les personnages en utilisant des if/else, il délègue à JoueurFactory le soin de le faire : ÉtatJeu appelle la méthode creeNouveauPerso de JoueurFactory qui, elle, réalise les if/else et les new. Le code de JoueurFactory peut alors être :
package fr.polytech.aco.jeu;
public class JoueurFactory {
public JoueurFactory() {}
public Joueur creeNouveauPerso(String type, String nom, int x, int y)
throws NotImplementedException {
switch(type) {
case "Magicien":
return new Magicien(nom, x, y);
case "Chevalier":
return new Chevalier(nom, x, y);
case "Voleur":
return new Voleur(nom, x, y);
default:
throw new NotImplementedException("type Joueur " + type + " not implemented yet");
}
}
}
package fr.polytech.aco.jeu;
public class Main {
public static void main(String[] args) {
try {
var joueurFactory = new JoueurFactory();
Joueur perso1 = joueurFactory.creeNouveauPerso("Magicien", "toto", 10, 100);
Joueur perso2 = joueurFactory.creeNouveauPerso("Chevalier", "titi", 20, 50);
System.out.println(perso2);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
Note
On voit que JoueurFactory viole le principe Ouvert/Fermé mais cela n’empêchera pas l’application d’être maintenable car ces violations seront limitées à quelques Factories bien identifiées plutôt que d’être disséminées dans tout le code de l’application.
Notez ici que, dans le switch, on lève une exception si l’on essaye de créer un type de personnage dont on n’a pas encore tenu compte dans la Factory. C’est donc à runtime plutôt qu’à compile time que l’on s’apercevra que l’on a oublié de mettre à jour la Factory quand on a rajouté le nouveau type de personnage. C’est moins bien qu’une erreur de compilation mais c’est plus robuste que de renvoyer un null.
Note
On peut « robustifier » un peu plus notre schéma :
en créant un package qui contient les classes Joueur, Magicien, Chevalier, Voleur et JoueurFactory, et uniquement ces classes, et
en faisant en sorte que les constructeurs des différentes classes de personnage soient package-private et celui de JoueurFactory soit public.
Dans ce cas, pour ÉtatJeu, le seul moyen de créer des personnages est d’utiliser JoueurFactory.
Avantages :
On désolidarise complètement le processus de création des classes qui utilisent ces créations.
On respecte le principe de responsabilité unique puisque l’unique rôle de la Factory est de créer des instances.
ÉtatJeu respecte le principe Ouvert/Fermé : de nouveaux types de construction des personnages n’impliquent aucune modification dans le code d’ÉtatJeu.
Inconvénients :
Le dernier diagramme UML est plus complexe que les précédents. Il ne faut donc pas trop abuser des Factories.

Utilisez le patron Factory pour créer les différents types de moteurs de l’USS Orville.
Les Coffres de notre jeu de rôle peuvent contenir :
des armures qui augmentent les points de défense du joueur,
des potions magiques qui augmentent l’intelligence du joueur,
des potions nutritives qui augmentent l’endurance du joueur,
des bottes qui permettent d’augmenter l’agilité du joueur,
des armes qui augmentent la force du joueur,
des masques de beauté qui augmentent le charisme du joueur.
Un Coffre peut contenir zéro ou un objet de chacune des catégories ci-dessus. Autrement dit, il y a \(2^6 = 64\) combinaisons possibles, donc 64 constructeurs de Coffre possibles. Par exemple :
Coffre(Armure arm, Botte botte); // contient une armure et des bottes
Coffre(PotionMagique pot, Arme arm); // contient une potion magique et une arme
Coffre(Armure arm); // contient juste une armure
On se doute bien que l’on n’a pas envie d’écrire 64 constructeurs. On ne va donc en construire qu’un seul, qui contient tous les types de contenus possibles :
Coffre(Armure armure,
PotionMagique potMagique,
PotionNutritive potNutritive,
Botte botte,
Arme arme,
MasqueBeauté masque);
et on passera des null pour les objets que le Coffre ne contient pas. Mais cela résulte en un code un peu « lourd » à utiliser. Par exemple, pour simuler des appels aux 3 constructeurs indiqués plus hauts, on aurait les appels suivants :
Coffre(armure, null, null, botte, null, null);
Coffre(null, potion, null, null, arme, null);
Coffre(armure, null, null, null, null, null);
Bref, on est amené à passer beaucoup de null en arguments. L’objectif de Builder est de pallier cela.
Définition
L’objectif du design pattern Builder est de construire des instances d’objets complexes en les initialisant étape par étape. Cela permet, entre autres, d’éviter d’avoir un seul constructeur avec plein de paramètres optionnels.
Il existe plusieurs variantes du Builder, mais une idée clef consiste :
à encapsuler une classe statique Builder à l’intérieur de la classe que l’on souhaite construire, le Builder contenant les mêmes attributs que la classe à construire,
à passer en paramètre du constructeur de la classe à construire une instance du Builder,
à créer dans le Builder des méthodes permettant d’initialiser les attributs et les objets imbriqués, ces méthodes retournant le Builder,
à fournir dans le Builder une méthode build() qui renvoie l’instance que l’on souhaitait construire.
Cela amène à un diagramme UML similaire à :
Voici un code Java qui illustre l’idée : le seul constructeur de Coffre que l’on propose a un Builder en argument. Ce Builder contient les mêmes attributs que le Coffre. La classe Builder est encapsulée à l’intérieur de la classe Coffre afin que ce soit plus simple de la retrouver quand on fait des mises à jour de l’application. Le main() ci-dessous illustre bien l’idée : on fait un new Coffre.Builder() afin de créer le Builder. Le new retournant un Builder, on peut s’en servir pour appeler une des méthodes set. Comme celles-ci retournent aussi des Builders, on peut enchaîner les set. Enfin, quand on a affecté tout le contenu du coffre, on appelle la méthode build() qui crée un vrai Coffre en lui passant en paramètre le Builder que l’on a créé précédemment. Cela donne un code assez esthétique (1 seul constructeur de Coffre au lieu de 64, pas une foultitude de null à passer en arguments).
public class Coffre {
private Armure armure;
private PotionMagique potionMagique;
private PotionNutritive potionNutritive;
private Botte botte;
private Arme arme;
private MasqueBeaute masqueBeaute;
Coffre(Builder builder) {
this.armure = builder.armure;
this.potionMagique = builder.potionMagique;
this.potionNutritive = builder.potionNutritive;
this.botte = builder.botte;
this.arme = builder.arme;
this.masqueBeaute = builder.masqueBeaute;
}
public String toString() {
String res = "{";
if (this.armure != null) { res += "armure, "; }
if (this.potionMagique != null) { res += "potionMagique, "; }
if (this.potionNutritive != null) { res += "potionNutritive, "; }
if (this.botte != null) { res += "botte, "; }
if (this.arme != null) { res += "arme, "; }
if (this.masqueBeaute != null) { res += "masque, "; }
if (res.length() > 2 ) res = res.substring(0, res.length() - 2);
res = res + "}";
return res;
}
// ===================================================================
// le builder qui va permettre d'instancier correctement les attributs
// ===================================================================
public static class Builder {
private Armure armure;
private PotionMagique potionMagique;
private PotionNutritive potionNutritive;
private Botte botte;
private Arme arme;
private MasqueBeaute masqueBeaute;
public Builder() {}
public Builder setArmure(Armure armure) {
this.armure = armure;
return this;
}
public Builder setPotionMagique(PotionMagique potionMagique) {
this.potionMagique = potionMagique;
return this;
}
public Builder setPotionNutritive(PotionNutritive potionNutritive) {
this.potionNutritive = potionNutritive;
return this;
}
public Builder setBotte(Botte botte) {
this.botte = botte;
return this;
}
public Builder setArme(Arme arme) {
this.arme = arme;
return this;
}
public Builder setMasqueBeaute(MasqueBeaute masqueBeaute) {
this.masqueBeaute = masqueBeaute;
return this;
}
public Coffre build() {
return new Coffre(this);
}
}
// ===================================================================
// utilisation du Builder
// ===================================================================
public static void main(String[] args) {
Coffre coffre = new Coffre.Builder()
.setArmure(new Armure())
.setBotte(new Botte())
.build();
System.out.println(coffre);
}
}
Avantages :
On évite les constructeurs à rallonge avec plein de paramètres optionnels.
On peut construire des objets en plusieurs étapes (certaines peuvent même dépendre d’étapes précédentes).
On peut assurer qu’une fois l’instance créée, on ne peut plus la modifier (pas de setter accessible).
Le Builder assure le principe de Responsabilité Unique. En effet, la construction est déléguée au Builder, qui n’assure que cette tâche, et ce qui ne relève que du fonctionnement du Coffre (business logic) est localisé dans la classe Coffre.
Inconvénients :
Si l’on encapsule le Builder dans la classe qu’il construit, on obtient des fichiers assez volumineux.
Si l’on encapsule le Builder dans la classe qu’il construit, on doit dupliquer les attributs de cette classe dans le Builder. On a donc un couplage très fort entre ces deux classes.
Utilisez le patron Builder pour créer le système de propulsion d’un navire de la flotte pouvant contenir des moteurs à impulsion électromagnétique, des moteurs auxiliaires et/ou un hyperdrive.