¿Cómo implementar el Patrón Factory Method en JavaScript?
El Patrón Factory Method es una solución ideal para la creación de objetos cuando tienes una jerarquía de clases. Este patrón evita la necesidad de especificar la clase exacta de los objetos que se producen al delegar la responsabilidad de la creación a sus subclases. Te conduce a un código más limpio y flexible, permitiendo que las estructuras se adapten fácilmente a futuros cambios. Veamos cómo implementar este patrón en JavaScript paso a paso.
¿Cuál es el primer paso para implementar el patrón?
Declaración de un producto base:
Para comenzar, debes declarar una clase base o interfaz que será retornada por la clase Factory y sus subclases. En el ejemplo del curso, se usa una clase BaseCar que cuenta con un método que no está implementado.
classBaseCar{showCost(){thrownewError('Method not implemented');}}
Esta estructura alienta el uso de métodos abstractos, muy comunes en programación orientada a objetos, que definen la firma pero no la implementación del método.
¿Cómo se implementan los productos concretos?
Definición de subclases concretas:
Una vez creada la clase base, se procede a implementar productos concretos que heredan de esta clase. Estas subclases deben sobrescribir el método abstracto.
classMastodonCarextendsBaseCar{showCost(){console.log('Mastodon Car Cost: 300,000 pesos mexicanos');}}classRhinoCarextendsBaseCar{showCost(){console.log('Rhino Car Cost: 100,000 pesos mexicanos');}}
Esta etapa establece un enlace directo entre las clases específicas de los productos y la clase base.
¿Cuál es el papel de la clase Factory?
Declaración de la clase Factory:
Una clase Factory o interfaz debe crear objetos que coincidan con el producto base. Este es el esqueleto que obliga a sus subclases a implementar un método específico.
classCarFactory{makeCar(){thrownewError('Method not implemented');}}
Esto fortalece el concepto de polimorfismo, donde se espera que las subclases implementen este método con su lógica particular.
¿Cómo se crean las fábricas concretas?
Implementación de fábricas concretas:
Las fábricas concretas extienden la clase base de la fábrica, retornando productos específicos.
Así se establece el puente entre las clases productoras y los productos concretos, dentro del patrón Method Factory.
¿Cómo se probó el patrón en la práctica?
Para demostrar el funcionamiento de esta estructura, se mostró cómo crear una aplicación que recibe una fábrica y produce el producto correspondiente, sin conocer de antemano qué tipo de fábrica utiliza.
functionappFactory(factory){const car = factory.makeCar(); car.showCost();}appFactory(newMastodonCarFactory());// Mastodon Car Cost: 300,000 pesos mexicanosappFactory(newRhinoCarFactory());// Rhino Car Cost: 100,000 pesos mexicanos
Aquí, la función appFactory es capaz de trabajar con cualquier variación de la fábrica, gracias a la uniformidad en la interfaz que provee el patrón.
¿Es posible una mejora creativa dentro del patrón Factory Method?
El instructor sugiere una sofisticación más, creando una función createFactory que extienda la versatilidad para crear fábricas basadas en strings.
functioncreateFactory(type){const factories ={Mastodon:MastodonCarFactory,Rhino:RhinoCarFactory};constFactory= factories[type];returnnewFactory();}appFactory(createFactory('Mastodon'));// Mastodon Car Cost: 300,000 pesos mexicanosappFactory(createFactory('Rhino'));// Rhino Car Cost: 100,000 pesos mexicanos
Esta abstracción permite expandir el código sin dificultad, simplemente añadiendo más tipos al objeto factories.
Reflexión final: ¿Qué aprendiste?
Implementar el Patrón Factory Method no solo refuerza los conceptos de programación orientada a objetos como la herencia y el polimorfismo, sino que también promueve la creatividad en el diseño de software. Añade una herramienta poderosa a tu repertorio, lista para usarse en problemas que requieran la creación flexible y extensible de objetos. ¿Qué otras aplicaciones creativas imaginas que podría tener este patrón en tus futuros proyectos?
¡Hola! Creería que a pesar que una clase abstracta pura y una interfaz puedan llegar a ser similares, hay algo importante a tener en cuenta y es que una clase abstracta pura se utiliza para definir una clase base de la cuál cada una de las subclases tienen relación, por otro lado, una interfaz se utiliza para definir un conjunto de métodos que deben ser implementados por diferentes clases y puede que exista una relación entre las clases que implementen esta interfaz como puede que sean clases no relacionadas de alguna forma, por lo que si usamos una clase abstracta pura o una interfaz variaría en el diseño y requerimientos de la aplicación.
Desde mi punto de vista, ambos cumplen con el mismo propósito, el cuál es la especificación de un contrato que se tiene que seguir para una clase en especifico. Como en JS no hay interfaces, pues creo que la mejor opción es usar una clase abstracta pura.
Algunos gajes del oficio al trabajar con JS
Es correcto Alvaro, todo depende del diseño.
Le agregaria, si ya tienes una clase abstracta pura que no tiene nada mas que metodos, no sera que el tipo de relacion que quieres construir entre dos clases no es la de "la clase es tambien un" sino que "la clase implementa este comportamiento".
Por eso hago la observacion de la similitud entre los conceptos.
classAnimal{comer(){}dormir(){}}classAveextendsAnimal{comer(){console.log("El ave está comiendo");}dormir(){console.log("El ave está durmiendo");}}const ave1 =newAve();ave1.comer();/*la clase "Ave" extiende la clase base "Animal" y redefine los métodos "comer" y "dormir" para que muestren un mensaje específico para las aves. Luego, se crea una instancia de "Ave" y se llama al método "comer"*/
En la super clase Animal, creo que se podría agregar un método abstracto como el visto en la clase tanto para los métodos de comer y dormir.
interfaceBaseCar{showCost():void}interfaceCarFactory{makeCar():BaseCar}classRhinoCarimplementsBaseCar{publicshowCost():void{console.log('Rhino Car Cost: $ 15.000')}}classMastodonCarimplementsBaseCar{publicshowCost():void{console.log('Mastodon Car Cost: $ 20.000')}}classRhinoFactoryimplementsCarFactory{publicmakeCar():BaseCar{returnnewRhinoCar()}}classMastodonFactoryimplementsCarFactory{publicmakeCar():BaseCar{returnnewMastodonCar()}}functionappFactory(factory:CarFactory):void{const car = factory.makeCar() car.showCost()}functioncreateFactory(type:'rhino'|'mastodon'):CarFactory{const factories ={ rhino:RhinoFactory, mastodon:MastodonFactory}constFactory=newfactories[type]returnFactory}appFactory(createFactory('rhino'))appFactory(createFactory('mastodon'))
Bien Enrique!
¡Hola!
Creería que al crear la función createFactory estamos aplicando el principio O de SOLID, en el que estamos abiertos a la extensión y cerrados a la modificación, en la que si queremos añadir un tipo de carro, solamente se debe de modificar la función createFactory sin modificar el código base de cada una de las clases de los carros, ¡Gracias por el ejemplo, Dani!
En general, todo el patrón nos esta ayudando a seguir el principio de Open-Closed. La función createFactory fue una pequeña abstracción al final.
Ya que si queremos agregar un nuevo carro, tendríamos que:
Modificar el "enum", que en este caso sería el objeto factories dentro de la función createFactories.
Crear la clase del nuevo carro
Crear la fabrica del nuevo carro
.
Por lo tanto, esta abierto a la extensión, pero nada del código anterior se modifico, entonces se cumple el principio.
Sinceramente el diagrama no me dejaba tan claro como funciona Factory. Entendí mucho mejor al ver el código JS, ahora me encantaría verlo con interfaces (que es algo que no comprendo del todo aún).
Me gustó más que el ejemplo de código se enseñe y revise acá en clase, los ejercicios están cool pero al menos yo si necesito un ejemplo para clarificar dudas que los diagramas/texto no me resuelven.
Todavía no termino de entender bien el por qué de la clase base, que al menos en el ejemplo es completamente abstracta y luego el método abstracto que tiene se termina modificando con polimorfismo en los children. ¿No es posible reemplazar eso simplemente con documentación? Si el día de mañana se decide por alguna razón renombrar un método en la clase parent, luego hay que ir factory por factory cambiándolo porque al estar implementando polimorfismo, que el método cambie de nombre en la clase parent, no afecta en absolutamente nada a las clases child. ¿Esto es así o me está quedando alguna cosa importante que no estoy viendo?
Agregué propiedades a la clase BaseCar las cuales se establecen desde cada uno de los productos concretos:
/** STEP 1 */classBaseCar{constructor(name, year){this.name= name;this.year= year;}showCost(){thrownewError("Method not implemented");}}/** STEP 2 */classMastodonCarextendsBaseCar{constructor(){super("mastodon",2024);}showCost(){console.log("Mastodon Car cost: 300,000 MXN");}}classRhinoCarextendsBaseCar{constructor(){super("rhino",2024);}showCost(){console.log("Rhino Car cost: 100,000 MXN");}}```/\*\*STEP1 \*/classBaseCar{constructor(name, year){this.name= name;this.year= year;}showCost(){thrownewError("Method not implemented");}}/\*\*STEP2 \*/classMastodonCarextendsBaseCar{constructor(){super("mastodon",2024);}showCost(){console.log("Mastodon Car cost: 300,000 MXN");}}classRhinoCarextendsBaseCar{constructor(){super("rhino",2024);}showCost(){console.log("Rhino Car cost: 100,000 MXN");}}
No entiendo para que hacer una clase factory si se puede tener la clase Car, la clase Rhino y Mastodont, y en la implementacion directamente llamar a un new Rhino o new Mastodont, la clase factory no es un paso extra?
La clase factory evita acoplamiento alto entre elementos creadores y productos al retornar un tipo base, y la creación de productos sucede en un único punto siguiendo el principio de responsabilidad única. Permite productos intercambiables y extensibilidad sin modificar código existente.
En que casos de uso real se aplica este patrón? Como ayudaría a crear mejor código?
Cuando requires crear objectos que tengan un comportamiento similar PERO la implementacion de este comportamiento sea diferente.
Sus ventajas radican en que si mañana requieres modificar el comportamiento de todas las fabricas lo puedes hacer en solo lugar y forzarte a implementarlo en las fabricas que implementen el comportamiento.
Ademas, no tendras que hacer uso de las clases especificas de los productos, sino que una abstraccion que te permitira crearlos, los detalles de implementacion de un nuevo producto son escondidos a los desarrolladores que no requieren saberlos.
Usar objetos para almacenar funciones o métodos es muy útil para no usar tantos anidamientos if o switch
Agregué propiedades a la clase BaseCar las cuales se establecen desde cada uno de los productos concretos: /** STEP 1 */class BaseCar { constructor(name, year) { this.name = name; this.year = year; }
showCost() { throw new Error("Method not implemented"); }}
/** STEP 2 */class MastodonCar extends BaseCar { constructor() { super("mastodon", 2024); }
showCost() { console.log("Mastodon Car cost: 300,000 MXN"); }}
class RhinoCar extends BaseCar { constructor () { super("rhino", 2024); }
showCost() { console.log("Rhino Car cost: 100,000 MXN"); }}```js
/** STEP 1 */
class BaseCar {
constructor(name, year) {
this.name = name;
this.year = year;
}
showCost() {
throw new Error("Method not implemented");
}
}
showCost() {
console.log("Rhino Car cost: 100,000 MXN");
}
}
lo de las referencias me parecio un detalle muy elegante, lo seguire usando en mis codigos.
#### Factory
> Este patrón consiste en definir clases tanto para las (fabricas especificas para instanciar productos específicos::fabricas) como para los productos las cuales implementan interfaces que son contratos genéricos para cada uno de las fabricas y productos.
publicclassMain{publicstaticvoidmain(String\[] args){Factory factoryA =newConcreteFactoryA();Factory factoryB =newConcreteFactoryB();Product productA = factoryA.makeProduct();Product productB = factoryB.makeProduct();System.out.println(productA.doProductOperation());System.out.println(productB.doProductOperation());}}interfaceProduct{StringdoProductOperation();}interfaceFactory{ProductmakeProduct();}classProductAimplementsProduct{@OverridepublicStringdoProductOperation(){return"making a ProductA product...";}}  classProductBimplementsProduct{@OverridepublicStringdoProductOperation(){return"making a ProductB product...";}}classConcreteFactoryAimplementsFactory{@OverridepublicProductmakeProduct(){returnnewProductA();}}  classConcreteFactoryBimplementsFactory{@OverridepublicProductmakeProduct(){returnnewProductB();}}
Estas clases están geniales! Hace meses estuve leyendo sobre patrones de diseño, y en ese momemto, me pareciaron temas muy crípticos. Pero con los ejemplos y las explicaciones, me están resultando muy digeribles.
Emocionado por continuar con las siguientes clases-.
La abstracción createFactory nos ofrece ambos beneficios de extensibilidad y reusabilidad. Depende de nosotros si vale la pena implementarlo dados nuestros requerimientos 🧐