I. Introduction

Depuis plusieurs semaines voire plusieurs mois, ma mission actuelle m'amène à enrichir les tests unitaires de l'application JEE sur laquelle je travaille, et ce pour diverses raisons que je n'étalerai pas ici. Bien évidemment, je n'ai pas toujours la chance de travailler sur du code neuf ; je dois donc faire avec l'existant, sous toutes ses formes, qu'elles soient bonnes ou mauvaises.

Dès lors, je me suis aperçu que je croisais quasi quotidiennement un antipattern répondant au doux nom de  « Telescoping Constructor » . A travers cet article, je me propose donc de vous l'expliquer par l'exemple puis de dresser les différentes alternatives pour y remédier. Enfin, et comme il est toujours bon d'aller un peu plus loin que l'objectif initial, je prendrai le temps de vous présenter les  Fluent Interfaces qui peuvent être abordées à partir de notre problématique initiale.

Comme tous les articles de mon blog, les exemples sont disponibles sur Image non disponible GitHub . Enfin, ces derniers sont écrits en  Java  mais peuvent s'appliquer à n'importe quel langage orienté objet.

II. L'antipattern « Telescoping Constructor »

II-A. De la nécessité de construire…

Un langage orienté objet fait la part belle aux objets, comme son nom l'indique (si si, je vous assure !). Prenons par exemple la classe suivante :

Une personne simple
Sélectionnez
  1. public class Person { 
  2.  
  3.     private final int id; 
  4.  
  5.     private boolean married; 
  6.     private int age; 
  7.     private String firstname; 
  8.     private String lastname; 
  9.  
  10. } 

J'ai modélisé très simplement une personne via un identifiant technique, ses nom et prénom, son âge et par un booléen indiquant si elle est mariée ou non. Le mot clé final  indique que l'identifiant technique est obligatoire ; je suis obligé de le fournir à l'instanciation de cette classe.

C'est une représentation volontairement simpliste mais pas forcément incongrue selon le contexte. Disons que je n'ai besoin que de cela pour l'instant. La création des objets se fera alors via un constructeur de ce type :

Un constructeur de personnes simples
Sélectionnez
  1. public Person(boolean married, int age, int id, String firstname, String lastname) { 
  2.     this.married = married; 
  3.     this.age = age; 
  4.     this.id = id; 
  5.     this.firstname = firstname; 
  6.     this.lastname = lastname; 
  7. } 

Les EDIEnvironnements de Développement Intégrés sont d'ailleurs largement utilisés pour générer de tels constructeurs. A plusieurs endroits dans mon code, je verrai donc dorénavant ce type d'appels :

Construire une personne simple
Sélectionnez
  1. final Person pierre = new Person(false, 15, 1, "Pierre", "Duchêne"); 

Ici, mon appel reste relativement lisible car je n'ai que cinq paramètres ; il m'est donc facile de savoir à quoi ils correspondent. Dans une application  JEE , ce type de code est plutôt rare car la construction des entités se fait généralement dans la couche d'accès aux données. C'est par exemple l' ORM qui s'en charge. En revanche, il n'est pas rare d'en trouver dans les tests unitaires, où l'on a besoin de vérifier le comportement de notre application selon les données contenues par l'instance.

II-B. … aux constructions incertaines

Maintenant, imaginons que l'application adresse de nouveaux besoins et qu'il faille ajouter de nouveaux champs à notre classe :

Une personne complexe
Sélectionnez
  1. public class Person { 
  2.  
  3.     private final int id; 
  4.  
  5.     private boolean married; 
  6.     private boolean single; 
  7.     private boolean widowed; 
  8.     private int age; 
  9.     private int children; 
  10.     private int femaleChildren; 
  11.     private int grandchildren; 
  12.     private int maleChildren; 
  13.     private String firstname; 
  14.     private String lastname; 
  15.     private String nickname; 
  16. } 

En conséquence, j'ajoute un constructeur pour pouvoir instancier ces nouveaux champs :

Un constructeur de personnes complexes
Sélectionnez
  1. public Person(boolean married, boolean single, boolean widowed, int age, int children, int femaleChildren, int grandchildren, int id, int maleChildren, String firstname, String lastname, String nickname) { 
  2.     this.married = married; 
  3.     this.single = single; 
  4.     this.widowed = widowed; 
  5.     this.age = age; 
  6.     this.children = children; 
  7.     this.femaleChildren = femaleChildren; 
  8.     this.grandchildren = grandchildren; 
  9.     this.id = id; 
  10.     this.maleChildren = maleChildren; 
  11.     this.firstname = firstname; 
  12.     this.lastname = lastname; 
  13.     this.nickname = nickname; 
  14. } 

Et là, mes appels aux constructeurs commencent à devenir difficiles à maintenir :

Construire une personne complexe
Sélectionnez
  1. final Person harry = new Person(true, false, false, 46, 2, 1, 1, 2, 1, "Harry", "Potter", "HP"); 

