Автоматично генериране на шаблона на Builder с FreeBuilder

1. Общ преглед

В този урок ще използваме библиотеката FreeBuilder, за да генерираме строителни класове в Java.

2. Шаблон за проектиране на Builder

Builder е един от най-широко използваните модели за създаване на дизайн в обектно-ориентирани езици. Той абстрахира екземпляра на сложен домейн обект и осигурява плавен API за създаване на екземпляр. По този начин помага да се поддържа кратък домейн слой.

Въпреки полезността си, конструкторът обикновено е сложен за изпълнение, особено в Java. Дори по-опростените обекти на стойност изискват много шаблонни кодове.

3. Внедряване на Builder в Java

Преди да пристъпим към FreeBuilder, нека внедрим конструктор на шаблони за нашия клас 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; } }

И вътрешен клас на Builder :

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

Съответно, вече можем да използваме конструктора за създаване на екземпляр на обекта Employee :

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

Както е показано по-горе, за въвеждането на клас на строител е необходим много типов код.

В следващите раздели ще видим как FreeBuilder може незабавно да опрости това изпълнение.

4. Зависимост на Maven

За да добавим библиотеката FreeBuilder, ще добавим зависимостта FreeBuilder Maven в нашия pom.xml :

 org.inferred freebuilder 2.4.1 

5. Анотация на FreeBuilder

5.1. Генериране на Builder

FreeBuilder е библиотека с отворен код, която помага на разработчиците да избягват кода на шаблона, докато внедряват класове на builder. Той използва обработката на анотации в Java, за да генерира конкретна реализация на шаблона на конструктора.

Ще анотираме нашия клас Employee от по-ранната секция с @ FreeBuilder и ще видим как той автоматично генерира класа на builder:

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

Важно е да се отбележи, че служителят вече е интерфейс, а не клас POJO. Освен това, той съдържа всички атрибути на обект на служител като методи.

Преди да продължим да използваме този конструктор, трябва да конфигурираме нашите IDE, за да избегнем проблеми с компилацията. Тъй като FreeBuilder автоматично генерира клас Employee_Builder по време на компилация, IDE обикновено се оплаква от ClassNotFoundException на ред номер 8 .

За да избегнем подобни проблеми, трябва да активираме обработката на анотации в IntelliJ или Eclipse . И докато правим това, ще използваме процесора за анотиране на FreeBuilder org.inferred.freebuilder.processor.Processor. Освен това директорията, използвана за генериране на тези изходни файлове, трябва да бъде маркирана като генериран корен на източници.

Като алтернатива можем също да изпълним mvn install, за да изградим проекта и да генерираме необходимите строителни класове.

И накрая, ние компилирахме нашия проект и вече можем да използваме класа Employee.Builder :

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

Като цяло има две основни разлики между това и класа на строителите, които видяхме по-рано. Първо, трябва да зададем стойността за всички атрибути от класа Employee . В противен случай изхвърля IllegalStateException .

Ще видим как FreeBuilder обработва незадължителни атрибути в следващ раздел.

Второ, имената на методите на Employee.Builder не следват конвенциите за именуване на JavaBean. Ще видим това в следващия раздел.

5.2. Конвенция за имената на JavaBean

За да принудим FreeBuilder да следва конвенцията за именуване на JavaBean, трябва да преименуваме нашите методи в Employee и да добавим префиксите на методите с get :

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

Това ще генерира getters и setter, които следват конвенцията за именуване JavaBean:

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

5.3. Методи за картографиране

Заедно с getters и setters, FreeBuilder също добавя методи за картографиране в класа на builder. Тези методи за преобразуване приемат UnaryOperator като вход, като по този начин позволяват на разработчиците да изчисляват сложни полеви стойности.

Да предположим, че нашият клас Служител също има поле за заплата:

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

Сега да предположим, че трябва да конвертираме валутата на заплатата, която е предоставена като вход:

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 предоставя такива методи за картографиране за всички полета.

6. Стойности по подразбиране и проверки на ограничения

6.1. Задаване на стойности по подразбиране

В Employee.Builder изпълнението говорихме досега очаква клиентът да премине стойности за всички области. Всъщност той се проваля в процеса на инициализация с IllegalStateException в случай на липсващи полета.

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

Реализацията на компилатора, която обсъдихме в тази статия, е много подобна на тази на Lombok, Immutables или всеки друг процесор за анотиране. Има обаче няколко отличителни характеристики , които вече обсъдихме:

    • Методи за картографиране
    • Вложени вграждаеми типове
    • Частични обекти

13. Заключение

В тази статия използвахме библиотеката FreeBuilder, за да генерираме клас на builder в Java. Приложихме различни персонализации на клас на строител с помощта на анотации, като по този начин намалихме кода на шаблона, необходим за неговото изпълнение .

Също така видяхме как FreeBuilder се различава от някои други библиотеки и накратко обсъдихме някои от тези характеристики в тази статия.

Всички примери за кодове са достъпни в GitHub.