Design Patterns - Abstract Factory

TLDR; El Abstract Factory es un patrón de diseño creacional que provee una interfaz para la creación de familias de objetos relacionados o dependientes sin especificar sus clases concretas.


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.

¿Porque necesitamos el patrón Abstract Factory?

Como se explicaba anteriormente en el patrón Factory Method, la inicialización de un objeto se puede volver compleja de manejar. El Factory y Factory Method manejan bien casos simples, que no involucren demasiadas variaciones. Pero, que pasa cuando hay varios productos relacionadas a una familia, así como se muestra en la siguiente imagen, un ejemplo son las mascotas: Gatos, Perros, etc, y adicionalmente esa familia de productos tienen variaciones como lo pueden ser las Razas Pequeñas, Razas Grandes, en fin… Aquí se vuelve más complejo el tema.

Aquí es donde entra el patrón Abstract Factory, que nos permite manejar este tipo de relaciones con diferentes variantes, simplificando la creación de objetos sin tener que especificar clases en concreto.

En definición, ¿que es el patrón Abstract Factory?

El Abstract Factory es un patrón de diseño creacional que provee una interfaz para la creación de familias de objetos relacionados o dependientes sin especificar sus clases concretas.

El problema en un ejemplo

Supongamos debemos programar una aplicación cuya la lógica sea la de una maquina para producir bebidas. Tenemos que manejar el Café y el Té, en su proceso de creación y consumo. Aplicando el patrón Abstract Factory iniciaremos creando 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. Después 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 explícitamente 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 tendríamos que modificar el método makeDrink para soportarlo. Aunque esto es un punto de partida.

Lo que queremos lograr, es tener un tipo de asociación entre la factoría 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; después instanciamos cada factory en el constructor() y hacemos una modificación en el método 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
  }
}

Listo, ahora que hemos creado las clases necesarias, usaremos la implementación.

// 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!

Básicamente 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é.

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

Estructura del patrón Abstract Factory

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

  1. Los Abstract Products, que en el ejemplo es la interfaz HotDrink es el lugar donde se declaran los productos relacionados que se van a utilizar. En nuestro caso sabemos que todos los productos que utilizaremos implementarán la clase consume().
  2. Los Concrete Products, que son las clases Coffee y Tea. Básicamente son las implementaciones de la clase abstracta HotDrink, en donde se tendrán todas las variantes.
  3. El Abstract Factory, que en nuestro ejemplo es la interfaz HotDrinkFactory, es el lugar que declara un conjunto de métodos para crear cada uno de los productos abstractos.
  4. Los Concrete Factories, que en nuestro ejemplo son las implementaciones TeaFactory y CoffeeFactory. Básicamente son las clases que implementan los métodos de creación del Abstract Factory HotDrinkFactory. Cada factory en concreto corresponde a una variación especifica del producto y solamente crea esa variante del producto.
  5. El cliente, en este caso, sería mucho más sencillo de implementar debido a que no es necesario conocer toda la especificación interna. De esta forma, el código del cliente que usa una fabrica no se va a acoplar a ninguna variante en especifico.

Conclusiones

  • 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 común.
  • Abstract Factory logra generar una separación de los detalles de la implementación de un grupo de objetos para un uso en general.
  • Los Abstract Factory son muy útiles cuando tu código tiene varias familias de productos relacionados, pero quieres que la dependencia entre ellos no sea a una clase en concreto. Siendo muy oportuno cuando se quiere extender el software sin modificarlo (Open/Closed Principle) y extraer los productos en un solo lugar (Single Responsibility Principle).
  • Usar cuando realmente sea necesario y tenga una jerarquía de factories, porque de lo contrario causarás que tu código sea más complicado de lo que ya es.

Ejercicio

Se requiere que se construya una solución para poder servir alimentos a perros y gatos, así como se muestra en el siguiente imagen:

Para poder integrar los productos relacionados y las familias, se requiere crear una solución aplicando el patrón Abstract Factory, para que se pueda fácilmente brindar alimentos dependiendo en la familia.

Estructura Inicial

Solucion Esperada.

Recursos adicionales y bibliografía

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

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

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