- Cet article fait partie de la série Démystification du GoF : Les design patterns par la pratique en Java
- Il fait suite à celui-ci : Le pattern Singleton
Le design pattern du monteur permet de séparer la construction complexe d’un objet de sa représentation. Cela permet à un process de pouvoir créer différentes représentations de notre objet sans changer l’implémentation. Alors qu’une fabrique abstraite ne s’occuperait de renvoyer qu’un objet fraichement instancié, un monteur pourrait renvoyer cet objet également initialisé.
Le Builder a besoin d’être dirigé par un objet externe, appelé Director, qui fera le lien entre le client et notre patron de conception.
Use case
Au lieu d’ouvrir les fichiers au cas par cas dans leur logiciel respectif (Excel pour un fichier xls par exemple), notre chef voudrait que nous implémentions une interface graphique (minimaliste) pour afficher le fichier sélectionné. Ce fichier pourrait avoir des commandes personnalisées selon le type de fichier choisi.
Afin de réaliser cette étape, nous allons nous baser sur le patron de l’Abstract Factory que nous avons modifié.
Représentation UML
Cette représentation UML de notre Use case peut être décomposée en 3 parties :
- La classe Manager qui s’occupera de créer le builder puis récupèrera la fenêtre Window construite par celui-ci.
- Le Builder qui est composé d’une classe abstraite ainsi que de deux ConcreteBuilder.
- La modélisation du builder s’apparente très fortement à une AbstractFactory car il travaillera sur les fichiers CSV et Excel.
- L’AbstractFactory qui a déjà été détaillée dans un article précédent.
On peut remarquer que les classes CSVBuilder et ExcelBuilder possèdent un attribut privé : window de type Window.
Ce type se chargera de créer une interface graphique qui embarquera les fichiers Document et Logs.
Le dernier appel au Builder sera pour utiliser la méthode : getWindow(). Elle renverra l’objet window fraichement créé ET complet.
Représentation JAVA
Afin de représenter ce pattern, nous partirons du fait que nos fichiers d’interface graphique sont déjà créés et seront représentés par leur type abstrait : l’interface Window.
public interface Window {
public void setFile(Document doc);
public void setLog(Log log);
/*Others useful methods !!!*/
}
Nous allons maintenant implémenter nos deux builders : CSVBuilder et ExcelBuilder
public class CSVBuilder {
private Window window;
public Window getWindow() {
return window;
}
public void createNewWindow() {
this.window = new WindowClass();
}
public void buildFile(String name) {
/*can be an attribute*/
/* Using factory with Singleton*/
AbstractFactory factory = CSVFactory.INSTANCE;
this.window.setFile(factory.makeDocument(name));
}
public void buildLog(String name) {
/* Using factory with Singleton*/
AbstractFactory factory = CSVFactory.INSTANCE;
this.window.setLog(factory.makeLog(name));
}
}
public class ExcelBuilder {
private Window window;
public Window getWindow() {
return window;
}
public void createNewWindow() {
this.window = new WindowClass();
}
public void buildFile(String name) {
/*can be an attribute*/
/* Using factory with Singleton*/
AbstractFactory factory = ExcelFactory.INSTANCE;
this.window.setFile(factory.makeDocument(name));
}
public void buildLog(String name) {
/* Using factory with Singleton*/
AbstractFactory factory = ExcelFactory.INSTANCE;
this.window.setLog(factory.makeLog(name));
}
}
Le pattern Singleton a été appliqué sur les deux fabriques pour ne travailler qu’avec une seule instance (Voir Abstract Factory pattern).
Le Builder va se concentrer principalement sur :
- La création de la fenêtre Window.
- L’ajout d’un fichier Document et de son Log associé dans la fenêtre ;
- Le renvoi de l’objet Window au Manager.
Etant donné que nous avons deux objets de type Builder, nous allons créer une interface AbstractBuilder.
public interface Builder {
public Window getWindow();
public void createNewWindow();
public void buildFile(String name);
public void buildLog(String name);
}
Cette Interface sera héritée par les 2 autre builders.
L’ensemble de nos patterns définis ici est fonctionnel. Il ne manque plus que la classe Manager pour utiliser le Builder permettant de construire l’objet Window :
public class Manager {
private Builder builder;
public Window openAFile(String name) {
/*Getting the type of the file*/
String type = FilenameUtils.getExtension(name);
/*Builder creation*/
/* Java 7 only*/
switch (type) {
case "xls" : builder = new ExcelBuilder();
break;
case "csv" : builder = new CSVBuilder();
break;
default: return null;
}
/* Feeding the Window object*/
builder.createNewWindow();
builder.buildFile(name);
builder.buildLog(name);
/*Object window created*/
return builder.getWindow();
}
}
Si vous regardez les dernières lignes du Manager, il fait appel au builder pour :
- Créer un objet privé window dans le builder
- Rajouter le fichier Document à window
- Rajouter le fichier Log à window
- Renvoyer la fenêtre au client.
L’utilisation actuelle du builder permet un découpage assez complet de notre application.
Utilisation d’un builder statique
Selon Joshua Bloch dans son livre : « Effective Java, 2nd Edition » :
The builder pattern is a good choice when designing classes whose constructors or static factories would have more than a handful of parameters.
Pour cet exemple, l’interface Window a été redéfinie :
public interface Window {
/*Print the final screen to the user*/
public void printScreen();
/*useful methods !!!*/
}
Prenons un exemple avec la classe WindowClass enrichie de nouveaux attributs.
public class WindowClass implements Window {
private final Log log; Document doc;
private final ScrollBar scroll;
private final String nameWindow;
private final int sizeX, sizeY;
/*Mandatory parameter*/
public WindowClass(Log log, Document doc) {
this(log, doc, "default");
}
public WindowClass(Log log, Document doc, String nameWindow) {
this(log, doc, nameWindow, 800, 600);
}
public WindowClass(Log log, Document doc, String nameWindow, int x, int y) {
this(log, doc, nameWindow, 800, 600, null);
}
/*fully initialize*/
public WindowClass (Log log, Document doc, String nameWindow, int sizeX,
int sizeY, ScrollBar scroll) {
this.log = log;
this.doc = doc;
this.nameWindow = nameWindow;
this.scroll = scroll;
this.sizeX = sizeX;
this.sizeY = sizeY;
}
public void printScreen() {System.out.println("This is the screen !");}
}
Cette classe possède de nombreux attributs pouvant être initialisés à la création de l’objet.
L’inconvénient majeur de cette implémentation est que l’on doit déclarer un constructeur différent selon le nombre de paramètres à initialiser. Et encore il faut le faire dans l’ordre sinon le nombre de
Une solution : ajoutons un builder !
Le principe serait :
- Ne déclarer qu’un constructeur dans WindowClass en privé contenant l’ensemble des attributs en paramètre.
- Créer une classe interne builder de type statique avec des attributs identiques à la classe parente (Attributs privés).
- Cette classe Builder va se charger de construire un objet WindowClass selon cet ordre :
- Création du constructeur builder avec les attributs obligatoires
- Utiliser des setters pour initialiser chaque attribut optionnel
- Appeler la méthode createWindow() qui renverra une instance de WindowClass avec l’ensemble des paramètres initialisés.
public class WindowClass2 implements Window {
private final Log log; Document doc;
private final ScrollBar scroll;
private final String nameWindow;
private final int sizeX, sizeY;
/*fully initialize*/
private WindowClass2(Log log, Document doc, String nameWindow, int sizeX,
int sizeY, ScrollBar scroll) {
this.log = log;
this.doc = doc;
this.nameWindow = nameWindow;
this.scroll = scroll;
this.sizeX = sizeX;
this.sizeY = sizeY;
}
/*Builder class*/
public static class WindowBuilder {
private final Log log; Document doc;
private ScrollBar scroll;
private String nameWindow;
private int sizeX, sizeY;
/*constructor*/
public WindowBuilder(Document doc, Log log) {
this.log = log;
this.doc = doc;
}
/*setters*/
public WindowBuilder scroll(ScrollBar scroll) {
this.scroll = scroll;
return this;
}
public WindowBuilder nameWindow(String name) {
this.nameWindow = name;
return this;
}
public WindowBuilder sizeX(int x) {
this.sizeX = x;
return this;
}
public WindowBuilder sizeY(int y) {
this.sizeY = y;
return this;
}
public WindowClass2 constructWindow() {
return new WindowClass2(log, doc, nameWindow, sizeX, sizeY, scroll);
}
}
public void printScreen() {System.out.println("This is the screen !");}
}
Le builder est 100% fonctionnel !
Nous avons rendu notre classe immuable grâce notamment à nos attributs de type Final.
Pourquoi avoir renvoyé un objet WindowBuilder pour les setters ? Je pense que la réponse viendra dans la simplicité d’utilisation de celui-ci dans la classe Main.
public class Main {
public static void main(String[] args) {
AbstractFactory factory = ExcelFactory.INSTANCE;
WindowClass2 window =
new WindowClass2
.WindowBuilder(factory.makeDocument("Otto.xsl"),
factory.makeLog("Otto.log"))
.nameWindow("Default")
.scroll(null)
.sizeX(800)
.sizeY(600)
.constructWindow();
}
}
Le Builder peut être chainé afin d’en simplifier son utilisation.
Enfin, la méhode constructWindow() va appeler le constructeur privé de la classe WindowClass afin de créer l’objet selon le Template du builder.
Il est bien sûr possible d’ajouter des contrôles au niveau de la méthode constructWindow() pouvant par exemple lancer une exception.
Conclusion
Le pattern Builder est un patron permettant de construire un objet selon les directions d’un Client (appliqué par le Director, classe Manager dans cet exemple). Contrairement à la Factory, il ne se contente pas forcément de créer l’objet puis de le renvoyer mais vraiment de le construire ainsi que ses attributs.
- Le client demande au Director de construire l’objet
- Le Director utilise le monteur pour construire l’objet (dirigé par le Director).
- Le client demande au Director le résultat (constructWindow()) et celui-ci lui renvoie l’objet construit.