Design Patterns - Builder

TLDR: El builder es un patrón de diseño creacional que te permite separar la construcción de un objeto completo desde su representación, así que, el mismo proceso de construcción puede crear diferentes representaciones.


¿Porque necesitamos el builder?

Algunos objetos son simples y pueden ser creados con una simple llamada de inicialización; pero en algunos casos la inicialización del objeto puede ser muy larga y se requiere construir el objeto en etapas, causando que esto pueda tomar tiempo y complicar las cosas.

Entonces, si se tiene un caso en que los argumentos de la inicialización del objeto son muchos, digamos más de 10 parámetros, esta actividad no es productiva.

En este caso, lo que se haría es separar construcción del objeto en representaciones llamadas Builders, que te permitirán sacar toda esa complejidad y variaciones de la construcción del objeto, así que, al momento de realizar el proceso de construcción, se podrán usar diferentes representaciones y crear el objeto deseado.

En definición, ¿que es el patrón Builder?

Permitir separar la construcción de un objeto completo desde su representación, así que, en el mismo proceso de construcción podrás crear diferentes representaciones.

El problema en un ejemplo

Imaginemos que estamos trabajando con algo relacionado con la construcción de casas y tenemos esta clase House y su constructor recibe varios parámetros

class House {
  constructor(materials, withGarage, withSwimmingPool, withGarden) {
    this.materials = materials;
    this.withGarage = withGarage;
    this.withSwimmingPool = withSwimmingPool;
    this.withGarden = withGarden;
  }

  buildHouse() {
    console.log(`Construyendo casa con: 
            Parqueaero  ${this.withGarage}
            Piscina ${this.withSwimmingPool}
            Garaje  ${this.withGarden}
        `);

    buildWalls(); // Construir Parades
    buildFloor(); // Construir Piso
    buildRoof(); // Construir Techo
    buildDoors(); // Construir Puerta
    // ...
  }
}

let casaConGaraje = new House("Madera", true, null, null);

casaConGaraje.buildHouse();
//     Construyendo casa con:
//            Garaje  true
//            Piscina null
//            Garaje  null

let casaConGarajeJardin = new House("Madera", true, null, true);

casaConGarajeJardin.buildHouse();
//     Construyendo casa con:
//            Garaje  true
//            Piscina null
//            Garaje  true

En la implementación de esta clase, como se puede ver, encontramos una falencia y es que funciona bien para un tipo de especificación sencilla, pero, en caso de que se quisiéramos tener una casa más grande, con patio trasero o con varias combinaciones tendríamos que modificar la clase House para incluir estos parámetros.

Al final nos encontraríamos con varios parámetros sin usar y haciendo que el constructor se vuelva difícil de manejar, adicionalmente el uso de la creación del objeto se vuelve muy dependiente a la documentación (Para entender que parámetro hace referencia a que combinación) sin ser auto descriptivo.

Una de las soluciones para esta necesidad sería comenzar a definir varios tipos de casas con sus especificaciones extendiendo la clase House y creando una jerarquía de subclases para cubrir todas las combinaciones de parámetros, pero esta solución eventualmente nos generará un considerable número de subclases.

Lo que el patrón sugiere es que extraigas todo el código de la construcción del objeto de su clase y lo muevas a un objeto separado de tipo Builders normalmente llamados Constructores Concretos, organizando la construcción del objeto en pasos, pero no es necesario invocar todos los pasos, solamente se llaman los pasos que son necesarios para producir una configuración en particular.

Ahora veamos un ejemplo de esto. En la clase abstracta Builder se declararán todos los pasos que son comunes en todos los Builders

class Builder {
  buildPart() {
    /* abstract */
  }
  getResults() {
    /* abstract */
  }
}

class House {
  /* Espacio para el objeto casa */
  constructor() {
    this.walls = `Sin definir`;
    this.floor = `Sin definir`;
    this.roof = `Sin definir`;
    this.doors = 0;
  }
}

Listo, ahora que tenemos la clase padre, crearemos las construcciones concretas que requerimos. Para eso vamos a crear 2 builders, la clase HouseWithGarageBuilder y HouseLuxuryBuilders que heredarán los pasos de su clase padre Builder y se agregarán en estas clases todos los pasos de esta implementación en especifico.

