Factory Method

Factory Method and Abstract Factory

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 El infierno de los parámetros opcionales (Optional parameter hell) en el que agregas más y más parámetros, y te das cuenta que alguno de esos pueden 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, osea 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.

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 dicen que está pasando. Como en el caso del sistema polar newPolarPoint(radio, angulo), ya se que parámetro es el correspondiente al radio y al angulo.
  • 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.