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 3 attributs et, dans ce cas, utiliser un simple constructeur avec en paramètre 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.
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 chainé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 via le nom des methods ils évitent ces tristes moments de solitude ressentis par les développeurs qui doivent invoquer des constructeurs avec 5 Strings et 2 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 :
@Test
void shouldNotBuildWithoutName() {
assertThatThrownBy(() -> fullBuilder().name(null).build())
.isExactlyInstanceOf(MissingMandatoryValueException.class)
.hasMessageContaining("name");
}
// ...
private static BranchBuilder fullBuilder(Branch target) {
// Here the code to get a fully setted builder
}
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 on veut construire un objet en parsant un XML avec parser 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).
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'aurai donc :
public class Branch {
private final String name;
private final Optional<String> description;
private final Username owner;
private Branch(BranchBuilder builder) {
// Fields controls
name = builder.name;
description = Optional.ofNullable(builder.description);
owner = new Username(builder.owner);
}
public static BranchBuilder builder() {
return new BranchBuilder();
}
// Business methods
public static class BranchBuilder {
private String name;
private String description;
private String owner;
public BranchBuilder name(String name) {
this.name = name;
return this;
}
public BranchBuilder description(String description) {
this.description = description;
return this;
}
public BranchBuilder owner(String owner) {
this.owner = owner;
return this;
}
public Branch build() {
return new Branch(this);
}
}
}
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.
Les Builders et l'héritage
Un des très gros défaut 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 :
public abstract class Field {
private final String key;
protected Field(FieldBuilder<?, ?> builder) {
// Fields controls
key = builder.key;
}
public abstract static class FieldBuilder<T extends FieldBuilder<T, U>, U extends Field> {
private String key;
@SuppressWarnings("unchecked")
public T key(String key) {
this.key = key;
return (T) this;
}
// Other fields methods
public abstract U build();
}
}
Et une implémentation :
public class MandatoryBooleanField extends Field {
protected MandatoryBooleanField(FieldBuilder<?, ?> builder) {
super(builder);
}
public static class MandatoryBooleanFieldBuilder extends FieldBuilder<MandatoryBooleanFieldBuilder, MandatoryBooleanField> {
@Override
public MandatoryBooleanField build() {
return new MandatoryBooleanField(this);
}
}
}
Même si cela semble complexe à l'écriture, à l'utilisation on utilisera ces Builders comme tous les autres !
Construire toujours plus
Les Builders sont très répandus. La très grand majorité des Frameworks les prennent très bien en compte. Par exemple, avec Jackson, on peut désérialiser des objets utilisant des Builders :
@JsonDeserialize(builder = BranchBuilder.class)
public class Branch {
private Branch(BranchBuilder builder) {
// Building logic
}
// Business methods
@JsonPOJOBuilder(withPrefix = "")
public static class BranchBuilder {
// All methods
}
}
Il n'y a pas vraiment de limitation à l'utilisation des Builders au quotidien, globalement, les essayer c'est les adopter !