Comme vu précédemment, ce type de code se retrouve très souvent dans les tests unitaires et possède quatre inconvénients majeurs, en termes de :

  • lisibilité :  il est impossible de savoir ce que signifie la valeur d'un paramètre ; dans l'exemple ci-dessus, on confond et l'on inverse facilement le nombre d'enfants et de petits-enfants ;
  • robustesse :  si je change l'ordre des paramètres d'un constructeur, j'affecte tous les appels à ce dernier ;
  • usage :
    • il est difficile de se rappeler de l'ordre des paramètres du constructeur,
    • il est difficile de savoir quel constructeur utiliser selon les variations souhaitées.

Cette situation correspond à l'antipattern  « Telescoping Constructor ».  Voyons maintenant quelles sont les alternatives pour y remédier.

Dans l'exemple ci-dessus, j'ai volontairement simplifié les choses. Dans la vie réelle, j'ai croisé des constructeurs avec pas loin du triple de paramètres, au bas mot…

III. Quelles alternatives ?

III-A. Java Beans

La première alternative qui s'offre à nous consiste à réduire le nombre de paramètres passés au constructeur. On ne fournit donc que les valeurs obligatoires lors de l'instanciation et l'on fait ensuite usage des accesseurs (ou  setters ) pour affecter les autres valeurs, d'où le lien avec la notion de Java Bean . Le constructeur s'en trouve simplifié :

Un constructeur réduit aux membres obligatoires
Sélectionnez
  1. public Person(final int id) { 
  2.     this.id = id; 
  3. } 

Et l'instanciation d'une personne prend cette forme :

Construire une personne à la mode « Java Beans »
Sélectionnez
  1. final Person paul = new Person(3); 
  2. paul.setAge(23); 
  3. paul.setChildren(0); 
  4. paul.setFemaleChildren(0); 
  5. paul.setFirstname("Paul"); 
  6. paul.setGrandchildren(0); 
  7. paul.setLastname("Nickel"); 
  8. paul.setMaleChildren(0); 
  9. paul.setMarried(true); 
  10. paul.setNickname("Paulo"); 
  11. paul.setSingle(false); 
  12. paul.setWidowed(false); 

Nous sommes loin d'être au top mais nous avons déjà amélioré notre code en termes de :

  • robustesse :  je réduis les paramètres passés au constructeur à ceux qui sont obligatoires, donc je réduis les possibilités de changement d'ordre et donc les impacts sur les instanciations ;
  • lisibilité :  je suis plus à même de savoir à quoi correspond chaque valeur ; la sémantique est portée par le nom de l'accesseur, si tant est que le membre est correctement nommé ;
  • usage :
    • je n'ai plus à me rappeler de l'ordre de passage des paramètres ; je fais comme bon me semble,
    • je n'ai plus qu'un seul constructeur à utiliser.

Néanmoins, cette technique est inefficace si le nombre de membres obligatoires est important. On retombe alors sur les inconvénients vus avec les constructions traditionnelles. De plus, nous avons introduit des faiblesses en termes de :

  • lisibilité :  je me retrouve avec une construction étalée sur une quinzaine de lignes là où une seule suffisait auparavant ;
  • robustesse :  cette technique n'est pas  thread-safe ;  une fois l'instance créée (ligne 1), plusieurs threads peuvent y accéder si je n'y prête pas attention. 

A la vue de tous ces éléments, cette technique n'est pas adaptée même si elle apporte quelques pistes intéressantes. Tournons-nous donc maintenant vers nos amies les fabriques.

III-B. Fabriques

La seconde alternative qui s'offre à nous pour améliorer la création de nos objets est une variante du patron de conception dit de la fabrique. Ce dernier fait partie d'un ensemble plus large proposé par le « gang des quatre » ( GOF dans la suite de cet article) dans l'ouvrage référence Image non disponible « Design Patterns: elements of reusable object-oriented software » , en 1994. Initialement, ce patron est dédié aux hiérarchies de classes et permet de créer des objets dont le type est dérivé d'une interface ou d'une classe abstraite, en fonction d'un certain nombre de paramètres. Il facilite la mise en œuvre du polymorphisme.

Mais au fil du temps, ce patron de conception créationnel a été détourné pour encapsuler la création d'objets. Le constructeur multiparamétré reste présent mais sa complexité est maintenant encapsulée dans une méthode statique que l'on nomme fabrique :

Une fabrique par défaut
Sélectionnez
  1. public static Person createDefaultPerson() { 
  2.     return new Person(true, false, false, 46, 2, 1, 1, 2, 1, "Jack", "Bauer", "JB"); 
  3. } 

Si l'on souhaite forcer l'usage de la (les) fabrique(s), le constructeur doit être défini comme privé. Ceci fait, mon appel devient simpliste :

Utiliser une fabrique par défaut
Sélectionnez
  1. final Person jack = Person.createDefaultPerson(); 

A ce stade, j'ai obtenu quelques gains en termes de :

  • lisibilité  : en temps normal, les constructeurs portent le nom de leur classe. On ne peut donc pas leur faire porter de sémantique comme c'est le cas avec les fabriques (exemple:  createDefaultPerson() ) ;
  • usage :  je n'ai plus à me rappeler des paramètres à passer ; ces derniers sont cachés au sein de la fabrique ;
  • robustesse :  ce type d'instanciation est  thread-safe puisque faisant usage des constructeurs traditionnels.

