Cidenet+Blog+Desarrollo limpio+Builder

Builder

¿Qué es?, ¿Qué problema resuelve?

Imagina que debes inicializar un objeto complejo, compuesto por muchos campos y objetos anidados. Usualmente, tal inicialización se da por medio de constructores con una gran cantidad de parámetros o incluso peor, inicializaciones múltiples en diferentes lugares del código cliente.

El patrón Builder separa la construcción de un objeto complejo de su representación, de modo que se puedan crear diferentes representaciones bajo un mismo proceso de construcción. La construcción de estos objetos se realiza paso a paso.

Diagrama

  • Builder: especifica una interfaz abstracta para crear un objeto.
  • ConcreteBuilder:
      • Construye y ensambla partes de un producto implementando la interfaz Builder.
      • Provee una interfaz para obtener el objeto construído.
  • Director: construye un objeto usando la interfaz Builder.
  • Producto: representa al objeto complejo a construir.

Cómo aplicar Builder:

Aplicaremos el patrón Builder en una clase utilizada para filtrar registros de base de datos dinámicamente. En el Framework de Spring existe una interfaz llamada Specification, la cual, permite hacer consultas dinámicas en tiempo de ejecución. Para tal fin, crearemos una clase con todos los campos por los que puede filtrar la especificación y buscaremos crear diferentes combinaciones de estos campos, para poder filtrar por algunos campos y por otros no. En este ejemplo se asume que se tiene una vista por la que el usuario puede hacer una búsqueda de x objeto y en la vista se tienen filtros de búsqueda, los cuales son opcionales.

Asumimos que se envía una petición http con los filtros como parámetros de la petición. Estos parámetros se reciben en el servicio de back end y se hace la consulta de acuerdo a estos.

Primero se crea la interfaz que indica qué partes del producto debe construir cada ConcreteBuilder.

Con el método build se obtiene el producto construido.

public interface BookBuilder {
   BookBuilder setIsbn(String isbn);
   BookBuilder setName(String name);
   BookBuilder setAuthor(String author);
   BookBuilder setReleaseDate(Date date);
   Book build();
}

El Builder debe construir un objeto que cuenta con la siguiente estructura.

public class FilterBookBuilder implements BookBuilder {

   private String isbn;
   private String name;
   private String author;
   private Date releaseDate;

   @Override
   public BookBuilder setIsbn(String isbn) {
       this.isbn = isbn;
       return this;
   }

   @Override
   public BookBuilder setName(String name) {
       this.name = name;
       return this;
   }

   @Override
   public BookBuilder setAuthor(String author) {
       this.author = author;
       return this;
   }

   @Override
   public BookBuilder setReleaseDate(Date date) {
       this.releaseDate = date;
       return this;
   }

Nótese cómo en cada método implementado se retorna a la misma instancia inflada con la nueva información. Esto con el fin de poder construir el objeto de forma encadenada, paso a paso y sin importar el orden.

El método build, símplemente, retorna una instancia del objeto construído.

@Override
public Book build() {
   return new Book(this);
}

El constructor en la clase Book debe ser privado o de paquete para evitar crear instancias por fuera de los Builder.

Book(FilterBookBuilder filterBookBuilder) {
   this.isbn = filterBookBuilder.getIsbn();
   this.name = filterBookBuilder.getName();
   this.author = filterBookBuilder.getAuthor();
   this.releaseDate = filterBookBuilder.getReleaseDate();
}

Constructor de la clase Book

Nótese cómo en este constructor simplemente se copia la información del Builder al objeto a crear. Se debe tener en cuenta que, en este constructor, se pueden realizar validaciones, así como lo hacen en la clase Message que se usa para crear notificaciones push de Firebase Messaging.

private Message(Message.Builder builder) {
this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data);
   this.notification = builder.notification;
   this.androidConfig = builder.androidConfig;
   this.webpushConfig = builder.webpushConfig;
   this.apnsConfig = builder.apnsConfig;
int count = Booleans.countTrue(new boolean[]{!Strings.isNullOrEmpty(builder.token), !Strings.isNullOrEmpty(builder.topic), !Strings.isNullOrEmpty(builder.condition)});
   Preconditions.checkArgument(count == 1, "Exactly one of token, topic or condition must be specified");
   this.token = builder.token;
   this.topic = stripPrefix(builder.topic);
   this.condition = builder.condition;
   this.fcmOptions = builder.fcmOptions;
}