class HouseWithGarageBuilders extends Builder {
  buildPart() {
    this.house = new House();
    this.buildWalls();
    this.buildFloor();
    this.buildRoof();
    this.buildDoors();

    this.buildGarage();
  }

  buildWalls() {
    this.house.walls = `paredes blancas`;
  }
  buildFloor() {
    this.house.floor = `piso de madera`;
  }
  buildRoof() {
    this.house.roof = `techo en flecha`;
  }
  buildDoors() {
    this.house.doors = 2;
  }
  buildGarage() {
    this.house.garage = `con garaje`;
  }

  getResults() {
    return this.house;
  }
}

class HouseLuxuryBuilders extends Builder {
  buildPart() {
    this.house = new House();
    this.buildWalls();
    this.buildFloor();
    this.buildRoof();
    this.buildDoors();

    // Pasos especificos para HouseLuxuryBuilders
    this.buildGarage();
    this.buildSwimmingPool();
    this.buildGarden();
  }

  buildWalls() {
    this.house.walls = `paredes blancas en veneciano`;
  }
  buildFloor() {
    this.house.floor = `piso de marmol`;
  }
  buildRoof() {
    this.house.roof = `techo en flecha con atico`;
  }
  buildDoors() {
    this.house.doors = 4;
  }
  buildGarage() {
    this.house.garage = `con garaje doble`;
  }
  buildSwimmingPool() {
    this.house.swimmingPool = `con piscina`;
  }
  buildGarden() {
    this.house.garden = `con jardin`;
  }

  getResults() {
    return this.house;
  }
}

Ahora que ya tenemos todas las especificaciones que necesitamos por el momento, podemos crear el “pegamento” que será la clase Director. El constructor de la clase Director recibirá una instancia de alguno de los 2 builders que hemos creado y extienden de Builder, como lo veremos en el siguiente código:

class Director {
  construct(builder) {
    console.log(`Orquestando las operaciones de construcción`);

    builder.buildPart();
    return builder.getResults();
  }
}

// Creamos una instancia del director
let director = new Director();

// E instanciamos el Builder que queremos crear
let luxHouse = new HouseLuxuryBuilders();

// Pasamos el builder al metodo construct, obteniendo el nuevo objeto requerido.
let newLuxHouse = director.construct(luxHouse);
console.log(`Casa Lujosa:: ${JSON.stringify(newLuxHouse)}`);
// Casa Lujosa::
// {
//    "walls":"paredes blancas en veneciano",
//    "floor":"techo en flecha con atico",
//    "doors":4,
//    "garage":"con garaje doble",
//    "swimmingPool":"con piscina",
//    "garden":"con jardin"
// }

// Instanciamos el otro Builder que creamos.
let withGarageHouse = new HouseWithGarageBuilders();
let newWithGarageHouse = director.construct(withGarageHouse);

console.log(`Casa con Garaje:: ${JSON.stringify(newWithGarageHouse)}`);
// Casa con Garaje::
// {
//    "walls":"paredes blancas",
//    "floor":"techo en flecha",
//    "doors":2,
//    "garage":"con garaje"
// }

Para usar nuestra implementación, hemos creado 2 tipos de casas; la primera una casa lujosa (Luxury) con varias características a este tipo de casas con especificaciones adicionales y la segunda es una casa sencilla con garaje. Ambas instancias de los builders se pasaron a la misma instancia de la clase director por medio del método construct() retornando el objeto House para su posterior uso.

Actualmente, hay una alternativa para este patrón y es usando FluentInterface, (Interfaz fluida) que es una Construcción orientada a objeto y ayuda que la construcción del objeto sea más intuitiva. Para esto vamos a reutilizar la clase House y implementaremos una clase llamada HouseBuilder como lo veremos a continuación:

class HouseBuilder {
  constructor(house = new House()) {
    this.house = house;
  }

  walls(walls) {
    this.house.walls = walls;
    return this; // Fluent Interface
  }

  floor(floor) {
    this.house.floor = floor;
    return this; // Fluent Interface
  }

  roof(roof) {
    this.house.roof = roof;
    return this; // Fluent Interface
  }

  doors(doors) {
    this.house.doors = doors;
    return this; // Fluent Interface
  }

  garage(garage) {
    this.house.garage = garage;
    return this; // Fluent Interface
  }

