Builder Design Pattern

El patrón de diseño Builder

TLDR: “Este patrón de diseño 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 parametros


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}
        `);

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

        ...
    }
}

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 especificacion sencilla, pero, en caso de que se quisieramos tener una casa más grande, con patio trasero o con varias combinaciones tendriamos que modificar la clase House para incluir estos parametros.

Al final nos encontrariamos con varios parametros sin usar y haciendo que el constructor se vuelva dificil de manejar, adicionalmente el uso de la creación del objeto se vuelve muy dependiente a la documentación (Para entender que parametro hace referencia a que combinación) sin ser autodescriptivo.

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 parametros, 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.floor = `Sin definir`;
        this.doors = 0;
    }
}

Listo, ahora que tenemos la clase padre, crearemos las construcciónes 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.floor = `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(); 

        this.buildGarage();
        this.buildSwimmingPool();
        this.buildGarden();
    }

    buildWalls() {  
        this.house.walls = `paredes blancas en veneciano`;
    }

    buildFloor() {  
        this.house.floor = `piso de marmol`;    
     }

    buildRoof() {  
        this.house.floor = `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 caracteristicas 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 metodo construct() retornando el objeto House para su posterior uso.

Estructura del patrón builder

En este patrón encontramos varios componentes que interacturan 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. Despues 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. Despues 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 via parametros en el constructor del objeto Director lo que permitiría reutilizarlo en futuras construcciones, o crear un metodo (como en el caso de nuestor ejemplo) construct(builder) en el que se reciben los builders a utilizar.

TODO: Actualizar diagrama con las clases del ejemplo

By the Guf

Summary

  • 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 estatica.
  • Se puede crear un builder fluent (Return self)
  • Diferentes fasetas de un objeto pueden ser construidas con diferentes builder trabajando en tandem via una clase base.

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)