Constructor privado de la clase Message que cuenta con validaciones.

Con este modelo se pueden crear objetos dinámicos. Por ejemplo, un filtro que busca libros solo por nombre y por autor

@Test
  public void shouldBuildABookWithNameAndAuthor() {
      Book book = new FilterBookBuilder()
              .setName("Book 1")
              .setAuthor("Author 1")
              .build();
assertEquals("Book{isbn='null', name='Book 1', author='Author 1', releaseDate=null}", book.toString());
  }

O una búsqueda sólo por fecha de registro e isbn

@Test
public void shouldBuildABookWithIsbnAndReleaseDate() {
   Calendar calendar = Calendar.getInstance();
   calendar.set(2020, Calendar.FEBRUARY, 1, 1, 1, 1);
   Book book = new FilterBookBuilder()
           .setIsbn("aert6789")
           .setReleaseDate(calendar.getTime())
           .build();
   assertEquals(
           "Book{isbn='aert6789', name='null', author='null', releaseDate=Sat Feb 01 01:01:01 COT 2020}",
           book.toString()
   );
}

Esto permite tener solo un constructor para crear diferentes combinaciones de libros de acuerdo con parámetros variables. Si no se aplica esta estrategia, se debe crear un constructor por cada combinación posible como se muestra en la imagen 10.

public Book(String name, String author) {
   this.name = name;
   this.author = author;
}

public Book(String isbn, Date releaseDate) {
   this.isbn = isbn;
   this.releaseDate = releaseDate;
}

Clase Book con diferentes implementaciones del constructor

En este caso no es posible crear un constructor por cada combinación posible, ya que hay atributos que tienen un mismo tipo de dato, por ejemplo: name – author, name – isbn.

Otra ventaja de aplicar el patrón Builder es que si un nuevo campo debe ser agregado a los filtros, ya no se filtra, por ejemplo, por nombre – autor, sino que además se debe buscar por isbn. En el caso de no aplicar Builder se debe modificar el constructor ya creado o crear uno nuevo, mientras que aplicando el patrón Builder solo es necesario crear el campo en el objeto, y llenarlo a través del Builder, dejando así la lógica ya creada intacta.

Conclusiones:

  • Con este patrón se puede variar la representación interna del producto.
  • Se esconde la estructura interna del producto, así como la construcción del producto.
  • Como el producto se construye a través de una interfaz, lo que se debe hacer para cambiar la representación interna del producto es definir un nuevo tipo de Builder.
  • Se obtiene completo control sobre el proceso de construcción, ya que el producto se crea paso a paso bajo el control del Concrete Builder.
  • Con este patrón se gana en modularidad ya que la forma como se crea un objeto complejo es encapsulada. El cliente no necesita saber nada acerca de las clases que definen la estructura interna del producto.

El código de ejemplo se puede encontrar en https://bit.ly/2R57Ysv

Fuentes:

https://sourcemaking.com/design_patterns/Builder
https://refactoring.guru/design-patterns/Builder
https://medium.com/xebia-engineering/fluent-Builder-pattern-with-a-real-world-example-7b61be375a40
Design Patterns : Elements of Reusable Object-Oriented Software

Contáctanos

Déjanos tus datos

    Medellín - Colombia

  • Calle 47D #72-29
  • (+57) 4 3222567
  • comunicaciones@cidenet.com.co

    Estados Unidos

  • 1200 Colorado Blvd, Denver Colorado 80220
  • (+1) 7723619239
  • jceballos@cidenet.net
WhatsApp