Conociendo el Patrón Factory

¿Alguna vez te ha pasado que creas una clase para representar alguna entidad en tu programa y tiempo después te piden que implementes la misma funcionalidad para una variante de esta misma clase?

Puede que tiempo después te pidan una tercera variante, y así indefinidamente.

Por ejemplo imaginemos que estamos construyendo una aplicación para tocar música y empezamos con un instrumento, entonces podríamos tener un código como el siguiente (para este artículo usaré typescript ya que me parece que es sencillo de entender):

class Instrument {

    tune(): void {
        console.log('afinando');
    }


    play(): void {
        console.log('tocando música');
    }

}
class Main {

    playMusic(): void {

        const myInstrument = new Instrument();
        myInstrument.tune();
        myInstrument.play();
    }
}


new Main().playMusic();

Listo, tenemos un buen programa que cumple con nuestro objetivo, pero entonces nos piden tener 2 instrumentos, un piano y un violín y que el método playMusic use uno u otro dependiendo de un parámetro proporcionado por el usuario o leído de alguna configuración.

Probablemente se nos ocurra convertir la clase Instrument en una interfaz e implementarla en 2 clases nuevas Piano y Violin.

interface Instrument {

    tune(): void;

    play(): void;

}
class Piano implements Instrument {

    tune(): void {
        console.log('Afinando el piano');
    }


    play(): void {
        console.log('Tocándo el piano');
    }
}
class Violin implements Instrument {

    tune(): void {
        console.log('Afinando el violín');
    }


    play(): void {
        console.log('Tocándo el violín');
    }
}
class Main {

    playMusic(instrumentType: number): void {

        if (instrumentType === PIANO) {
            const piano = new Piano();
            piano.tune();
            piano.play();

        } else {
            const violin = new Violin();
            violin.tune();
            violin.play();

        }
    }
}

new Main().playMusic(PIANO);

El problema ha quedado resuelto, aún mejor, tal vez se nos ocurra ir un paso más allá y aprovechar la interfaz para refactorizar el código repetido:
 

class Main {

    playMusic(instrumentType: number): void {

        let instrument: Instrument
        if (instrumentType === PIANO) {
            instrument = new Piano();
        } else {
            instrument = new Violin();
        }

        instrument.tune();
        instrument.play();
    }
}

Pero entonces tendremos el problema de que cada vez que queramos agregar un instrumento habrá que tocar la el método playMusic lo cual viola el principio de Responsabilidad única ya que nuestro método tiene 2 razones para cambiar, cuando queramos modificar la forma de tocar nuestro instrumento y cuando queramos agregar más instrumentos (además que intentar escribir pruebas para esto sería un dolor de cabeza, pero eso lo dejaremos para otro post).

Para estos casos los desarrolladores “ancestrales” definieron un patrón por medio de el cual podemos desacoplar la lógica de creación al cual llamaron Factory pattern o fábrica en español.

¿Qué es el Patrón Factory?

El patrón factory pertenece a la familia de patrones creacionales, y su objetivo permitirnos abstraer la lógica de creación de objetos que pertenecen a la misma familia.

Este patrón lo podemos representar de la siguiente manera:

Donde:

  • La interfaz Product en nuestro ejemplo anterior sería nuestra interfaz Instrument, es decir, la abstracción (clase padre o interfaz) de la entidad que queremos crear.
  • ProductA sería una implementación concreta de la interfaz, en este caso usaremos Piano.
  • ProductB sería otra implementación concreta de la interfaz, para nuestro ejemplo usaremos Violin.
  • Factory sería la clase encargada de crear nuestro objeto.
  • Client es la clase que usa estos objetos, para nuestro ejemplo sería Main.

Veamos como se vería nuestro ejemplo anterior implementando el patrón factory:

const PIANO = 1;
const VIOLIN = 2;
interface Instrument {
    tune(): void;
    play(): void;
}
class Piano implements Instrument {

    tune(): void {
        console.log('Afinando el piano');
    }


    play(): void {
        console.log('Tocándo el piano');
    }
}
class Violin implements Instrument {

    tune(): void {
        console.log('Afinando el violín');
    }


    play(): void {
        console.log('Tocándo el violín');
    }
}
class InstrumentFactory {

    static make(instrumentType: number): Instrument {
        switch (instrumentType) {
        case PIANO:
            return new Piano();
        case VIOLIN:
            return new Violin();
       default:
            throw 'Instrumento no válido';
        }
    }
}
class Main {

    playMusic(instrumentType: number): void {
        const instrument = InstrumentFactory.make(instrumentType);
        instrument.tune();
        instrument.play();
     }
}

new Main().playMusic(PIANO);

Conclusión

Como puedes observar puedes usar el patrón factory para extender tu aplicación de manera sencilla, separando la lógica de creación de objetos y dejando esta responsabilidad a tu clase fábrica.

Comparto el código completo de este ejemplo:

https://github.com/raag/factory_example/tree/main