Patrón Builder: bueno, bonito y ¿barato?

El patrón Builder nos permite crear representaciones diferentes de un mismo objeto, al mismo tiempo que centraliza el proceso de creación en un único punto. Este patrón es muy flexible, ofrece una forma sencilla y legible de crear un objeto, y se vuelve especialmente atractivo a la hora de trabajar con objetos inmutables que tienen varios campos opcionales.

El objetivo de este artículo es explicar el patrón Builder escribiendo un ejemplo completo. Para los que nunca lo habéis usado, creo que os resultará tan evidente como lógico. ¡Vamos a ello!

Tormenta de constructores

Estoy bastante seguro de que a la hora de diseñar vuestros objetos os habéis encontrado más de una vez escribiendo varios constructores para abastecer todas las necesidades de creación:

public final class Person {

  private final String firstName;
  private final String surname;
  private final String dni;
  private final Date birthDate;
  private final String address;
  private final Gender gender; //enum

  public Person(String firstName, String surname, String dni) {
    this(firstName, surname, dni, null);
  }

  public Person(String firstName, String surname, String dni, Date birthDate) {
    this(firstName, surname, dni, birthDate, null);
  }

  public Person(String firstName, String surname, String dni, Date birthDate, String address) {
    this(firstName, surname, dni, birthDate, address, null);
  }

  public Person(String firstName, String surname, String dni, Date birthDate, String address, Gender gender) {
    this.firstName = firstName;
    this.surname = surname;
    this.dni = dni;
    this.birthDate = birthDate;
    this.address = address;
    this.gender = gender;
  }

  //getters...

}

Esto se debe a que no siempre tenemos disponible toda la información a la hora de construir un objeto. El principal problema que tiene el código de arriba es puede volverse ininteligible de cara al cliente si el número de variables crece (y con ellas los constructores); podríamos, por ejemplo, llegar muy fácilmente al punto en que no sabemos qué constructor utilizar o a qué pertenece tal String o tal int.

El Builder le da armonía

Leed con atención las modificaciones realizadas sobre nuestra clase Person:

public final class Person {

  private final String firstName; //required
  private final String surname; //required
  private final String dni; //required
  private final Date birthDate; //optional
  private final String address; //optional
  private final Gender gender; //optional

  private Person(Builder builder) {
    this.firstName = builder.firstName;
    this.surname = builder.surname;
    this.dni = builder.dni;
    this.birthDate = builder.birthDate;
    this.address = builder.address;
    this.gender = builder.gender;
  }

  //getters...

  public static class Builder {

    private final String firstName;
    private final String surname;
    private final String dni;
    private Date birthDate;
    private String address;
    private Gender gender;

    public Builder(String firstName, String surname, String dni) {
      this.firstName = firstName;
      this.surname = surname;
      this.dni = dni;
    }

    public Builder birthDate(Date birthDate) {
      this.birthDate = birthDate;
      return this;
    }

    public Builder address(String address) {
      this.address = address;
      return this;
    }

    public Builder gender(Gender gender) {
      this.gender = gender;
      return this;
    }

    public Person build() {
      return new Person(this);
    }

  }

}

Ahora el código de construcción se localiza en la clase estática Builder. Sin embargo Person sigue manteniendo su inmutabilidad, y todos sus atributos son seteados en su constructor (que pasa a ser privado, y por lo tanto inaccesible para el cliente). Por otro lado, el constructor de Builder tan sólo recibe aquellos atributos requeridos declarados como final.

Con este cambio el código cliente se vuelve significativamente más fácil de escribir:

Person person = new Person.Builder("Juan", "Perez", "54545454D")
.birthDate(new Date())
.gender(Gender.MALE)
.build();

Concurrencia

Al igual que en un constructor, con el patrón Builder también podremos validar los parámetros del objeto para comprobar su estado.

public Person build() {
  Person person = new Person(this);
  if(person.dni.length() < 9) {
    throw new IllegalStateException("Incorrect DNI"); // thread-safe
  }
  return person;
}

Es fundamental que se validen después de copiar los parámetros del builder al objeto, y que se validen en los campos del objeto y no en los campos del builder. La razón de esto es que, como el builder no es thread-safe, si comprobamos los parámetros antes de crear el objeto, sus valores pueden ser cambiados por otro hilo entre el momento en que se comprueban los parámetros y el momento en que se copian.

Conclusión

El patrón Builder puede convertirse en una solución muy válida para aquellas clases que tienen un número importante de parámetros (y todavía más si dichas clases deben ser inmutables). De todas formas, no está de más recordar que este patrón implica la creación de un objeto extra, con lo cual incrementa también el gasto de memoria; de ahí que no se deba considerar un patrón de obligada implementación, sino una herramienta más a tener en cuenta para construir software legible y de calidad.

Enlaces de referencia