Sep 24, 2019

Factory Method de Patrones de Creación


A veces, cuando tenemos que crear diferentes instancias y hacerlo con una forma básica, podríamos agregar complejidad al diseño. En esos casos, los patrones de creación se utilizan para controlar el proceso de creación y ocultar la complejidad al mismo tiempo.

Situación

Considere que estamos creando una aplicación para una Fábrica de ensambladores de automóviles, que tiene que ensamblar los automóviles de diferentes partes y necesitan comercializar varios modelos. Para este ejemplo, las partes serán Tires (Neumáticos) y Engine (Motor) pero debemos tener en cuenta el crecimiento y la diversidad.

Descripción

En el patrón Factory, creamos objetos sin exponer la lógica de creación al cliente y hacemos referencia a objetos recién creados utilizando una interfaz común.

Diagrama UML

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

Participantes

Las clases e interfaces que participan en este patrón son:

  • Producto como IAutomobile: define la interfaz de los objetos que crea el método de fábrica.
  • ConcreteProduct como Car y ElectricCar: implementa la interfaz del Producto.
  • Creador como CarFactory: declara el método de fábrica, que devuelve un objeto de tipo Producto. El creador también puede definir una implementación predeterminada del método de fábrica que devuelve un objeto ConcreteProduct predeterminado.
  • ConcreteCreator como ElectrictCar: anula el método de fábrica para devolver una instancia de ConcreteProduct.

Implementación

Comencemos a crear la interfaz del producto, en este caso los productos serían nuestros automóviles. Tenemos que especificar las propiedades comunes que pueden tener cada modelo que ensamblemos. Al final, todos son automóviles:

interface IAutomobile {
  tires: Tire[];
  engine: Engine;
}

A continuación, creamos el producto de clase (Car) extendiendo la interfaz del producto, y cada componente (Tire and Engine) que pertenecen al producto como clases.

class Car implements IAutomobile {
    constructor(public tires: Tire[], public engine: Engine) { }
}

class Tire {
    constructor(public brand: string) { }
}

class Engine {
    constructor(public cylinder: number) { }
}

Estos componentes están utilizando una característica de typescript para declarar propiedades con su nivel de acceso en el constructor.

Ahora, tenemos que crear la implementación predeterminada de la clase CarFactory.

class CarFactory {
  buildCar(): Car {}
  joinTires(): Tire[] {}
  createEngine(): Engine {}
}

Comencemos a sumergirnos en los métodos que crean los componentes necesarios para la construcción de un automóvil promedio.

joinTires(): Tire[] {
    let tireSet = []

    for (let i = 0; i < 4; i++) {
        tireSet.push(new Tire("Generic"))
    }

    return tireSet
}

createEngine(): Engine {
    return new Engine(4)
}

El método joinTires itera 4 veces agregando un objeto neumático 'genérico' a una matriz que luego es retornado. Mientras tanto, createEngine devuelve una instancia simple de un motor con 4 cylinders (cilindros).

Después de eso, los ponemos en el método buildCar:

buildCar(): IAutomobile {

    const tires = this.joinTires()
    const engine = this.createEngine()

    return new Car(tires, engine);
}

Ahora, para construir un automóvil, solo tenemos que crear una instancia de la clase CarFactory y llamar a su método buildCar:

const factory = new CarFactory()

const car = factory.buildCar()

La implementación de este patrón nos permite crear una nueva instancia sin el uso del operador de palabra clave new. A primera vista, esto puede parecer inútil, pero ahora podemos anular el método y cambiar la clase devuelta en función de la interfaz del producto implementada.

En caso de que tengamos que crear otro modelo, como un modelo de automóvil eléctrico, obviamente debe tener un motor eléctrico.

Para lograr esto, solo tenemos que implementar la interfaz IAutomobile para este nuevo producto.

class ElectricCar implements IAutomobile {
    constructor(public tires: Tire[], public engine: Engine) { }
}

Como todos sabemos, los autos eléctricos no usan cilindros en su motor, por lo que debemos crear una nueva clase llamada ElectricEngine. Esta nueva clase será ligeramente diferente de su clase principal Engine.

class ElectricEngine extends Engine {
  constructor(batteryAh: number) {
    super(0)
  }
}

Aquí, cambiamos el parámetro del constructor con el consumo de battery Ah (Amperes de bateria) y usando el método super para pasar este valor al constructor padre.

Luego, extendemos desde la clase factory para anular el método factory y otros que necesitemos (como _createEngine _).

class ElectricCarFactory extends CarFactory {
  createEngine() {
    return new ElectricEngine(3.8)
  }

  buildCar() {
    const tires = this.joinTires()
    const engine = this.createEngine()

    return new ElectricCar(tires, engine)
  }
}

Misión cumplida, podemos usar otra clase con el mismo factory method para crear un producto familiar y mantener el diseño de la aplicación escalable.

const factory = new ElectricCarFactory()

const car = factory.buildCar()

Puedes ver el código completo aquí

Consecuencia

En la implementación anterior, el método de fábrica buildCar es quien maneja cada paso en el ensamblaje del automóvil, pero ¿qué pasa si tenemos otro modelo y necesita un componente adicional? Afortunadamente, tenemos la misma estructura con el mismo esquema, pero eso no siempre sucederá.

¿Cuándo se debe usar?

Hay muchas situaciones de usarlo:

  • cuando no sabe de antemano los tipos exactos y las dependencias de los objetos con los que su código debería funcionar.
  • cuando desee proporcionar a los usuarios de su librería o framework una forma de extender sus componentes internos.

Comenta abajo cuál es tu escenario ideal para usar el patrón Factory method.

Factory Method de Patrones de Creación
Commentarios