Mais je vous arrête de suite, cette solution est loin d'être idéale. En effet, on ne passe plus les paramètres au constructeur et pour chaque variation, il faut donc créer une nouvelle fabrique. Voyez ci-après un petit aperçu de ce que cela pourrait donner :

Un surplus de fabriques...
Sélectionnez
  1. public static Person createDefaultPerson() { 
  2.     return new Person(true, false, false, 46, 2, 1, 1, 2, 1, "Jack", "Bauer", "JB"); 
  3. } 
  4.  
  5. public static Person createDefaultSinglePerson() { 
  6.     return new Person(false, true, false, 46, 2, 1, 1, 2, 1, "Jack", "Bauer", "JB"); 
  7. } 
  8.  
  9. public static Person createDefaultWindowedPerson() { 
  10.     return new Person(false, true, true, 46, 2, 1, 1, 2, 1, "Jack", "Bauer", "JB"); 
  11. } 

Et dites-vous bien que le nombre de combinaisons possibles augmente avec la quantité de paramètres ! Finalement, cette solution comporte pas mal d'inconvénients :

  • lisibilité :  si je n'ai plus à me soucier de l'ordre et du sens des paramètres, je n'ai fait que reléguer ces informations au sein des fabriques. De plus, lorsque ces dernières deviennent trop nombreuses, la lecture des variations n'est plus évidente ;
  • robustesse :  si un possible changement d'ordre des paramètres de mon constructeur n'impacte plus mon code appelant, il touche tout de même à l'ensemble de mes fabriques. J'ai donc déshabillé Paul pour habiller Pierre ;
  • usage :  pour chaque variation, je dois créer une fabrique. À terme, je ne saurai donc plus laquelle utiliser et risquerai de dupliquer une existante.

Clairement, cette solution n'est pas la bonne. Tournons-nous donc maintenant vers un autre patron de conception et je dirais même plus, de construction.

III-C. Builder pattern

III-C-1. Principe

Pour aborder cette nouvelle alternative, je vais utiliser une analogie. Imaginons que vous entrez dans une pizzeria. Vous commandez la pizza du jour et quelques minutes plus tard, elle est sur votre table. Cependant, vous n'avez aucune idée du processus de construction et surtout, vous ne pouvez pas influer sur son contenu. Cette pizza du jour représente les fabriques vues précédemment. Ce que nous souhaitons, c'est pouvoir choisir les spécificités de notre pizza à partir d'un catalogue tout en s'appuyant sur une base commune. Cette idée, c'est le builder pattern .

Un  builder (ou bâtisseur) est une classe qui :

  1. Contient une variable d'instance pour chaque paramètre de la classe à construire ;
  2. Initialise ces variables avec une valeur par défaut ;
  3. Propose une méthode statique permettant d'initier la construction ;
  4. Propose des méthodes chaînables permettant de surcharger chacune des variables ;
  5. Propose une méthode de construction qui instancie un nouvel objet à partir des variables d'instances et peut se charger d'un certain nombre de vérifications.

Un exemple célèbre est celui du  StringBuilder qui permet de construire des chaînes de caractères ( StringBuffer étant sa version synchronisée).

Ce patron de conception est particulièrement adapté lorsqu'il existe un besoin de variation important sur la construction d'un objet. C'est très souvent le cas lorsque l'on construit des objets dans des tests unitaires ou avec des objets forts variants tel un DOMDocument Object Model . S'il fallait donner une règle quant à son utilisation, je suivrai celle énoncée par Joshua Bloch dans « Effective Java, 2de edition »:

« Le builder pattern est un choix avisé lors de la conception de classes dont les constructeurs ou fabriques statiques possèdent plus d'une poignée de paramètres. »

A noter qu'il diffère du Image non disponible patron de conception proposé par le GOF , ce dernier contenant notamment une classe directrice, rendant le tout un peu trop verbeux pour ce besoin précis.

III-C-2. Application au cas d'exemple

Appliquons maintenant ces différents points à l'exemple que nous poursuivons depuis le début de cet article. Tout d'abord, nous créons une variable d'instance avec une valeur par défaut pour chaque paramètre de l'objet à construire :

Variables d'instance du bâtisseur
Sélectionnez
  1. public static class PersonBuilder { 
  2.     private boolean married = false; 
  3.     private boolean single = true; 
  4.     private boolean widowed = false; 
  5.     private int age = 18; 
  6.     private int children = 0; 
  7.     private int femaleChildren = 0; 
  8.     private int grandchildren = 0; 
  9.     private int id = 1; 
  10.     private int maleChildren = 0; 
  11.     private String firstname = "Henry"; 
  12.     private String lastname = "Golant"; 
  13.     private String nickname = "Riri"; 
  14. } 

Puis, nous proposons une méthode statique qui permet d'initialiser la construction. Celle-ci retourne l'instance en cours de construction via l'usage du constructeur privé :

