Cet article fait suite à celui-ci : Démystification du GoF : Les design patterns par la pratique en Java
La Factory Method représente l’art de déléguer l’instanciation à une méthode distincte. Cela nous permet de séparer la création des objets de leur utilisation. Ce pattern peut être utilisé lorsqu’on ne connaît pas la classe qui sera utilisée à la compilation. Afin de pouvoir créer une multitude d’objets différents (appartenant à une même famille), il faudra utiliser l’héritage en surchargeant notre Factory (ci-dessous).
Use case
Ne connaissant pas l’ensemble des fichiers à valider, nous allons créer une architecture suffisamment souple afin d’ajouter différents types de fichiers au fil de l’eau.
Nous commençons avec le format CSV, qui pourrait être le type de fichier le plus courant.
Représentation UML
Cette représentation UML est une des multiples approches que propose ce pattern. Nous avons décidé d’utiliser une classe abstraite pour notre Factory, mais cela pourrait tout aussi bien être notre main(). De plus, la classe CSV Factory pourrait être utilisée avec une interface qui permettrait de camoufler les fabriques concrètes de la Factory abstraite (représentée ci-dessous).
La classe CSV Factory va, quant à elle, se charger d’instancier notre objet CSVFile et renvoyer à la méthode processAFile() un objet de type Document.
Le principe consiste surtout à séparer l’instanciation de l’implémentation de la solution.
Représentation JAVA
Pour commencer, nous allons représenter notre interface Document.
public interface Document {
public void close();
public void validate();
public void send();
public List<String> readLine();
}
La classe CSVFile permet ainsi de gérer nos fichiers au format CSV.
public final class CSVFile implements Document {
private boolean validate = false;
private String name;
List<String> lines;
public CSVFile(String name) {
this.name = name;
lines = new ArrayList<>(); //java7
}
public void close() {
System.out.println("CSVFile "+name+" close !");
}
public void validate() {
this.validate = !this.validate;
System.out.println("CSVFile "+name+" validate : "
+ Boolean.toString(this.validate));
}
public void send() {
if (this.validate)
System.out.println("CSVFile "+name+" sent !");
else
System.out.println("CSVFile "+name+" need"+
" to be validated before sent !");
}
public List<String> readLine() {
lines.add("Line "+(lines.size()+1));
return lines;
}
}
Bon ok on triche mais ce n’est pas le plus important 🙂
À ce moment précis, nous avons implémenté notre document CSV et notre interface pour l’exposer. Désormais, il nous reste seulement la création de notre Factory, afin de récupérer nos fichiers CSV.
Dans notre modélisation, le pattern Factory se décompose en deux parties :
- Une classe abstraite possédant une méthode abstraite makeDocument()
- Une classe concrète contenant l’implémentation de notre fichier CSV et renvoyant l’interface exposée : Document.
public abstract class Factory {
public void processAFile(String name) {
Document doc = makeDocument(name);
doc.readLine();
doc.readLine();
List<String> l = doc.readLine();
System.out.println("file : " + l);
doc.validate();
doc.send();
doc.close();
}
public abstract Document makeDocument(String name);
}
L’utilisateur appellera la méthode processAFile(String name) avec un nom de fichier en paramètre. Un appel à la Factory Method via la méthode makeDocument() nous permettra de récupérer un objet de type Document.
public class CSVFactory implements Factory {
public Document makeDocument(String name) {
return new CSVFile(name);
}
}
Notre Factory est minimaliste mais néanmoins fonctionnelle !
public class Main {
public static void main(String[] args) {
Factory f = new CSVFactory();
f.processAFile("OttO.csv");
}
}
Output :
Lines : [Line 1, Line 2, Line 3, Line 4]
CSVFile Otto.csv validate : true
CSVFile Otto.csv sent !
CSVFile Otto.csv close !
OK c’est peut-être un peu « Too much » pour ne traiter QUE les fichiers CSV… Mais c’est suffisamment souple pour rajouter un nouveau type de fichier, sans devoir refactorer notre code !
Par exemple, pour ajouter la gestion du type Excel, il nous suffirait de créer une classe ExcelFile, implémentant Document, de créer sa Factory associée et c’est fini !
public class ExcelFactory implements Factory {
public Document makeDocument(String name) {
return new ExcelFile(name);
}
}
ExcelFactory implémentant un ExcelFile.
public class ExcelMain {
public static void main(String[] args) {
Factory f1 = new CSVFactory();
Factory f2 = new ExcelFactory();
f1.processAFile("OttO.csv");
f2.processAFile("data.xls");
}
}
Output :
file : [Line 1, Line 2, Line 3]
CSVFile OttO.csv validate : true
CSVFile OttO.csv sent !
CSVFile OttO.csv close !
file : [Line 1, Line 2, Line 3]
ExcelFile data.xls validate : true
ExcelFile data.xls sent !
ExcelFile data.xls close !
On ne touche pas l’existant !
Quelques variantes de la Factory
L’implémentation de notre Factory est naïve et requiert pour chaque nouveau type d’objet, une ConcreteFactory (ExcelFactory, CSVFactory, …), adaptée au type de fichier que nous souhaitons obtenir.
Pour cela, il pourrait y avoir la solution suivante: créer une seule ConcreteFactory (on oublie la classe abstraite), en passant le type de manière implicite (paramètre ou récupération dans le fichier).
Switch case
Exemple avec un simple Switch case d’une Factory pour illustrer notre propos :
public class FactoryIf extends Factory {
public Document getCSVFile(String name) {
return new CSVFile(name);
}
public Document getExcelFile(String name) {
return new ExcelFile(name);
}
public Document makeDocument(String name) {
/*Using Apache Commons IO*/
String type = FilenameUtils.getExtension(name);
if (type != null || !"".equals(type))
switch(type) {
case "csv" : return getCSVFile(name);
case "xls" : return getExcelFile(name);
default:
}
System.out.println("Type is null or invalid");
return null;
}
}
Ci-dessus, nous avons réussi à déléguer la gestion de l’objet souhaité, dans notre Factory. Mais au prix d’un Switch/Case … Pas très performant, surtout si nous devons rajouter un nouveau type (ou une centaine), nous devrons alourdir notre switch case…
Un peu de Réflexion s’impose !
Pour résumer :
- Créer une ConcreteFactory pour chaque nouveau cas est pratique, mais cela nous impose de créer une ConcreteFactory par type de fichier souhaité.
- Ne faire qu’une seule classe de Factory et gérer l’instanciation par condition (switch, if ou autre) revient à compacter son code. Mais en contrepartie, on serait complètement dépendant de notre condition. Et si nous devons gérer une centaine de types de fichiers … ce n’est peut-être pas le format le plus adapté.
Dans nos deux précédents cas, une évidence s’impose : on veut la flexibilité du premier, tout en gérant, de manière implicite, l’instanciation comme dans le second.
Utilisons la Réflexion !
Pour cela, nous allons créer une nouvelle classe Factory : FactoryReflexion
public class FactoryReflexion {
/*
* the second parameter will use Reflexion
* to instantiate automatically our cons.
*/
public Document makeDocument(String name,String classe) throws
/*Exception raise by forName method*/
ClassNotFoundException,
/*Exception raise by getConstructor method*/
NoSuchMethodException,
/*Exception raise by newInstance method*/
IllegalAccessException,
InstantiationException,
InvocationTargetException
{
/* getting the class with forName()
* Be careful to control that class retrieved is a class
* xxxFile and not another one !
*/
Class c = Class.forName(classe);
/*Getting the constructor with one parameter of type String*/
Constructor cons = c.getConstructor(String.class);
/*Getting a new instance of our class*/
return ((Document) cons.newInstance(name));
}
}
public class MainReflexion {
public static void main(String[] args)
throws ClassNotFoundException, NoSuchMethodException,
InvocationTargetException, InstantiationException,
IllegalAccessException {
FactoryReflexion ref = new FactoryReflexion();
Document xls = ref.makeDocument("pnl", "ExcelFile");
xls.validate();
xls.send();
xls.close();
Document csv = ref.makeDocument("data", CSVFile.class.getName());
csv.validate();
csv.send();
csv.close();
}
}
Output :
ExcelFile pnl validate : true
ExcelFile pnl sent !
ExcelFile pnl close !
CSVFile data validate : true
CSVFile data sent !
CSVFile data close !
Ceci est une instanciation naïve de la réflexion (nous renvoyons toutes les exceptions au développeur 🙂 ) mais le concept est le suivant :
- Nous récupérons la classe en paramètre dans notre ClassLoader et nous l’instancions.
- Les constructeurs des classes CSVFile, ExcelFile (etc.) prennent un seul paramètre : Name (type String). Nous cherchons donc le constructeur avec un paramètre de type String.
- Une fois ce dernier récupéré, nous essayons de l’instancier avec la méthode newInstance ().
- Si cela fonctionne, on nous renverra une instance d’une classe xxxFile de type Object.
- Un simple cast nous permettra la récupération d’un objet Document.
Ainsi, cette méthode nous permet de maîtriser, de manière totalement transparente, toutes les évolutions futures telles que la gestion des fichiers Word, en ne créant qu’une seule classe WordFile.
CEPENDANT, ATTENTION À LA GESTION D’ERREUR !
En utilisant la réflexion, les erreurs peuvent survenir au niveau du Runtime, alors qu’avant nous étions en compilation. Et il n’y a rien de plus agaçant que de voir son application planter devant l’utilisateur… De plus, la réflexion entraînera une grande complexité, pouvant alors engranger les situations suivantes :
- Des chutes de performances ;
- Des limites de restriction (requièrent une runtime permission) ;
- Une exposition dangereuse de l’encapsulation.
Conclusion
Le pattern de Factory Method est vraiment efficace afin de déporter l’allocation de notre code. Cela permet de découper plus facilement notre application.
Comme nous l’avons constaté précédemment, l’évolution de ce pattern est très simple. Ainsi, une nouvelle classe héritant de notre Factory suffit à implémenter un nouveau type de fichier.
La suite dans un prochain article.