Generazione automatica del pattern Builder con FreeBuilder

1. Panoramica

In questo tutorial, useremo la libreria FreeBuilder per generare classi builder in Java.

2. Builder Design Pattern

Builder è uno dei modelli di creazione di design più utilizzati nei linguaggi orientati agli oggetti. Esso astrae l'istanza di un oggetto dominio complesso e fornisce un'API fluente per la creazione di un'istanza. In tal modo aiuta a mantenere uno strato di dominio conciso.

Nonostante la sua utilità, un builder è generalmente complesso da implementare, in particolare in Java. Anche gli oggetti valore più semplici richiedono molto codice boilerplate.

3. Implementazione del generatore in Java

Prima di procedere con FreeBuilder, implementiamo un generatore boilerplate per la nostra 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; } }

E una classe Builder interna :

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); } }

Di conseguenza, ora possiamo utilizzare il builder per istanziare l' oggetto Employee :

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

Come mostrato sopra, è necessario molto codice boilerplate per implementare una classe builder.

Nelle sezioni successive, vedremo come FreeBuilder può semplificare istantaneamente questa implementazione.

4. Dipendenza da Maven

Per aggiungere la libreria FreeBuilder, aggiungeremo la dipendenza Maven di FreeBuilder nel nostro pom.xml :

 org.inferred freebuilder 2.4.1 

5. Annotazione FreeBuilder

5.1. Generazione di un costruttore

FreeBuilder è una libreria open source che aiuta gli sviluppatori a evitare il codice boilerplate durante l'implementazione delle classi builder. Utilizza l'elaborazione delle annotazioni in Java per generare un'implementazione concreta del pattern builder.

Ci annotare il dipendente di classe della sezione precedente con @ FreeBuilder e vedere come si genera automaticamente la classe costruttore:

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

È importante sottolineare che Employee ora è un'interfaccia piuttosto che una classe POJO. Inoltre, contiene tutti gli attributi di un oggetto Employee come metodi.

Prima di continuare a utilizzare questo builder, dobbiamo configurare i nostri IDE per evitare problemi di compilazione. Poiché FreeBuilder genera automaticamente la classe Employee_Builder durante la compilazione, l'IDE di solito lamenta ClassNotFoundException sulla riga numero 8 .

Per evitare tali problemi, è necessario abilitare l'elaborazione delle annotazioni in IntelliJ o Eclipse . E mentre lo facciamo, useremo il processore di annotazioni di FreeBuilder org.inferred.freebuilder.processor.Processor. Inoltre, la directory utilizzata per generare questi file di origine dovrebbe essere contrassegnata come Radice delle origini generate.

In alternativa, possiamo anche eseguire mvn install per costruire il progetto e generare le classi builder richieste.

Infine, abbiamo compilato il nostro progetto e ora possiamo utilizzare la classe Employee.Builder :

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

Tutto sommato, ci sono due differenze principali tra questa e la classe builder che abbiamo visto prima. Innanzitutto, dobbiamo impostare il valore per tutti gli attributi della classe Employee . In caso contrario, genera un'eccezione IllegalStateException .

Vedremo come FreeBuilder gestisce gli attributi opzionali in una sezione successiva.

In secondo luogo, i nomi dei metodi di Employee.Builder non seguono le convenzioni di denominazione JavaBean. Lo vedremo nella prossima sezione.

5.2. Convenzione di denominazione JavaBean

Per imporre a FreeBuilder di seguire la convenzione di denominazione JavaBean, dobbiamo rinominare i nostri metodi in Employee e anteporre ai metodi get :

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

Questo genererà getter e setter che seguono la convenzione di denominazione JavaBean:

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

5.3. Metodi di mappatura

Insieme a getter e setter, FreeBuilder aggiunge anche metodi mapper nella classe builder. Questi metodi mapper accettano un UnaryOperator come input, consentendo così agli sviluppatori di calcolare valori di campo complessi.

Supponiamo che la nostra classe Employee abbia anche un campo stipendio:

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

Supponiamo ora di dover convertire la valuta dello stipendio fornito come input:

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 fornisce questi metodi di mappatura per tutti i campi.

6. Valori predefiniti e controlli dei vincoli

6.1. Impostazione dei valori predefiniti

L' implementazione Employee.Builder di cui abbiamo discusso finora prevede che il client passi i valori per tutti i campi. È un dato di fatto, fallisce il processo di inizializzazione con un IllegalStateException in caso di campi mancanti.

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'implementazione del builder di cui abbiamo discusso in questo articolo è molto simile a quelle di Lombok, Immutables o qualsiasi altro processore di annotazioni. Tuttavia, ci sono alcune caratteristiche distintive che abbiamo già discusso:

    • Metodi di mappatura
    • Tipi costruibili annidati
    • Oggetti parziali

13. Conclusione

In questo articolo, abbiamo utilizzato la libreria FreeBuilder per generare una classe builder in Java. Abbiamo implementato varie personalizzazioni di una classe builder con l'aiuto di annotazioni, riducendo così il codice boilerplate richiesto per la sua implementazione .

Abbiamo anche visto come FreeBuilder è diverso da alcune delle altre librerie e abbiamo discusso brevemente alcune di queste caratteristiche in questo articolo.

Tutti gli esempi di codice sono disponibili su GitHub.