1. Información general
En este tutorial, usaremos la biblioteca FreeBuilder para generar clases de constructores en Java.
2. Patrón de diseño del constructor
Builder es uno de los patrones de diseño de creación más utilizados en lenguajes orientados a objetos. Se abstrae la instanciación de un objeto de dominio complejo y proporciona una API de fluidez para la creación de una instancia. Por tanto, ayuda a mantener una capa de dominio concisa.
A pesar de su utilidad, un constructor es generalmente complejo de implementar, particularmente en Java. Incluso los objetos de valor más simples requieren una gran cantidad de código repetitivo.
3. Implementación del constructor en Java
Antes de continuar con FreeBuilder, implementemos un generador repetitivo para nuestra clase de empleado :
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; } }
Y una clase de constructor interno :
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 consecuencia, ahora podemos usar el constructor para instanciar el objeto Empleado :
Employee.Builder emplBuilder = new Employee.Builder(); Employee employee = emplBuilder .setName("baeldung") .setAge(12) .setDepartment("Builder Pattern") .build();
Como se muestra arriba, se necesita mucho código repetitivo para implementar una clase de constructor.
En las secciones posteriores, veremos cómo FreeBuilder puede simplificar instantáneamente esta implementación.
4. Dependencia de Maven
Para agregar la biblioteca FreeBuilder, agregaremos la dependencia FreeBuilder Maven en nuestro pom.xml :
org.inferred freebuilder 2.4.1
5. Anotación de FreeBuilder
5.1. Generando un constructor
FreeBuilder es una biblioteca de código abierto que ayuda a los desarrolladores a evitar el código repetitivo al implementar clases de constructores. Utiliza el procesamiento de anotaciones en Java para generar una implementación concreta del patrón del constructor.
Vamos a anotamos nuestra Empleado clase de la sección anterior con @ FreeBuilder y ver cómo se genera automáticamente la clase constructor:
@FreeBuilder public interface Employee { String name(); int age(); String department(); class Builder extends Employee_Builder { } }
Es importante señalar que Employee ahora es una interfaz en lugar de una clase POJO. Además, contiene todos los atributos de un objeto Empleado como métodos.
Antes de continuar usando este constructor, debemos configurar nuestros IDE para evitar problemas de compilación. Dado que FreeBuilder genera automáticamente la clase Employee_Builder durante la compilación, el IDE generalmente se queja de ClassNotFoundException en la línea número 8 .
Para evitar estos problemas, debemos habilitar el procesamiento de anotaciones en IntelliJ o Eclipse . Y mientras lo hacemos, usaremos el procesador de anotaciones org.inferred.freebuilder.processor.Processor de FreeBuilder. Además, el directorio utilizado para generar estos archivos de origen debe estar marcado como Raíz de fuentes generadas.
Alternativamente, también podemos ejecutar mvn install para construir el proyecto y generar las clases de constructor requeridas.
Finalmente, hemos compilado nuestro proyecto y ahora podemos usar la clase Employee.Builder :
Employee.Builder builder = new Employee.Builder(); Employee employee = builder.name("baeldung") .age(10) .department("Builder Pattern") .build();
Con todo, hay dos diferencias principales entre esta y la clase de constructor que vimos anteriormente. Primero, debemos establecer el valor para todos los atributos de la clase Empleado . De lo contrario, arroja una IllegalStateException .
Veremos cómo FreeBuilder maneja los atributos opcionales en una sección posterior.
En segundo lugar, los nombres de método de Employee.Builder no siguen las convenciones de nomenclatura de JavaBean. Veremos esto en la siguiente sección.
5.2. Convención de nomenclatura de JavaBean
Para hacer que FreeBuilder siga la convención de nomenclatura de JavaBean, debemos cambiar el nombre de nuestros métodos en Empleado y prefijar los métodos con get :
@FreeBuilder public interface Employee { String getName(); int getAge(); String getDepartment(); class Builder extends Employee_Builder { } }
Esto generará getters y setters que siguen la convención de nomenclatura de JavaBean:
Employee employee = builder .setName("baeldung") .setAge(10) .setDepartment("Builder Pattern") .build();
5.3. Métodos del asignador
Junto con captadores y definidores, FreeBuilder también agrega métodos de mapeo en la clase de constructor. Estos métodos de mapeador aceptan un UnaryOperator como entrada, lo que permite a los desarrolladores calcular valores de campo complejos.
Supongamos que nuestra clase Empleado también tiene un campo de salario:
@FreeBuilder public interface Employee { Optional getSalaryInUSD(); }
Ahora suponga que necesitamos convertir la moneda del salario que se proporciona como entrada:
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 proporciona estos métodos de asignación para todos los campos.
6. Valores predeterminados y comprobaciones de restricciones
6.1. Establecer valores predeterminados
La implementación de Employee.Builder que hemos discutido hasta ahora espera que el cliente pase valores para todos los campos. De hecho, falla el proceso de inicialización con una IllegalStateException en caso de que falten campos.
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
La implementación del constructor que hemos discutido en este artículo es muy similar a las de Lombok, Immutables o cualquier otro procesador de anotaciones. Sin embargo, hay algunas características distintivas que ya hemos discutido:
-
- Métodos de mapeador
- Tipos edificables anidados
- Objetos parciales
13. Conclusión
En este artículo, usamos la biblioteca FreeBuilder para generar una clase de constructor en Java. Implementamos varias personalizaciones de una clase de constructor con la ayuda de anotaciones, reduciendo así el código repetitivo requerido para su implementación .
También vimos cómo FreeBuilder es diferente de algunas de las otras bibliotecas y discutimos brevemente algunas de esas características en este artículo.
Todos los ejemplos de código están disponibles en GitHub.