Design Patterns - Factory y Factory Method

TLDR; El Factory Method es un patrón que te permite crear Objetos sin saber de previamente los tipos exactos y las dependencias de los objetos requeridos.


¿Porque necesitamos el patrón Factory?

En algunas ocasiones, la lógica de creación de objetos se vuelve algo compleja. Si tienes una simple inicialización, no hay nada de que preocuparse, pero en algunas ocasiones se puede volver cada vez más grande, sujeta a cambios y más sofisticada. En este tipo de situaciones nos gustaría mover esta lógica a algún lado para mantener las cosas ordenadas.

También, cuando realizamos el proceso de creación de un objeto, la descripción de los métodos de inicialización no es la más acertada, porque usualmente es un __init___, constructor() o el mismo nombre de la clase. En el caso de Javascript, no puedes sobrecargarlo con un set diferente de argumentos con diferentes nombres. Y muchas veces puedes entrar en el problema del ** infierno de los parámetros opcionales (Optional parameter hell)_** en el que agregas más y más parámetros, y te das cuenta de que alguno de esos puede ser opcionales o con valores por defecto y bueno, de alguna forma hay que organizar todo esto.

Entonces aquí vamos a hablar de un creador de objetos al por mayor. Un solo comando que nos permitirá crear un objeto.

Hay varias variaciones de este patrón:

  • Con un método separado (Factory Method)
  • Con una clase separada (Factory)
  • Puedes crear una herencia de factorías con el Abstract Factory

En definición, ¿que es un Factory?

Básicamente es un componente responsable únicamente de la creación de objetos al por mayor (no por partes). Encapsulando y separando la creación de los objetos del resto del código.

El problema en un ejemplo

Imaginemos que estamos trabajando con algo relacionado a la geometría y tenemos esta clase Point


class Point  {
  // Inicialmente lo vamos a inicializar
  // con las coordenadas X y Y
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

La clase Point ha vivido en el tiempo y digamos que siempre han sido puntos cartesianos, ósea que X y Y son coordenadas Cartesianas.

Pero imaginemos que ahora queremos inicializar coordenadas polares. Entonces estarías tentado a hacer lo siguiente:


class Point {
  // Coordenadas Cartesianas
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  // Coordenadas Polares
  constructor(radio, angulo) {
    this.x = radio * Math.cos(angulo);
    this.y = radio * Math.sin(angulo);
  }
}

Desafortunadamente nosotros no podemos tener 2 constructores en Javascript. En C# o Java se puede tener más de un constructor haciendo sobrecarga de métodos, pero deben tener firmas diferentes (diferente número de parámetros) o en lenguajes como Swift o Objective-c es permitido tener más de un constructor con el mismo número de parámetros, pero no en Javascript.

Entonces, si quisiéramos que solo un constructor nos permitiera manejar estas 2 situaciones terminaríamos agregando un montón de cosas más complicadas. Por ejemplo, podemos resolverlo agregando un enumerador para el tipo de coordenadas, de la siguiente forma:


//Especificación para determinar el tipo de coordenadas a utilizar.
SistemaCoordenadas = {
  cartesiano: 0,
  polar: 1,
};

class Point {
  //SistemaCoordenadas por defecto será Cartesiano
  constructor(a, b, cs = SistemaCoordenadas.cartesiano) {
    switch (cs) {
      case SistemaCoordenadas.cartesiano:
        this.x = a;
        this.y = b;
        break;
      case SistemaCoordenadas.polar:
        this.x = a * Math.cos(b);
        this.y = a * Math.sin(b);
        break;
    }
  }
}

Lo que podemos ver anteriormente puede parecer una solución, pero hay varios problemas aquí:

  • Los nombres de los argumentos: Los parámetros llamados A y B realmente no me dicen nada relacionado a lo que espera el constructor. Como lo sería el recibir algo más descriptivo como X y Y, o RADIO y ANGULO.
  • Difícil de modificar: Si quisiéramos agregar otro sistema de coordenadas, tendríamos que incluirlo en el enumerador, modificar el switch y estaríamos violando el principio Open–closed. Esto es algo que debemos de evitar mientras sea posible.
  • Documentación compleja: Para poder que los demás desarrolladores usen esta clase, hay que hacer una documentación bastante explicita, donde se señale que el punto A es RADIO y cosas así. Adicionalmente ante cualquier modificación, hay que estar actualizándola.

Entonces, para poder solucionar este problema de una forma más limpia y clara, vamos a estudiar el patrón Factory Method y Factory.

Factory Method

El Factory Method, como su nombre lo indica, es un método fabrica que nos permite crear un objeto. Y lo bueno de esto es que no debes de llamarlo constructor(), puedes llamarlo como necesites, como veremos a continuación:


class Point {
  // Constructor por defecto.
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  // Factory Method para coordenadas Cartesianas
  static newCartesianPoint(x, y) {
    return new Point(x, y);
  }