Méthode d'initialisation du bâtisseur
Sélectionnez
  1. public static class PersonBuilder { 
  2.  
  3.     private PersonBuilder() { 
  4.     } 
  5.  
  6.     public static PersonBuilder createDefaultPerson() { 
  7.         return new PersonBuilder(); 
  8.     } 
  9. } 

Ceci fait, nous définissons les méthodes qui vont permettre de surcharger les différentes variables d'instances. Ci-après celles dédiées aux noms et prénoms :

Méthodes de surcharge des variables du bâtisseur
Sélectionnez
  1. public static class PersonBuilder { 
  2.  
  3.     public PersonBuilder withFirstname(final String firstname) { 
  4.         this.firstname = firstname; 
  5.         return this; 
  6.     } 
  7.  
  8.     public PersonBuilder withLastname(final String lastname) { 
  9.         this.lastname = lastname; 
  10.         return this; 
  11.     } 
  12. } 

Notez que pour être chaînables, ces méthodes retournent l'instance en cours de construction. Enfin, nous définissons la méthode qui finalise la construction de l'objet, opère quelques vérifications et retourne l'instance créée à partir des variables du bâtisseur :

Méthode de finalisation du bâtisseur
Sélectionnez
  1. public Person build() { 
  2.     if (married && single) { 
  3.         throw new IllegalArgumentException("You can't be married and single at the same time!"); 
  4.     } 
  5.  
  6.     return new Person(married, single, widowed, age, children, femaleChildren, grandchildren, id, maleChildren, firstname, lastname, nickname); 
  7. } 

Ceci fait, je peux maintenant faire usage de mon bâtisseur pour obtenir une personne par défaut :

Une construction sans surcharge
Sélectionnez
  1. final Person henry = PersonBuilder.createDefaultPerson().build(); 

Je peux également surcharger les noms et prénoms par l'usage des méthodes chaînables :

Une construction avec une surcharge
Sélectionnez
  1. final Person joan = PersonBuilder.createDefaultPerson().withFirstname("Joan").withLastname("Baez").build(); 

Enfin, mes constructions sont dorénavant vérifiées lors de la création de l'instance. Admettons que je cherche à créer une personne célibataire et mariée :

Une construction invalide
Sélectionnez
  1. final Person sandy = PersonBuilder.createDefaultPerson().withMarriedStatus(true).build(); 

Lors de l'exécution, une erreur sera levée :

Exception obtenue sur la console
Sélectionnez
  1. Exception in thread "main" java.lang.IllegalArgumentException: You can't be married and single at the same time! 
  2.     at fr.mistertie.exemples.fctfi.model.Person$PersonBuilder.build(Person.java:94) 
  3.     at fr.mistertie.exemples.fctfi.service.PersonService.main(PersonService.java:42) 

Ainsi, l'usage du  builder pattern me permet d'obtenir un certain nombre d'avantages en termes de :

  • lisibilité :
    • Les caractéristiques de l'objet créé sont bien plus faciles à déterminer puisque la sémantique est portée par les méthodes chaînées ; plus de noms de fabriques interminables ou de constructeurs aux multiples paramètres,
    • La construction reste sommaire de par l'usage des méthodes chaînées ;
  • usage :
    • Je dispose de bien plus de flexibilité pour introduire mes variations ; je redéfinis seulement ce qui m'intéresse via l'usage des méthodes chaînées,
    • Je peux dorénavant introduire des vérifications dès l'instanciation de mon objet et m'assurer ainsi de sa validité,
    • Je n'ai plus à me rappeler de l'ordre des paramètres du constructeur ; je fais ce que bon me semble ;
  • robustesse :
    • Contrairement à la technique dite du  Java Bean , l'usage du  builder pattern est  thread-safe puisque l'instanciation se fait au travers du constructeur traditionnel lors de l'appel final. De plus, ce dernier se base sur les variables d'instance du bâtisseur,
    • Si je change l'ordre des paramètres, je n'impacte aucune de mes constructions.

L'inconvénient principal de ce  pattern est sa verbosité ; il oblige à écrire un bâtisseur pour chaque classe dont on souhaite en faire profiter.

IV. Optimisations du Builder Pattern

IV-A. Gestion de l'héritage

L'exemple que nous poursuivons depuis le début de cet article ne fait pas usage de l'héritage. Or, ce cas d'utilisation courant induit quelques modifications sur notre bâtisseur. Découvrons-les en simplifiant un instant notre exemple. Soit la classe suivante :

Un parent simple
Sélectionnez
  1. public class Parent { 
  2.  
  3.     private int fieldA; 
  4.     private int fieldB; 
  5. } 

Le bâtisseur associé doit maintenant vous sembler familier :

