Automatische Generierung des Builder-Musters mit FreeBuilder

1. Übersicht

In diesem Tutorial verwenden wir die FreeBuilder-Bibliothek, um Builder-Klassen in Java zu generieren.

2. Builder-Entwurfsmuster

Builder ist eines der am häufigsten verwendeten Creation Design Patterns in objektorientierten Sprachen. Es abstrahiert die Instanziierung eines komplexen Domänenobjekts und bietet eine fließende API zum Erstellen einer Instanz. Es hilft dabei, eine prägnante Domänenschicht aufrechtzuerhalten.

Trotz seiner Nützlichkeit ist die Implementierung eines Builders im Allgemeinen komplex, insbesondere in Java. Noch einfachere Wertobjekte erfordern viel Boilerplate-Code.

3. Builder-Implementierung in Java

Bevor wir mit FreeBuilder fortfahren, implementieren wir einen Boilerplate Builder für unsere Employee- Klasse:

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

Und eine innere Builder- Klasse:

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

Dementsprechend können wir jetzt den Builder zum Instanziieren des Employee- Objekts verwenden:

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

Wie oben gezeigt, ist für die Implementierung einer Builder-Klasse viel Boilerplate-Code erforderlich.

In den späteren Abschnitten werden wir sehen, wie FreeBuilder diese Implementierung sofort vereinfachen kann.

4. Maven-Abhängigkeit

Um die FreeBuilder-Bibliothek hinzuzufügen, fügen wir die FreeBuilder Maven-Abhängigkeit in unsere pom.xml ein :

 org.inferred freebuilder 2.4.1 

5. FreeBuilder- Anmerkung

5.1. Builder generieren

FreeBuilder ist eine Open-Source-Bibliothek, mit der Entwickler beim Implementieren von Builder-Klassen den Boilerplate-Code vermeiden können. Es verwendet die Annotationsverarbeitung in Java, um eine konkrete Implementierung des Builder-Musters zu generieren.

Wir werden unsere Employee- Klasse aus dem vorherigen Abschnitt mit @ FreeBuilder kommentieren und sehen, wie sie automatisch die Builder-Klasse generiert:

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

Es ist wichtig darauf hinzuweisen, dass Employee jetzt eher eine Schnittstelle als eine POJO-Klasse ist. Darüber hinaus enthält es alle Attribute eines Employee- Objekts als Methoden.

Bevor wir diesen Builder weiter verwenden, müssen wir unsere IDEs konfigurieren, um Kompilierungsprobleme zu vermeiden. Da FreeBuilder die Employee_Builder- Klasse während der Kompilierung automatisch generiert , beschwert sich die IDE normalerweise über ClassNotFoundException in Zeile 8 .

Um solche Probleme zu vermeiden, müssen wir die Annotationsverarbeitung in IntelliJ oder Eclipse aktivieren . Dabei verwenden wir den Anmerkungsprozessor org.inferred.freebuilder.processor.Processor von FreeBuilder. Darüber hinaus sollte das zum Generieren dieser Quelldateien verwendete Verzeichnis als Stamm generierter Quellen markiert sein.

Alternativ können wir auch mvn install ausführen , um das Projekt zu erstellen und die erforderlichen Builder-Klassen zu generieren.

Schließlich haben wir unser Projekt kompiliert und können jetzt die Employee.Builder- Klasse verwenden:

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

Alles in allem gibt es zwei Hauptunterschiede zwischen dieser und der Builder-Klasse, die wir zuvor gesehen haben. Zuerst müssen wir den Wert für alle Attribute der Employee- Klasse festlegen . Andernfalls wird eine IllegalStateException ausgelöst .

Wir werden in einem späteren Abschnitt sehen, wie FreeBuilder mit optionalen Attributen umgeht.

Zweitens folgen die Methodennamen von Employee.Builder nicht den JavaBean-Namenskonventionen. Wir werden dies im nächsten Abschnitt sehen.

5.2. JavaBean-Namenskonvention

Um zu erzwingen, dass FreeBuilder der JavaBean-Namenskonvention folgt, müssen wir unsere Methoden in Employee umbenennen und den Methoden get voranstellen :

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

Dadurch werden Getter und Setter generiert, die der JavaBean-Namenskonvention folgen:

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

5.3. Mapper-Methoden

In Verbindung mit Gettern und Setzern fügt FreeBuilder der Builder-Klasse auch Mapper-Methoden hinzu. Diese Mapper-Methoden akzeptieren einen UnaryOperator als Eingabe, sodass Entwickler komplexe Feldwerte berechnen können.

Angenommen, unsere Mitarbeiterklasse hat auch ein Gehaltsfeld:

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

Angenommen, wir müssen die Währung des Gehalts eingeben, das als Eingabe bereitgestellt wird:

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 bietet solche Mapper-Methoden für alle Felder.

6. Standardwerte und Einschränkungsprüfungen

6.1. Festlegen von Standardwerten

Die bisher diskutierte Employee.Builder- Implementierung erwartet, dass der Client Werte für alle Felder übergibt. Tatsächlich schlägt der Initialisierungsprozess mit einer IllegalStateException fehl, wenn Felder fehlen.

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

Die in diesem Artikel beschriebene Builder-Implementierung ist der von Lombok, Immutables oder einem anderen Annotation-Prozessor sehr ähnlich. Es gibt jedoch einige Unterscheidungsmerkmale , die wir bereits besprochen haben:

    • Mapper-Methoden
    • Verschachtelte baubare Typen
    • Teilobjekte

13. Schlussfolgerung

In diesem Artikel haben wir die FreeBuilder-Bibliothek verwendet, um eine Builder-Klasse in Java zu generieren. Wir haben verschiedene Anpassungen einer Builder-Klasse mithilfe von Anmerkungen implementiert und so den für die Implementierung erforderlichen Boilerplate-Code reduziert .

Wir haben auch gesehen, wie sich FreeBuilder von einigen anderen Bibliotheken unterscheidet, und einige dieser Eigenschaften in diesem Artikel kurz besprochen.

Alle Codebeispiele sind auf GitHub verfügbar.