Génération automatique du modèle Builder avec FreeBuilder

1. Vue d'ensemble

Dans ce didacticiel, nous utiliserons la bibliothèque FreeBuilder pour générer des classes de générateur en Java.

2. Modèle de conception du constructeur

Builder est l'un des modèles de conception de création les plus utilisés dans les langages orientés objet. Il résume l'instanciation d'un objet de domaine complexe et fournit une API fluide pour créer une instance. Cela aide ainsi à maintenir une couche de domaine concise.

Malgré son utilité, un constructeur est généralement complexe à implémenter, notamment en Java. Les objets de valeur encore plus simples nécessitent beaucoup de code standard.

3. Implémentation de Builder en Java

Avant de continuer avec FreeBuilder, implémentons un générateur standard pour notre classe Employee :

public class Employee { private final String name; private final int age; private final String department; private Employee(String name, int age, String department) { this.name = name; this.age = age; this.department = department; } }

Et une classe Builder interne :

public static class Builder { private String name; private int age; private String department; public Builder setName(String name) { this.name = name; return this; } public Builder setAge(int age) { this.age = age; return this; } public Builder setDepartment(String department) { this.department = department; return this; } public Employee build() { return new Employee(name, age, department); } }

En conséquence, nous pouvons maintenant utiliser le générateur pour instancier l' objet Employee :

Employee.Builder emplBuilder = new Employee.Builder(); Employee employee = emplBuilder .setName("baeldung") .setAge(12) .setDepartment("Builder Pattern") .build();

Comme indiqué ci-dessus, beaucoup de code standard est nécessaire pour implémenter une classe de générateur.

Dans les sections suivantes, nous verrons comment FreeBuilder peut instantanément simplifier cette implémentation.

4. Dépendance de Maven

Pour ajouter la bibliothèque FreeBuilder, nous allons ajouter la dépendance FreeBuilder Maven dans notre pom.xml :

 org.inferred freebuilder 2.4.1 

5. Annotation FreeBuilder

5.1. Générer un constructeur

FreeBuilder est une bibliothèque open-source qui aide les développeurs à éviter le code standard lors de l'implémentation des classes de générateur. Il utilise le traitement des annotations en Java pour générer une implémentation concrète du modèle de générateur.

Nous annotons notre classe Employee de la section précédente avec @ FreeBuilder et verrons comment elle génère automatiquement la classe de générateur:

@FreeBuilder public interface Employee { String name(); int age(); String department(); class Builder extends Employee_Builder { } }

Il est important de souligner que Employee est désormais une interface plutôt qu'une classe POJO. De plus, il contient tous les attributs d'un objet Employee en tant que méthodes.

Avant de continuer à utiliser ce générateur, nous devons configurer nos IDE pour éviter tout problème de compilation. Comme FreeBuilder génère automatiquement la classe Employee_Builder lors de la compilation, l'EDI se plaint généralement de ClassNotFoundException sur la ligne numéro 8 .

Pour éviter de tels problèmes, nous devons activer le traitement des annotations dans IntelliJ ou Eclipse . Et ce faisant, nous utiliserons le processeur d'annotation de FreeBuilder org.inferred.freebuilder.processor.Processor. En outre, le répertoire utilisé pour générer ces fichiers source doit être marqué comme Generated Sources Root.

Alternativement, nous pouvons également exécuter mvn install pour construire le projet et générer les classes de générateur requises.

Enfin, nous avons compilé notre projet et pouvons maintenant utiliser la classe Employee.Builder :

Employee.Builder builder = new Employee.Builder(); Employee employee = builder.name("baeldung") .age(10) .department("Builder Pattern") .build();

Dans l'ensemble, il existe deux différences principales entre celle-ci et la classe de constructeur que nous avons vue précédemment. Tout d'abord, nous devons définir la valeur de tous les attributs de la classe Employee . Sinon, il lève une IllegalStateException .

Nous verrons comment FreeBuilder gère les attributs facultatifs dans une section ultérieure.

Deuxièmement, les noms de méthode de Employee.Builder ne suivent pas les conventions de dénomination JavaBean. Nous verrons cela dans la section suivante.

5.2. Convention de dénomination JavaBean

Pour forcer FreeBuilder à suivre la convention de dénomination JavaBean, nous devons renommer nos méthodes dans Employee et préfixer les méthodes avec get :

@FreeBuilder public interface Employee { String getName(); int getAge(); String getDepartment(); class Builder extends Employee_Builder { } }

Cela générera des getters et des setters qui suivent la convention de dénomination JavaBean:

Employee employee = builder .setName("baeldung") .setAge(10) .setDepartment("Builder Pattern") .build();

5.3. Méthodes du mappeur

Couplé avec des getters et des setters, FreeBuilder ajoute également des méthodes de mappage dans la classe de générateur. Ces méthodes de mappage acceptent un UnaryOperator comme entrée, permettant ainsi aux développeurs de calculer des valeurs de champ complexes.

Supposons que notre classe Employee dispose également d'un champ Salaire:

@FreeBuilder public interface Employee { Optional getSalaryInUSD(); }

Supposons maintenant que nous devions convertir la devise du salaire fournie en entrée:

long salaryInEuros = INPUT_SALARY_EUROS; Employee.Builder builder = new Employee.Builder(); Employee employee = builder .setName("baeldung") .setAge(10) .mapSalaryInUSD(sal -> salaryInEuros * EUROS_TO_USD_RATIO) .build();

FreeBuilder fournit de telles méthodes de mappage pour tous les champs.

6. Valeurs par défaut et vérifications des contraintes

6.1. Définition des valeurs par défaut

L' implémentation Employee.Builder dont nous avons parlé jusqu'à présent attend du client qu'il transmette des valeurs pour tous les champs. En fait, il échoue le processus d'initialisation avec une IllegalStateException en cas de champs manquants.