Un bâtisseur de parent
Sélectionnez
  1. public static class ParentBuilder { 
  2.  
  3.     private int fieldA = 0; 
  4.     private int fieldB = 0; 
  5.  
  6.     private ParentBuilder() { 
  7.     } 
  8.  
  9.     public static ParentBuilder createDefaultParent() { 
  10.         return new ParentBuilder(); 
  11.     } 
  12.  
  13.     public Parent build() { 
  14.         return new Parent(fieldA, fieldB); 
  15.     } 
  16.  
  17.     public ParentBuilder withFieldA(final int fieldA) { 
  18.         this.fieldA = fieldA; 
  19.         return this; 
  20.     } 
  21.  
  22.     public ParentBuilder withFieldB(final int fieldB) { 
  23.         this.fieldB = fieldB; 
  24.         return this; 
  25.     } 
  26.  
  27.     protected int getFieldA() { 
  28.         return fieldA; 
  29.     } 
  30.  
  31.     protected int getFieldB() { 
  32.         return fieldB; 
  33.     } 
  34.  
  35. } 

Notez cependant deux subtilités :

  • le constructeur par défaut du bâtisseur a la visibilité protected ;
  • les variables d'instances du bâtisseur ont des accesseurs avec la visibilité protected.

Nous verrons par la suite l'origine de ces dernières. Mais pour l'instant, nous pouvons déjà faire un usage simple de ce bâtisseur, comme vu dans le chapitre précédent :

Une construction de parent
Sélectionnez
  1. final Parent parent = ParentBuilder.createDefaultParent().build(); 
  2. final Parent parent = ParentBuilder.createDefaultParent().withFieldA(2).withFieldB(14).build(); 

Imaginons maintenant que cette classe est étendue par une autre :

Un enfant lui aussi parent
Sélectionnez
  1. public class Child extends Parent { 
  2.  
  3.     private int fieldC; 
  4.  
  5.     public Child(final int fieldA, final int fieldB, final int fieldC) { 
  6.         super(fieldA, fieldB); 
  7.         this.fieldC = fieldC; 
  8.     } 
  9. } 

Outre un champ supplémentaire, la classe fille possède un constructeur faisant appel à celui de la classe mère. Voyons maintenant à quoi ressemble le bâtisseur d'une telle classe.

Ce dernier possède deux variables d'instance ; l'une, attendue, concerne le membre fieldC de la classe fille. L'autre, est une instance du bâtisseur du parent, avec sa valeur par défaut.

Les variables d'instance du bâtisseur d'enfants
Sélectionnez
  1. public static class ChildBuilder { 
  2.  
  3.     private ParentBuilder parentBuilder = ParentBuilder.createDefaultParent(); 
  4.     private int fieldC = 0; 
  5. } 

De manière similaire, on trouve deux méthodes de surcharge ; l'une pour le champ de la classe fille et l'autre pour le bâtisseur parent.

Les méthodes de surcharge du bâtisseur d'enfants
Sélectionnez
  1. public static class ChildBuilder { 
  2.  
  3.     public ChildBuilder withParentBuilder(final ParentBuilder parentBuilder) { 
  4.         this.parentBuilder = parentBuilder; 
  5.         return this; 
  6.     } 
  7.  
  8.     public ChildBuilder withFieldC(final int fieldC) { 
  9.         this.fieldC = fieldC; 
  10.         return this; 
  11.     } 
  12. } 

La méthode initialisant la construction ne change pas dans sa forme. Mais c'est elle qui nécessite que le constructeur sans arguments de la classe mère ait la visibilité protected. Enfin, la méthode de finalisation fait usage des variables d'instances des deux bâtisseurs. D'où la présence des accesseurs dans la classe mère.

La création et la finalisation du bâtisseur d'enfants
Sélectionnez
  1. public static class ChildBuilder { 
  2.  
  3.     public static ChildBuilder createDefaultChild() { 
  4.         return new ChildBuilder(); 
  5.     } 
  6.  
  7.     public Child build() { 
  8.         return new Child(parentBuilder.getFieldA(), parentBuilder.getFieldB(), fieldC); 
  9.     } 
  10. } 

Avec une telle gestion des bâtisseurs, je peux maintenant mettre en œuvre plusieurs scénarios :

  • créer un enfant par défaut ;
  • créer un enfant en surchargeant uniquement le champ fieldC ;
  • créer un enfant en surchargeant les champs parents.
Différents cas d'utilisation du bâtisseur d'enfants
Sélectionnez
  1. final Child child = ChildBuilder.createDefaultChild().build(); 
  2. final Child child = ChildBuilder.createDefaultChild().withFieldC(12).build(); 
  3. final Child child = createDefaultChild().withParentBuilder(createDefaultParent().withFieldA(14)).withFieldC(12).build(); 

Les sorties consoles nous montrent bien que le résultat est celui attendu :

Sorties console des constructions
Sélectionnez
  1. Child(super=Parent(fieldA=0, fieldB=0), fieldC=0) 
  2. Child(super=Parent(fieldA=0, fieldB=0), fieldC=12) 
  3. Child(super=Parent(fieldA=14, fieldB=0), fieldC=12) 

IV-B. Partage des bâtisseurs

Si vous utilisez couramment les bâtisseurs, vous mourez probablement d'envie de me signaler qu'à l'usage, ce pattern peut amener à dupliquer du code. Reprenons notre exemple à base de personnes et imaginons que, dans un test unitaire, nous souhaitions en créer deux quasiment identiques :