  swimmingPool(swimmingPool) {
    this.house.swimmingPool = swimmingPool;
    return this;
  }
  garden(garden) {
    this.house.garden = garden;
    return this;
  }

  build() {
    return this.house;
  }
}

En esta clase, encontramos todas las propiedades que necesitan ser agregadas al proceso de construcción del objeto. El objeto House será instanciado en el constructor, y cada uno de los métodos asignarán la propiedad que se pasará y se retornará this que (Que es el objeto que creamos en el constructor). Y para retornar la información usaremos el método build().

Para usar el nuevo acercamiento, vamos a construir una casa con varias especificaciones como se detalla en el siguiente código, y finalmente vamos a ejecutar la función build() para que el objeto House sea retornado.

let houseBuilder = new HouseBuilder();
let house = houseBuilder
  .floor("piso de marmol")
  .roof("techo en plancha")
  .doors(3)
  .garage("con garaje doble")
  .swimmingPool("Con piscina de 30x30")
  .build();
console.log(house);
/*
House {
  floor: 'piso de marmol',
  walls: 'Sin definir',
  doors: 3,
  roof: 'techo en plancha',
  garage: 'con garaje doble',
  swimmingPool: 'Con piscina de 30x30'
}
*/

Usando FluentInterface podemos construir objetos de una forma fluida y dinámica, ya que podemos invocar a las funciones que necesitemos de acuerdo con las necesidades del objeto.

Código completo del ejemplo: https://bit.ly/3fXkqpG

Código completo del ejemplo usando FluentInterface: https://bit.ly/3g1CkaJ

Estructura del patrón builder

En este patrón encontramos varios componentes que interactuaran entre si.

  1. Inicialmente declaramos la clase padre Builder, que es una interfaz que definirá todos los pasos que serán comunes en todos los Builders.
  2. Después creamos los Concrete Builders o los Constructores en concreto (Builders) como los hemos llamado, que tendrán diferentes implementaciones de la construcción por pasos, ej: buildWalls(), buildFloor(), etc.
  3. Eso retornará el Producto en sí, al que nosotros llamamos el objeto House, que es el producto construido por diferentes builders pero no pertenece a la misma jerarquía que estos.
  4. Después tendremos al Director, que es una clase que define el orden en el que se llamaran los pasos del constructor, en este caso solo usamos 2 pasos buildPart() y getResults(), en el que podemos crear o reutilizar configuraciones en especifico de los productos.
  5. Y por último tenemos al cliente o la implementación en el que se asociarán los objetos builder con el director. Esta asociación se puede realizar vía parámetros en el constructor del objeto Director lo que permitiría reutilizarlo en futuras construcciones, o crear un método (como en el caso de nuestro ejemplo) construct(builder) en el que se reciben los builders a utilizar.

Conclusiones

  • Un builder, es un componente separado creado para la construcción de un objeto.
  • Puede dar un al builder un inicializador o retornar una función estática.
  • Se puede crear un builder fluent (Return self)
  • Diferentes facetas de un objeto pueden ser construidas con diferentes builder trabajando en tándem vía una clase base.

Ejercicio

Se requiere que se construya una solución para la creación de un computador gaming con las siguientes especificaciones:

  • videoCard: ‘MSI Nvidia geforce RTX 2070 super ventus 8GB’
  • diskDrive: ‘SSD Kingston a400 480GB’
  • ram: ‘Hyperx 32GB 3200MHz hyperx fury’
  • processor: ‘INTEL CORE I9 9900K 8/16 3.6GHZ 5.0GHZ 16MB’
  • board: ‘Gigabyte Aorus z390 elite’
  • peripherals: ‘Mouse y Teclado RAZER’
  • computerScreen: “27’ ASUS DESIGNO Ref:MZ27AQL Res:2560X1440 IPS

Se requiere crear una solución aplicando el patrón builder, usando la variación de FluentInterface, para poder crear el objeto Computer con todas las especificaciones requeridas.

Estructura Inicial

Solución esperada

Recursos adicionales y bibliografia

Alexander Shvets - Dive into design Patterns

Erich Gamma, Richard Helm, Ralph Johnson, John M. Vlissides - Design Patterns Elements of Reusable Object-Oriented Software (1994)

Addy Osmani - Learning JavaScript Design Patterns (2012)