In order to avoid such failures, we can either set default values for fields or make them optional.

We can set default values in the Employee.Builder constructor:

@FreeBuilder public interface Employee { // getter methods class Builder extends Employee_Builder { public Builder() { setDepartment("Builder Pattern"); } } }

So we simply set the default department in the constructor. This value will apply to all Employee objects.

6.2. Constraint Checks

Usually, we have certain constraints on field values. For example, a valid email must contain an “@” or the age of an Employee must be within a range.

Such constraints require us to put validations on input values. And FreeBuilder allows us to add these validations by merely overriding the setter methods:

@FreeBuilder public interface Employee { // getter methods class Builder extends Employee_Builder { @Override public Builder setEmail(String email) { if (checkValidEmail(email)) return super.setEmail(email); else throw new IllegalArgumentException("Invalid email"); } private boolean checkValidEmail(String email) { return email.contains("@"); } } }

7. Optional Values

7.1. Using Optional Fields

Some objects contain optional fields, the values for which can be empty or null. FreeBuilder allows us to define such fields using the Java Optional type:

@FreeBuilder public interface Employee { String getName(); int getAge(); // other getters Optional getPermanent(); Optional getDateOfJoining(); class Builder extends Employee_Builder { } }

Now we may skip providing any value for Optional fields:

Employee employee = builder.setName("baeldung") .setAge(10) .setPermanent(true) .build();

Notably, we simply passed the value for permanent field instead of an Optional. Since we didn't set the value for dateOfJoining field, it will be Optional.empty() which is the default for Optional fields.

7.2. Using @Nullable Fields

Although using Optional is recommended for handling nulls in Java, FreeBuilder allows us to use @Nullable for backward compatibility:

@FreeBuilder public interface Employee { String getName(); int getAge(); // other getter methods Optional getPermanent(); Optional getDateOfJoining(); @Nullable String getCurrentProject(); class Builder extends Employee_Builder { } }

The use of Optional is ill-advised in some cases which is another reason why @Nullable is preferred for builder classes.

8. Collections and Maps

FreeBuilder has special support for collections and maps:

@FreeBuilder public interface Employee { String getName(); int getAge(); // other getter methods List getAccessTokens(); Map getAssetsSerialIdMapping(); class Builder extends Employee_Builder { } }

FreeBuilder adds convenience methods to add input elements into the Collection in the builder class:

Employee employee = builder.setName("baeldung") .setAge(10) .addAccessTokens(1221819L) .addAccessTokens(1223441L, 134567L) .build();

There is also a getAccessTokens() method in the builder class which returns an unmodifiable list. Similarly, for Map:

Employee employee = builder.setName("baeldung") .setAge(10) .addAccessTokens(1221819L) .addAccessTokens(1223441L, 134567L) .putAssetsSerialIdMapping("Laptop", 12345L) .build();

The getter method for Map also returns an unmodifiable map to the client code.

9. Nested Builders

For real-world applications, we may have to nest a lot of value objects for our domain entities. And since the nested objects can themselves need builder implementations, FreeBuilder allows nested buildable types.

For example, suppose we have a nested complex type Address in the Employee class:

@FreeBuilder public interface Address { String getCity(); class Builder extends Address_Builder { } }

Now, FreeBuilder generates setter methods that take Address.Builder as an input together with Address type:

Address.Builder addressBuilder = new Address.Builder(); addressBuilder.setCity(CITY_NAME); Employee employee = builder.setName("baeldung") .setAddress(addressBuilder) .build();

Notably, FreeBuilder also adds a method to customize the existing Address object in the Employee:

Employee employee = builder.setName("baeldung") .setAddress(addressBuilder) .mutateAddress(a -> a.setPinCode(112200)) .build();

Along with FreeBuilder types, FreeBuilder also allows nesting of other builders such as protos.

10. Building Partial Object

As we've discussed before, FreeBuilder throws an IllegalStateException for any constraint violation — for instance, missing values for mandatory fields.

Although this is desired for production environments, it complicates unit testing that is independent of constraints in general.

To relax such constraints, FreeBuilder allows us to build partial objects:

Employee employee = builder.setName("baeldung") .setAge(10) .setEmail("[email protected]") .buildPartial(); assertNotNull(employee.getEmail());

So, even though we haven't set all the mandatory fields for an Employee, we could still verify that the email field has a valid value.

11. Custom toString() Method

With value objects, we often need to add a custom toString() implementation. FreeBuilder allows this through abstract classes:

@FreeBuilder public abstract class Employee { abstract String getName(); abstract int getAge(); @Override public String toString() { return getName() + " (" + getAge() + " years old)"; } public static class Builder extends Employee_Builder{ } }

We declared Employee as an abstract class rather than an interface and provided a custom toString() implementation.

12. Comparison with Other Builder Libraries

L'implémentation du générateur dont nous avons discuté dans cet article est très similaire à celles de Lombok, Immutables ou de tout autre processeur d'annotations. Cependant, il existe quelques caractéristiques distinctives dont nous avons déjà discuté:

    • Méthodes du mappeur
    • Types de build imbriqués
    • Objets partiels

13. Conclusion

Dans cet article, nous avons utilisé la bibliothèque FreeBuilder pour générer une classe de générateur en Java. Nous avons implémenté diverses personnalisations d'une classe de constructeur à l'aide d'annotations, réduisant ainsi le code standard requis pour son implémentation .

Nous avons également vu en quoi FreeBuilder est différent de certaines des autres bibliothèques et avons brièvement discuté de certaines de ces caractéristiques dans cet article.

Tous les exemples de code sont disponibles à l'adresse over sur GitHub.