Un Paul peut en cacher un autre
Sélectionnez
  1. final Person paulSpencer = PersonBuilder.createDefaultPerson().withAge(18).withFirstname("Paul").withLastname("Spencer").build(); 
  2. final Person paulJones = PersonBuilder.createDefaultPerson().withAge(18).withFirstname("Paul").withLastname("Jones").build(); 

L'exemple est ici volontairement simpliste mais prend tout son sens lorsque le nombre d'appels de méthodes chaînées communes augmente. On pourrait être tenté de faire les choses comme ceci :

Mutualisation valide des bâtisseurs
Sélectionnez
  1. final PersonBuilder paulBuilder = PersonBuilder.createDefaultPerson().withAge(18).withFirstname("Paul"); 
  2. final Person paulSpencerJr = paulBuilder.withLastname("Spencer").build(); 
  3. final Person paulJonesJr = paulBuilder.withLastname("Jones").build(); 

Cela fonctionne dans ce cas, mais uniquement car la méthode chaînée utilisée (i.e. withLastName() ) est  la même ! Si je surcharge d'autres valeurs, le résultat sera plus incertain :

Mutualisation invalide des bâtisseurs
Sélectionnez
  1. final PersonBuilder maryBuilder = PersonBuilder.createDefaultPerson().withAge(18).withFirstname("Mary"); 
  2. final Person marySpencer = maryBuilder.withChildren(2).withFemaleChildren(2).build(); 
  3. final Person maryJones = maryBuilder.withLastname("Jones").build(); 

Dans le code ci-dessus, le deuxième objet ne s'attend pas à obtenir une instance avec deux filles mais seulement un objet de 18 ans s'appelant Mary Jones :

Sortie console prouvant l'invalidité
Sélectionnez
  1. Person [id=1, married=false, single=true, widowed=false, age=18, children=2, femaleChildren=2, grandchildren=0, maleChildren=0, firstname=Mary, lastname=Golant, nickname=Riri] 
  2. Person [id=1, married=false, single=true, widowed=false, age=18, children=2, femaleChildren=2, grandchildren=0, maleChildren=0, firstname=Mary, lastname=Jones, nickname=Riri] 

Pour parer à ce type de mésaventure, il faut optimiser son bâtisseur. Nous ajoutons un nouveau constructeur qui prend un bâtisseur en entrée et en copie les données.

Constructeur par copie
Sélectionnez
  1. public static PersonBuilder createFrom(final PersonBuilder prototype) { 
  2.     final PersonBuilder builder = new PersonBuilder(); 
  3.     builder.married = prototype.married; 
  4.     builder.single = prototype.single; 
  5.     builder.widowed = prototype.widowed; 
  6.     builder.age = prototype.age; 
  7.     builder.children = prototype.children; 
  8.     builder.femaleChildren = prototype.femaleChildren; 
  9.     builder.grandchildren = prototype.grandchildren; 
  10.     builder.id = prototype.id; 
  11.     builder.maleChildren = prototype.maleChildren; 
  12.     builder.firstname = prototype.firstname; 
  13.     builder.lastname = prototype.lastname; 
  14.     builder.nickname = prototype.nickname; 
  15.  
  16.     return builder; 
  17. } 

On peut ainsi facilement partager certaines étapes de nos constructions :

Mutualisation valide via une copie
Sélectionnez
  1. final PersonBuilder shareableMaryBuilder = PersonBuilder.createDefaultPerson().withAge(18).withFirstname("Mary"); 
  2. final Person marySpencerJr = PersonBuilder.createFrom(shareableMaryBuilder).withChildren(2).withFemaleChildren(2).build(); 
  3. final Person maryJonesJr = PersonBuilder.createFrom(shareableMaryBuilder).withLastname("Jones").build(); 

Le résultat est bien celui attendu :

Sortie console prouvant la validité
Sélectionnez
  1. Person [id=1, married=false, single=true, widowed=false, age=18, children=2, femaleChildren=2, grandchildren=0, maleChildren=0, firstname=Mary, lastname=Golant, nickname=Riri] 
  2. Person [id=1, married=false, single=true, widowed=false, age=18, children=0, femaleChildren=0, grandchildren=0, maleChildren=0, firstname=Mary, lastname=Jones, nickname=Riri] 

Notez qu'à la place du constructeur par copie, j'aurais pu utiliser un clonage classique. Cette technique à l'avantage de déléguer l'écriture du code de la copie à Java.

IV-C. Réduction de la verbosité

Nous l'avons vu précédemment, le principal inconvénient du Builder Pattern est sa verbosité. Pour réduire cette dernière, nous allons voir ensemble ce qu'une bibliothèquecomme lombok peut faire pour nous.

Lombok est une bibliothèque qui, à partir d'un ensemble d'annotations, se propose d'écrire tout le code rébarbatif à notre place. Plus précisément, elle intervient durant la phase de compilation pour modifier le code source et y ajouter certaines méthodes.

Si vous désirez en savoir plus ou n'êtes pas familier de cette bibliothèque, je vous conseille ce tutoriel, par Thierry Leriche-Dessirier ainsi que ce post qui explique son fonctionnement.

