Abstract Factory

Abstract Factory

TLDR; Provee una interface para la creación de familias de objetos relacionados o dependientes sin especificar sus clases concretas.

Porque necesitamos el patrón Abstract Factory?

Un Absract Factory (fabrica abstracta) es un patrón de diseño que se encarga de crear objetos, y que a diferencia del Factory Method, estos están relacionados por un tema en común sin la necesidad de especificar la clase exacta de el objeto que será creado.

Encapsulando un grupo de factories con una meta en especifico y que puedas tener una jerarquía de objetos.

El problema en un ejemplo

En el siguiente ejemplo, se creará una clase base llamada HotDrink, pero como en Javascript no existen cosas como clases abstractas, la forma de implementarlo aquí es que se creará una clase y se dejan las funciones en blanco. Despues se crearán las clases Tea y Coffee que extiendan de HotDrink por medio de la palabra reservada extends.


class HotDrink
{
    consume() { /* abstract */ }
}

class Tea extends HotDrink
{
    consume()
    {
        console.log(`Este té es rico con limón!`)
    }
}

class Coffee extends HotDrink
{
    consume()
    {
        console.log(`Este café es delicioso!`)
    }
}

En Javascript, las clases abstractas no se comportan como una miembro abstracto como lo hace en C# o Java, pero realizar una Jerarquía de la clase HotDrink ayuda a entender que estas clases están relacionadas.

Hasta este punto tenemos una jerarquía de HotDrinks, lo que haremos a continuación será tener una jerarquía de factories que se encargará de construir estas bebidas. Si pensamos en el café o en el té, un CoffeeFactory o un TeaFactory pueden tener la misma jerarquía, para eso crearemos las 2 factories encargadas de crear el té y el café, y su clase base llamada HotDrinkFactory.


class HotDrinkFactory
{
    prepare(amount){ /* abstract */ }
}

// Construyendo los factories para el Tea y el Coffee
class TeaFactory extends HotDrinkFactory
{
    prepare(amount)
    {
        console.log(`Pon en la bolsa de té, hervir agua, verter ${amount}ml`);
        return new Tea(); // Se retorna el objeto con fines de personalización.
    }
}
class CoffeeFactory extends HotDrinkFactory
{
    prepare(amount)
    {
        console.log(`Muele algunos granos, hervir agua, verter ${amount}ml`);
        return new Coffee(); // Se retorna el objeto para personalización.
    }
}

Ahora que tenemos construidos las factories, queremos poder consumirlo; para eso construiremos la maquina de hacer bebidas calientes HotDrinkMachine y poder interactuar con las clases creadas.


class HotDrinkMachine
{
    makeDrink(type)
    {
        switch (type)
        {
            case 'tea': 
                return new TeaFactory().prepare(200);
            case 'coffee': 
                return new CoffeeFactory().prepare(50);
            default:
                throw new Error('');
        }
    }
}

let machine = new HotDrinkMachine();
let drink = machine.makeDrink('tea');
drink.consume(); 

//Pon en la bolsa de té, hervir agua, verter 200ml
//Este té es rico con limón!

Con esta implementación, no estamos en realidad haciendo uso del patrón Abstract Factory, solamente estamos explicitamente usando esas factories a mano, el cual es un acercamiento bastante mal porque estaríamos violando el principio Open–closed (Caso similarmente visto en el patrón Factory Method). Esto está mal porque siempre que tengamos un nuevo factor tendriamos que modificar el metodo makeDrink para soportarlo. Aunque esto es un punto de partida.

Lo que queremos lograr, es tener un tipo de asociación entre la factoria y el objeto que actualmente hace HotDrinkMachine, que por el momento es realizado manualmente. Para realizar esto, crearemos un enumerador llamado AvailableDrink para tener una especie de lista con todos los diferentes tipos que vamos a almacenar; despues instanciamos cada factory en el constructor() y hacemos una modificación en el metodo makeDrink().

let AvailableDrink = Object.freeze({
    coffee: CoffeeFactory,
    tea: TeaFactory
});

class HotDrinkMachine
{
    constructor()
    {
        this.factories = {};
        // Instanciamos todas las factories
        for (let drink in AvailableDrink)
        {
            /*
             * NOTA: AvailableDrink es un enumerador que tiene valores y puede ser iterada por un for. 
             * NOTA: Para poder instanciar cada factory, 
             *   se pone al final los parentesis para indicar 
             *   que es una nueva instancia. 
             * Al final el array de this.factories tendrá 
             *   un key que será el key drink, que puede ser tea o coffee 
             *   y los values serán las instancias de cada factory
            */
            this.factories[drink] = new AvailableDrink[drink]();
        }
    }

    makeDrink(type, amount)
    {
        console.log(`Ingresando parametros para preparar ${type}, en ${amount}ml`);

        return this.factories[type].prepare(amount); //Retorna Objeto Tea o Coffee
    }
}

// Ahora para poder usar la maquina correctamente. 
let machine = new HotDrinkMachine();

// TÉ
let drinkTea = machine.makeDrink('tea', 200);
// => Ingresando parametros para preparar té, en 200ml
// => Pon en la bolsa de té, hervir agua, verter 200ml

drinkTea.consume(); 
// => Este té es rico con limón!

// CAFÉ
let drinkCoffee = machine.makeDrink('coffee', 50);
// => Ingresando parametros para preparar coffee, en 50ml
// => Muele algunos granos, hervir agua, verter 50ml

drinkCoffee.consume(); 
// => Este café es delicioso!

Basicamente algunas veces terminamos con situaciones que presentan una jerarquía de tipos, en este caso tenemos la jerarquía de HotDrink que está hecha por té y café. Y se tiene la correspondiente jerarquía de factories, HotDrinkFactory y se tiene diferentes formas de construir un té y un café.

Conclusiones

El siguiente diagrama representa la implementación realizada anteriormente donde se presentan las dos jerarquías principales.

  • La jerarquía de factories puede ser usada para crear objetos relacionados. Ya que se pueden encapsular un grupo de factories individuales con una meta en comun.
  • Abstract Factory logra generar una separación de los detalles de la implementación de un grupo de objetos para un uso en general.
  • Abstract Factory

Recursos adicionales y bibliografia

https://www.dofactory.com/javascript/factory-method-design-pattern

https://addyosmani.com/resources/essentialjsdesignpatterns/book/

https://loredanacirstea.github.io/es6-design-patterns/#abstract-factory