Tutoriel pour apprendre à développer le patron de conception Builder

Colin Damon

Quand on fait du développement orienté objet, on cherche à construire des objets cohérents avec tous leurs attributs. Il arrive que les objets que l'on souhaite construire aient plus de trois attributs et, dans ce cas, utiliser un simple constructeur avec en paramètres tous les attributs n'est pas forcément une bonne idée !

Dans ces cas, l'utilisation d'un Builder peut être une bonne solution ! Nous allons voir ici quelques-unes des manières de faire des Builders en Java.

Vous pouvez réagir par rapport à cet article. Commentez

- Titre : Tutoriel pour apprendre à développer le patron de conception Builder

- Auteur : Colin Damon

- Parution : 23 juin 2020

- Licence : Copyright ® 2020 Colin Damon. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Quelques avantages des Builders

- II. Des Builders simples

- III. Les Builders et l'héritage

- IV. Construire toujours plus

- V. Remerciements

I - Quelques avantages des Builders

Un petit rappel pour commencer : un Builder est une class qui permettra la construction d'une autre class en fournissant une API fluent. Chaque méthode renvoie l'instance du Builder permettant ainsi les invocations chaînées. Les différentes invocations peuvent se lire comme une phrase, d'où le nom de fluent. Une fois les différents paramètres renseignés, on invoquera une method de construction (souvent build()).

Les builders ne se contentent pas de faciliter l'écriture des invocations avec une API fluent : avec leurs APIs permettant de décrire simplement les paramètres par le nom des methods, ils évitent ces tristes moments de solitude ressentis par les développeurs qui doivent invoquer des constructeurs avec cinq Strings et deux Booleans…

Ils facilitent aussi l'écriture des cas de tests de construction de nos objets (ou d'erreurs de construction) puisqu'il est très facile de construire une instance de Builder complète et correcte dans les tests et de changer un seul paramètre pour tester un cas précis :

1. @Test

2. void shouldNotBuildWithoutName() {

3. assertThatThrownBy(() -> fullBuilder().name(null).build())

4. .isExactlyInstanceOf(MissingMandatoryValueException.class)

5. .hasMessageContaining("name");

6. }

7. // ...

8. private static BranchBuilder fullBuilder(Branch target) {

9. // Ici, le code complet pour construire un builder

10. }

Les builders peuvent aussi être de bons alliés de nos algorithmes puisqu'ils sont mutables et peuvent permettre la construction d'objets immuables. Par exemple, si l’on veut construire un objet en parsant un XML avec un parseur SAX (oui, même aujourd'hui on est parfois obligé de faire ça…), il sera très pratique de renseigner un Builder jusqu'à trouver notre balise fermante pour finalement construire notre objet.

Pour ces raisons, j'utilise aujourd'hui beaucoup (trop ?) de Builders dans la construction des produits sur lesquels je travaille et je ne peux que vous inviter à en faire autant ! (Même si c'est un peu verbeux, il faut bien l'admettre.)

II - Des Builders simples

Il existe pléthore de manières de faire des Builders. Après en avoir essayé certaines, voici ce que j'aime faire actuellement :

- une static factory du Builder dans la class qui doit être construite ;

- une static class de Builder dans le même fichier que la class à construire ;

- des methods d'affectation des valeurs dans le Builder qui portent simplement le nom de l'attribut ;

- une method build dans le Builder qui invoque un constructeur privé de la class à construire en passant l'instance du Builder.

Si je veux faire un Builder pour Branch qui se construit avec un name, une description et un owner, j'aurais donc :

1. public class Branch {

2. private final String name;

3. private final Optional<String> description;

4. private final Username owner;

6. private Branch(BranchBuilder builder) {

7. // Fields controls

9. name = builder.name;

10. description = Optional.ofNullable(builder.description);

11. owner = new Username(builder.owner);

12. }

14. public static BranchBuilder builder() {

15. return new BranchBuilder();

16. }

18. // Business methods

20. public static class BranchBuilder {

21. private String name;

22. private String description;

23. private String owner;

25. public BranchBuilder name(String name) {

26. this.name = name;

28. return this;

29. }

30. public BranchBuilder description(String description) {

31. this.description = description;

32. return this;

33. }

34. public BranchBuilder owner(String owner) {

35. this.owner = owner;

36. return this;

37. }

38. public Branch build() {

39. return new Branch(this);

40. }

41. }

42. }

Dans cet exemple, j'ai fait le choix de manipuler des primitives dans mon Builder et de faire la création des objets dans le constructeur de la class. Il est aussi possible de prendre directement les objets en paramètre des méthodes du Builder.

Je trouve que cette manière de faire a de nombreux avantages :

- c'est la class que l'on construit qui garde le contrôle sur le contenu des données lors de la construction ;

- on ne peut construire la class qu'en passant par son Builder ;

- la complexité du code du Builder est extrêmement réduite.

III - Les Builders et l'héritage

Un des très gros défauts de l'approche précédente est qu'elle bloque totalement les extensions. Il est possible de faire des extensions avec des Builders en utilisant les generics :

1. public abstract class Field {

2. private final String key;

4. protected Field(FieldBuilder<?, ?> builder) {

5. // Fields controls

7. key = builder.key;

8. }

9. public abstract static class FieldBuilder<T extends FieldBuilder<T, U>, U extends Field> {

10. private String key;

11. @SuppressWarnings("unchecked")

12. public T key(String key) {

13. this.key = key;

14. return (T) this;

15. }

16. // Other fields methods

17. public abstract U build();

18. }

19. }

Et une implémentation :

1. public class MandatoryBooleanField extends Field {

2. protected MandatoryBooleanField(FieldBuilder<?, ?> builder) {

3. super(builder);

4. }

5. public static class MandatoryBooleanFieldBuilder extends FieldBuilder<MandatoryBooleanFieldBuilder, MandatoryBooleanField> {

6. @Override

7. public MandatoryBooleanField build() {

8. return new MandatoryBooleanField(this);

9. }

10. }

11. }

Même si cela semble complexe à l'écriture, à l'utilisation on utilisera ces Builders comme tous les autres !

IV - Construire toujours plus

Les Builders sont très répandus. La très grande majorité des Frameworks les prennent très bien en compte. Par exemple, avec Jackson, on peut désérialiser des objets utilisant des builders :

1. @JsonDeserialize(builder = BranchBuilder.class)

2. public class Branch {

3. private Branch(BranchBuilder builder) {

4. // Building logic

5. }

6. // Business methods

7. @JsonPOJOBuilder(withPrefix = "")

8. public static class BranchBuilder {

9. // All methods

10. }

11. }

Il n'y a pas vraiment de limitation à l'utilisation des builders au quotidien. Globalement, les essayer c'est les adopter !

V - Remerciements

Cet article a été publié avec l'aimable autorisation de Colin Damon. L'article original peut être vu sur le blog de la société Ippon.

Nous remercions également Winjerome pour la mise au gabarit, Alexandre T pour la relecture orthographique de cet article.