Patrón creacional para construcción de objetos
Este patrón es muy sencillo, pero creo que tiene mucho potencial para mejorar la lectura del código y su mantenimiento. Empecemos viendo un posible constructor.
public User(final String passport, final String name,
final String lastName, final String email,
final String school) {
this.passport = passport;
this.name = name;
this.lastName = lastName;
this.email = email;
this.school = school;
}
Tenemos que la clase usuario tiene 5 parámetros, no son muchos, pero todos son del mismo tipo, y es muy sencillo equivocarse, seguramente alguna vez te ha ocurrido.
Por otro lado, suponiendo que no todos son obligatorios, podríamos crear varios constructores, empezando por atributos obligatorios y acabando por los no obligatorios, de forma que se pueda elegir cual de ellos utilizar.
Además, podemos aprovechar para definir valores por defecto.
public User(final String passport, final String name) {
this.passport = passport;
this.name = name;
this.school = "<None>";
}
public User(final String passport, final String name,
final String lastName, final String email) {
this.passport = passport;
this.name = name;
this.lastName = lastName;
this.email = email;
}
public User(final String passport, final String name,
final String lastName, final String email,
final String school) {
this.passport = passport;
this.name = name;
this.lastName = lastName;
this.email = email;
this.school = school;
}
A esto se le llama Telecosping constructor pattern. El principal problema de hacer esto es que no escala correctamente, y por cada parámetro nuevo que queramos añadir, tendremos que añadir otro constructor.
Otra solución es usar el patrón JavaBeans.
private String passport;
private String name;
private String lastName;
private String email;
private String school;
public User() {
}
....
final User user = new User();
user.setPassport("passport");
user.setName("name");
user.setLastName("lastName");
user.setEmail("email");
user.setSchool("school");
Usando este patrón vemos claramente los valores, y donde los estamos seteando, pero sin embargo, queda muy redundante estar haciendo user.
, y también podemos tener al objeto en estados inconsistentes. Principalmente porque lo podemos crear con atributos obligatorios vacíos, lo cual podría llevar a situaciones no deseadas durante el desarrollo.
La solución a todo esto, es usar el patrón Builder, primero veamos como sería el resultado final.
final User user = User.builder()
.passport("passport")
.name("name")
.lastName("lastName")
.email("email")
.school("school")
.build();
Como podemos ver el builder:
Es más legible como los setter en el patrón JavaBeans
No deja un estado inconsistente porque hasta que llamamos al método build() no creamos el objeto User
Gracias al punto 2 podemos crear objetos inmutables
Para crear el patrón Builder, tendremos que tener una innerclase dentro de nuestra clase User, en esa clase tendremos métodos que irán seteando en el builder los atributos, y devolviéndose asimismo. Gracias a eso, podemos concatenar las llamadas y tener la construcción del objeto agrupada.
Aunque también podríamos tener estados intermedios guardando el Builder.
final User.Builder userBuilder = User.builder()
.passport("passport")
.name("name");
if(isAdult()) {
userBuilder.lastName("lastName")
.email("email")
.school("school");
}
final User user = userBuilder.build();
Así quedaría la clase Builder.
public static class Builder {
private String passport;
private String name;
private String lastName;
private String email;
//Default value
private String school = "<None>";
public Builder passport(final String passport) {
this.passport = passport;
return this;
}
public Builder name(final String name) {
this.name = passport;
return this;
}
public Builder lastName(final String lastName) {
this.lastName = lastName;
return this;
}
public Builder email(final String email) {
this.email = email;
return this;
}
public Builder school(final String school) {
this.school = school;
return this;
}
public User build() {
return new User(this);
}
}
Que pasa si queremos tener una subclase de User (Employee), y queremos usar el patrón Builder en Employee, tenemos que tener en cuenta que también vamos a querer setear los parámetros de User al crear un Employee.
Antes de mostrar un ejemplo, te voy a comentar el principal problema que vamos a tener.
Y es que cuando tengamos que crear Employee, y vayamos a setear un atributo del padre (por ejemplo passport), nos devolverá el Builder del padre, y ya no tendremos acceso a los atributos del hijo.
Employee employee = Employee.builder()
.salary(2.2f) //Funcionaría
.passport("passport")
.name("name")
.lastName("lastName")
.email("email")
.school("school")
.salary(2.2f) //Nos daría error por estar detras de school
.build();
Obviamente queremos poder acceder a todos los atributos en todas las situaciones, y para eso la solución es modificar lo que devuelve el padre, diciéndole que devuelve un Builder que hereda de él, usando a su vez un método que podemos llamar self().
public static class Builder<T extends UserManual.Builder<T>> {
public T passport(final String passport) {
this.passport = passport;
return self();
}
...
public T self() {
return (T) this;
}
}
//Employee
public static class Builder extends UserManual.Builder<Builder> {
private float salary;
public Builder salary(final float salary) {
this.salary = salary;
return this;
}
@Override
public EmployeeManual build() {
return new EmployeeManual(this);
}
@Override
public Builder self() {
return this;
}
}
Así podremos usar los atributos del padre o del hijo según queramos, el único punto feo, es que estamos haciendo un casting en el padre (T) this.
Eso aún podría solucionarse, pero considero que aporta poco y queda más enrevesado que simplemente castear el padre. Aunque si la clase fuera abstracta, no sería necesario implementar el método self(), y ya tendríamos la solución al problema.
Por último, quiero comentar que este patrón no es necesario hacerlo a mano (salvo excepciones especificas), puedes usar Lombok, lo cual simplifica mucho el uso de este patrón.
Para una clase cualquiera puedes usar la anotación @builder y para herencia debes usar la anotación @SuperBuilder en todas las clases (Superclase y Subclase).
@Builder
public class User {
...
}
@SuperBuilder
public class User {
...
}
@SuperBuilder
public class Employee {
...
}
Referencias
Joshua Bloch, Effective Java (3ª edición), Addison-Wesley, 2018.
Top comments (0)