Lombok ne propose pas d'annotation par défaut pour mettre en œuvre le Builder Pattern . En revanche, c'est le cas de la bibliothèque d'extensions lombok-pg . Voyons ensemble comment faire. Tout d'abord, notre classe s'en trouve simplifiée :

Une personne affinée
Sélectionnez
  1. @Builder(prefix = "with") 
  2. public class ThinPerson { 
  3.  
  4.     private final int id; 
  5.  
  6.     private boolean married; 
  7.     private boolean single; 
  8.     private boolean widowed; 
  9.     private int age; 
  10.     private int children; 
  11.     private int femaleChildren; 
  12.     private int grandchildren; 
  13.     private int maleChildren; 
  14.     private String firstname; 
  15.     private String lastname; 
  16.     private String nickname; 
  17. } 

Nous avons posé l'annotation @Builder sur notre classe. C'est cette dernière qui va nous permettre de générer le bâtisseur associé. Grâce à l'attribut prefix , nous avons précisé que les méthodes de surcharge devront commencer par with . Si vous regardez dans la vue Outline de votre EDI , vous pouvez observer que Lombok a généré pas mal de choses. Nous y reviendrons par la suite.

Voyons maintenant l'usage que nous pouvons faire de ce bâtisseur. Essayons de créer une personne par défaut :

Création invalide d'une personne par défaut via Lombok
Sélectionnez
  1. final ThinPerson henry = ThinPerson.thinPerson().build(); 

Nous faisons usage de la méthode statique thinPerson() qui initialise la construction. Elle équivaut à la méthode createDefaultPerson() vue précédemment. Nous faisons également usage de la méthode build() qui finalise la construction. Néanmoins, ce code ne compile pas («  The method build() is undefined for the type ThinPerson.IdDef » ) car le bâtisseur de lombok oblige à (re)définir tous les champs finaux, ce qui est le cas de notre champ « id ». Il nous faut donc mettre à jour notre code :

Création valide d'une personne par défaut via Lombok
Sélectionnez
  1. final ThinPerson henry = ThinPerson.thinPerson().withId(12).build(); 

La surcharge des autres champs n'a alors rien de bien compliqué :

Création et surcharge d'une personne via Lombok
Sélectionnez
  1. final ThinPerson joan = ThinPerson.thinPerson().withId(12).withFirstname("Joan").withLastname("Baez").build(); 

Au premier abord, le bâtisseur proposé par Lombok semble donc faire le travail demandé. Mais à y regarder de plus près, les inconvénients sont nombreux en termes de :

  • lisibilité :  il est impossible de renommer la méthode qui initialise la construction. Or, le nom de classe qui est utilisé est loin d'être le plus explicite ;
  • usage :  il est impossible de créer une instance sans redéfinir les champs finaux ;
  • robustesse :
    • il est impossible d'introduire une quelconque validation dans la méthode de finalisation,
    • il n'est pas possible de partager les bâtisseurs car d'une part, ces derniers sont privés et d'autre part, ils font usage de la première méthode vue ensemble (ex : les méthodes de surcharge retournent l'instance en cours de création),
    • il n'y a pas d'intelligence dans les valeurs par défaut des variables d'instance. En effet, lombok ne se base que sur le type de la variable pour en déduire la valeur associée. Un entier sera donc initialisé à 0, un objet à null , etc. Ce qui n'a pas forcément de sens selon le contexte.

Vous l'aurez compris, si cette solution réduit la verbosité de votre code, elle est loin d'être optimale et doit être réservée à des cas d'utilisation très simples où les contraintes ne vous impacteront pas. Dans notre cas, je considère quelle n'est pas adaptée et reste donc sur les bâtisseurs obtenus précédemment.

Une alternative plus souple consiste à faire usage du générateur de code de son EDI préféré.

V. Aller plus loin ; les fluent interfaces

Maintenant que nous sommes arrivés à une solution satisfaisante, essayons d'y ajouter la touche finale. En l'occurrence, il s'agit d'y rajouter une pincée de lisibilité via les  fluent interfaces (ou interfaces fluides). Ces dernières sont des façades sémantiques qui peuvent par exemple, venir se greffer au dessus d'un code existant pour :

  • réduire sa verbosité ;
  • améliorer sa lisibilité ;
  • exprimer son contenu.

Contrairement aux idées reçues, les  fluent interfaces  ne correspondent pas simplement au chaînage des méthodes. Elles introduisent un lien sémantique entre les objets afin d'en exprimer le contenu et surtout la relation. Elles sont largement utilisées pour créer des  DSL (Domain Specific Languages ). L'idée est qu'un code en faisant usage doit pouvoir être lu comme une phrase en anglais.

Reprenons l'exemple suivi tout au long de cet article. Disons que nous souhaitons proposer une API pour créer des couples de personnes. Nous pourrions utiliser notre bâtisseur pour ce faire :