  static newPolarPoint(radio, angulo) {
    return new Point(radio * Math.cos(angulo), radio * Math.sin(angulo));
  }
}

Entonces estamos teniendo varios beneficios aquí, vamos a señalarlos:

  • Los métodos ya me dicen que está pasando.
  • El nombre de los parámetros ya me dice que está pasando. Como en el caso del sistema polar newPolarPoint(radio, ángulo), ya se que parámetro es el correspondiente al radio y al ángulo.
  • Si se quiere agregar un nuevo sistema cartesiano, solamente será necesario agregar un nuevo método static, realizar la transformación necesaria y me retornará un objeto nuevo.

Ahora usemos esa clase


// Implementación con la solución anterior.
let p1 = new Point(2, 3, SistemaCoordenadas.cartesiano);

// Con la nueva solución usando FactoryMethod
let p = Point.newCartesianPoint(4, 5);
console.log(p); // Point { x: 4, y: 5 }

let p2 = Point.newPolarPoint(5, Math.PI / 2);
console.log(p2); // Point {x: 3.061616997868383e-16, y: 5 }

Resumen del Factory Method

El FactoryMethod es básicamente un método estático, creado para fabricar una nueva instancia del objeto de la clase y te da algunos beneficios, como el ser bastante explicito sobre los nombramientos permitiéndote saber que tipo de objeto estas creando y emparejar los parámetros enviados correctamente.

Factory

Existen variaciones del patrón Factory, este que veremos a continuación es el patrón factory siguiendo el principio de diseño Single responsibility, donde básicamente especifica que si tenemos separada la responsabilidad de crear objetos (utilizando una clase separada Ej: PointFactory), esta clase factory, aunque puede que no tenga métodos dentro de el relacionados el mismo, si tendrá toda al responsabilidad de creación del Point. Separando así toda la complejidad de creación de objetos en otra clase teniendo un código más limpio y entendible.

En el siguiente código podemos ver un ejemplo de una clase PointFactory, dicha clase tiene 2 métodos estáticos que se encargan de devolver nuevas instancias del objeto Point de acuerdo a las necesidades.


class PointFactory {
  // Factory Method para coordenadas Cartesianas
  static newCartesianPoint(x, y) {
    return new Point(x, y);
  }

  static newPolarPoint(radio, angulo) {
    return new Point(radio * Math.cos(angulo), radio * Math.sin(angulo));
  }
}

// Ahora la generación de las instancias lo maneja el PointFactory
let p = PointFactory.newCartesianPoint(4, 5);
console.log(p); // Point { x: 4, y: 5 }

let p2 = PointFactory.newPolarPoint(5, Math.PI / 2);
console.log(p2); // Point {x: 3.061616997868383e-16, y: 5 }

El patrón Factory, a diferencia del FactoryMethod, es solamente una clase o un componente separado que tendrá la responsabilidad de contener toda la lógica de creación objetos de un determinado tipo. En este caso esta clase retornará objetos de la clase Point.

En algunos casos, para evitar que el desarrollador use directamente la clase Point, es común crear un método static en la clase Point llamado factory(). Esto da a entender que la clase Point tiene un factory asociado para la creación de instancias. Veamos un ejemplo:


class Point {
  // Constructor por defecto.
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  // Obtiene el factory de Point
  static factory(x, y) {
    return new PointFactory();
  }
}

// Generación de la instancia usando el método factory
let p = Point.factory.newCartesianPoint(4, 5);
console.log(p); // Point { x: 4, y: 5 }

let p2 = Point.factory.newPolarPoint(5, Math.PI / 2);
console.log(p2); // Point {x: 3.061616997868383e-16, y: 5 }

La forma de crear la instancia por medio de un método factory depende del usuario que esté realizando la implementación y de como esté configurado el proyecto, pero la forma fácil de dar a entender que se tiene un Factory relacionado a esa clase.

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

El siguiente diagrama representa la implementación realizada anteriormente donde se presenta la interacción entre las dos clases.

Abstract Factory

La tercer variación que existe del patrón Factory es el Abstract Factory básado en clases abstractas pero este ya es otro patrón adicional.

Conclusiones

  • El patrón factory method es un método estático que es usado para la creación de objetos de acuerdo con las necesidades.
  • Se utiliza cuando la creación del objeto es compleja y requiere que la inicialización esté mejor especificada.
  • Un Factory puede ser externo o residir dentro del un objeto como una clase interna.

Ejercicio

Se requiere que se construya una solución para el manejo del servicio de transportes. Hay 2 Productos a utilizar:

MotorcycleService
CarService
Se requiere crear una solución aplicando el patrón Factory, que permita crear una instancia de alguna de las 2 clases fácilmente.

Estructura Inicial

Solución esperada

Recursos adicionales y bibliografia

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

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

https://refactoring.guru/design-patterns/factory-method