Création d'un couple célèbre
Sélectionnez
  1. final Person barackObama = PersonBuilder.createDefaultPerson().withFirstname("Barack").withLastname("Obama").withMarriedStatus(true).withSingleStatus(false).build(); 
  2. final Person michelleObama = PersonBuilder.createDefaultPerson().withFirstname("Michelle").withLastname("Obama").withMarriedStatus(true).withSingleStatus(false).build(); 

L'usage correspond à ce que nous avons vu auparavant mais s'avère relativement verbeux, peu lisible et n'exprime pas le fait que les deux personnes sont mariées. Nous allons donc créer une façade sémantique pour cacher cette complexité et exprimer la relation entre les objets créés :

Une interface fluide
Sélectionnez
  1. public class PersonFluentInterface { 
  2.  
  3.     private final Person fiance; 
  4.  
  5.     private PersonFluentInterface(final Person fiance) { 
  6.         this.fiance = fiance; 
  7.     } 
  8.  
  9.     public static PersonFluentInterface aGuyNamed(final String firstname, final String lastname) { 
  10.         return new PersonFluentInterface(PersonBuilder.createDefaultPerson().withFirstname(firstname).withLastname(lastname).build()); 
  11.     } 
  12.  
  13.     public static Person aGirlNamed(final String firstname) { 
  14.         return PersonBuilder.createDefaultPerson().withFirstname(firstname).build(); 
  15.     } 
  16.  
  17.     public Set<Person> isMarriedTo(final Person fiancee) { 
  18.         this.fiance.setMarried(true); 
  19.         this.fiance.setSingle(false); 
  20.         fiancee.setMarried(true); 
  21.         fiancee.setSingle(false); 
  22.         fiancee.setLastname(this.fiance.getLastname()); 
  23.  
  24.         final Set<Person> couple = new HashSet<>(2); 
  25.         couple.add(fiancee); 
  26.         couple.add(this.fiance); 
  27.         return couple; 
  28.     } 
  29. } 

On remarque quelques similitudes avec le bâtisseur mais ne nous y trompons pas, les contraintes ne sont pas les mêmes. A vrai dire, il y en a même moins ; j'ai par exemple décidé d'offrir une méthode statique pour démarrer mon chaînage et deux méthodes finales. L'une me permet d'obtenir une fille et l'autre de marier le garçon du chaînage avec une autre personne ; le couple obtenu est ainsi retourné. Par l'usage des imports statiques, je peux ensuite obtenir l'usage suivant :

Usage de l'interface fluide
Sélectionnez
  1. final Set<Person> theObamas = aGuyNamed("Barack", "Obama").isMarriedTo(aGirlNamed("Michelle")); 

Ici, le contenu de ma ligne est rempli de sémantique ; j'exprime qu'un garçon nommé Barack Obama est marié avec une fille appelée Michelle, peu importe les mécanismes sous-jacents. J'exprime cette relation et réduis la verbosité de mon bâtisseur ; c'est idéal pour que l'utilisateur de mon API sache rapidement s'en servir.

A travers ce court exemple volontairement simpliste, on découvre l'intérêt des  fluent interfaces . Elles ont vocation à améliorer la lisibilité et à faciliter l'apprentissage des  API . Elles doivent être utilisées avec parcimonie et être réservées à cet usage. Enfin, sachez qu'il n'est pas rare de prendre du temps pour construire une bonne interface  fluent  ; plusieurs itérations peuvent être nécessaires.

A titre d'exemples, voyez les interfaces proposées par la bibliothèque FEST ( Java ) :

Exemple Fest Assert
Sélectionnez
  1. File xFile = writeFile("xFile", "The Truth Is Out There"); 
  2. assertThat(xFile).exists().isFile().isRelative(); 
  3. assertThat(xFile).canRead().canWrite(); 
  4. assertThat(contentOf(xFile)).startsWith("The Truth").contains("Is Out").endsWith("There"); 

Ainsi que par l'outil Gatling ( Scala ) :

Exemple Gatling
Sélectionnez
  1. .exec(http("request_1") 
  2.     .get("/") 
  3.     .headers(headers_1) 
  4.     .check(status.is(302) 
  5. ) 

VI. Conclusion

Dans ce billet, je vous ai présenté l'antipattern  « Telescoping Constructor » et les alternatives disponibles pour y remédier, listant les avantages et inconvénients de chacune. La solution du bâtisseur semble être la plus appropriée dans bien des usages et je vous la recommande vivement, particulièrement dans l'écriture de vos tests unitaires. C'est d'autant plus vrai que l'arrivée de Java 8 et des lambdas devrait permettre de faire encore mieux (voir ce lien ).

Enfin, je vous ai donné un aperçu des  Fluent Interfaces et vous invite donc à creuser ce sujet pour en connaître davantage (ce dernier fait parfois polémique, cf. « Les fluent interfaces sont le diable » ).

VII. Remerciements

Je remercie les membres de la communauté developpez.com pour leur relecture attentive et leurs conseils avisés qui ont permis d'enrichir l'article initial. Je remercie particulièrement Laurent.B , Mickael Baron , Thierry Leriche-Dessirier et Nemek . Je remercie également Phanloga pour sa relecture orthographique.

VIII. Autour de ce billet