Factory Method y Simple Factory
En resumen: Una fábrica guarda en un solo lugar la decisión de qué clase crear: Simple Factory pone la elección en un punto, y Factory Method deja que una subclase decida qué objeto fabricar.
Problema
Tres sitios conocen cada tipo de envío
El pedido dice cómo llega al cliente: "address" (a domicilio), "office" (en una oficina de mensajería) o "locker" (en un casillero). Cada tipo tiene una clase con su propio precio y su propia etiqueta:
class AddressDelivery { price = 5.9;
label(recipient) { return `${recipient}, a domicilio`; }}
class OfficeDelivery { price = 3.9;
label(recipient) { return `${recipient}, en una oficina de mensajería`; }}
class LockerDelivery { price = 2.9;
label(recipient) { return `${recipient}, en un casillero`; }}Tres partes de la tienda necesitan el envío: la caja, la vista previa del precio y la impresora de etiquetas. Cada una decide por su cuenta qué clase crear:
function checkout(type, items) { let delivery; if (type === "address") { delivery = new AddressDelivery(); } else if (type === "office") { delivery = new OfficeDelivery(); } else if (type === "locker") { delivery = new LockerDelivery(); } else { throw new Error(type); } return items + delivery.price;}
function previewPrice(type) { let delivery; if (type === "address") { delivery = new AddressDelivery(); } else if (type === "office") { delivery = new OfficeDelivery(); } else if (type === "locker") { delivery = new LockerDelivery(); } else { throw new Error(type); } return delivery.price;}
function printLabel(type, recipient) { let delivery; if (type === "office") { delivery = new OfficeDelivery(); } else { delivery = new AddressDelivery(); } return delivery.label(recipient);}console.log(`Pedido: ${checkout("locker", 40).toFixed(2)} €`);console.log(`Vista previa: envío ${previewPrice("locker").toFixed(2)} €`);console.log("Etiqueta: " + printLabel("locker", "María"));Pedido: 42.90 €Vista previa: envío 2.90 €Etiqueta: María, a domicilioEl mes pasado llegaron los casilleros. Alguien añadió "locker" en la caja y en la vista previa. La impresora de etiquetas se escribió antes y tiene un else para la dirección. María pagó el envío a un casillero, y el mensajero fue a su casa.
Un tipo nuevo son tres cambios, y uno se olvida. La elección «qué clase para qué tipo» está copiada, y las copias no saben unas de otras.
La idea
Simple Factory: new vive en un solo lugar
La elección «qué clase para qué tipo» es una sola decisión, así que vive en un solo lugar: una fábrica (factory). Recibe el tipo y devuelve un objeto nuevo. Quien llama no escribe new y no conoce la clase: recibe un objeto que tiene price y label.
Los roles:
- el producto: la forma común,
priceylabel(recipient), que tiene cada clase; - los productos concretos:
AddressDelivery,OfficeDelivery,LockerDelivery; - la fábrica:
createDelivery(type), el único lugar que sabe qué clase corresponde a qué tipo; - los clientes: la caja, la vista previa, la etiqueta. Solo preguntan.
Esto se llama Simple Factory. No es del libro de la Banda de los Cuatro (Gang of Four), pero es la forma más común. Factory Method, el del libro, es algo un poco distinto y viene más adelante en la lección.
Una regla: un tipo desconocido es un error, no un valor por defecto silencioso. Una fábrica que no conoce el tipo lanza una excepción con un mensaje claro. Si no, el error no desaparece, se va con el mensajero a la dirección equivocada.
En JavaScript la fábrica es una tabla tipo → clase. Una clase es un valor como cualquier otro, así que la tabla guarda las propias clases, ynew Delivery() crea un objeto de la que tomó.La tabla «nombre → estrategia» de la lección de Strategy es lo mismo: una fábrica que te da un objeto a partir de un texto.
Míralo
Una fábrica para los tres sitios
const deliveries = new Map([ ["address", AddressDelivery], ["office", OfficeDelivery], ["locker", LockerDelivery],]);
function createDelivery(type) { const Delivery = deliveries.get(type); if (Delivery === undefined) { throw new Error(`Tipo de envío desconocido: ${type}`); } return new Delivery();}La caja, la vista previa y la etiqueta ya no eligen, preguntan:
function checkout(type, items) { return items + createDelivery(type).price;}
function previewPrice(type) { return createDelivery(type).price;}
function printLabel(type, recipient) { return createDelivery(type).label(recipient);}console.log(`Pedido: ${checkout("locker", 40).toFixed(2)} €`);console.log(`Vista previa: envío ${previewPrice("locker").toFixed(2)} €`);console.log("Etiqueta: " + printLabel("locker", "María"));Pedido: 42.90 €Vista previa: envío 2.90 €Etiqueta: María, en un casilleroLa etiqueta es para un casillero. La elección está en un solo lugar, así que no puede estar bien en la caja y mal en la etiqueta.
Más precisamente, para curiosos
¿Por qué new Delivery() y no simplemente Delivery()? En JavaScript una clase no se puede llamar como una función. Sin new el programa se detiene con:
TypeError: Class constructor AddressDelivery cannot be invoked without 'new'Por eso la tabla también puede guardar funciones que crean el objeto: ["address", () => new AddressDelivery()]. Entonces la fábrica llama a make() sin new.
Míralo
Un tipo nuevo: una clase y una línea
La tienda añade la recogida en tienda: tipo "pickup", gratis. Lo nuevo es una clase:
class PickupDelivery { price = 0;
label(recipient) { return `${recipient}, en la tienda`; }}y una línea en la fábrica, resaltada aquí:
const deliveries = new Map([ ["address", AddressDelivery], ["office", OfficeDelivery], ["locker", LockerDelivery], ["pickup", PickupDelivery],]);
function createDelivery(type) { const Delivery = deliveries.get(type); if (Delivery === undefined) { throw new Error(`Tipo de envío desconocido: ${type}`); } return new Delivery();}const home = createDelivery("address");console.log(`${home.label("María")}: ${home.price.toFixed(2)} €`);
const locker = createDelivery("locker");console.log(`${locker.label("María")}: ${locker.price.toFixed(2)} €`);
const pickup = createDelivery("pickup");console.log(`${pickup.label("María")}: ${pickup.price.toFixed(2)} €`);María, a domicilio: 5.90 €María, en un casillero: 2.90 €María, en la tienda: 0.00 €const home = createDelivery("address");console.log(`${home.label("María")}: ${home.price.toFixed(2)} €`);
const locker = createDelivery("locker");console.log(`${locker.label("María")}: ${locker.price.toFixed(2)} €`);
const pickup = createDelivery("pickup");console.log(`${pickup.label("María")}: ${pickup.price.toFixed(2)} €`);Paso a paso, en palabras
- createDelivery("address") recibe solo el texto del pedido. La fábrica busca el molde que le corresponde.
- Del molde sale un AddressDelivery nuevo. Quien llama no pregunta qué clase es: usa solo price y label().
- La misma fábrica, otro texto: "locker" pasa de largo los otros moldes y se detiene en LockerDelivery.
- La etiqueta ya dice casillero: la clase se elige en un solo lugar, y la caja, la vista previa y la etiqueta preguntan a ese lugar.
- Un tipo nuevo: la clase PickupDelivery y una línea más en la fábrica. Las líneas que llaman a createDelivery no cambian.
- "pickup" pasa como cualquier otro tipo. El tipo nuevo se añadió en un solo lugar.
La caja, la vista previa y la etiqueta no se tocan. Se enteran del tipo nuevo cuando un pedido lo pide.
Míralo
Tipo desconocido
El tipo viene de fuera: de un formulario, de la web de un socio, de un archivo viejo. Tarde o temprano aparece un tipo que no existe:
try { const delivery = createDelivery("drone"); console.log(delivery.label("María"));} catch (e) { console.log(e.message);}Tipo de envío desconocido: droneEl error dice qué tipo es desconocido. Se captura donde el pedido entra en la tienda, y el cliente ve un mensaje en lugar de un paquete en el lugar equivocado.
¿Y sin la comprobación?deliveries.get(type) devuelve undefined, y el error no dice ni qué tipo ni qué se estaba buscando:function createDelivery(type) { const Delivery = deliveries.get(type); return new Delivery();}TypeError: Delivery is not a constructorMíralo
Factory Method: la subclase decide
Simple Factory elige por texto. A veces la elección no es por texto, sino por quién pregunta.
El informe de ventas se envía como CSV (para contabilidad) y como tabla HTML (en un correo). Los dos hacen lo mismo: cada producto es una fila, y al final hay una fila «Total». Solo es distinto el archivo en el que se escriben las filas:
class CsvFile { lines = ["producto;total"];
add(product, total) { this.lines.push(`${product};${total.toFixed(2)}`); }
text() { return this.lines.join("\n"); }}
class HtmlFile { rows = [];
add(product, total) { this.rows.push(`<tr><td>${product}</td><td>${total.toFixed(2)}</td></tr>`); }
text() { return "<table>\n" + this.rows.join("\n") + "\n</table>"; }}createFile, que la subclase sobrescribe.class ReportExporter { createFile() { return new CsvFile(); }
export(sales) { const file = this.createFile(); let overall = 0; for (const [product, total] of sales) { file.add(product, total); overall += total; } file.add("Total", overall); return file.text(); }}
class HtmlExporter extends ReportExporter { createFile() { return new HtmlFile(); }}const sales = [["Café", 12.5], ["Té", 8]];console.log(new ReportExporter().export(sales));console.log(new HtmlExporter().export(sales));producto;totalCafé;12.50Té;8.00Total;20.50<table><tr><td>Café</td><td>12.50</td></tr><tr><td>Té</td><td>8.00</td></tr><tr><td>Total</td><td>20.50</td></tr></table>Esto es Factory Method: la clase base llama a un método que crea el objeto (this.createFile()), y la subclase decide qué clase exactamente.
¿Por qué no pasar un archivo ya hecho? El archivo acumula filas. Cada export tiene que empezar con un archivo vacío, así que el informe no guarda un archivo, sabe cómo crear uno nuevo.
¿Cuándo basta Simple Factory? Factory Method compensa cuando alrededor de la creación hay un algoritmo común que las subclases no deben repetir (aquí: las filas y el total). Si solo quieres un objeto por tipo, sin algoritmo alrededor, Simple Factory es más claro. Y una subclase que existe solo para devolver un objeto nuevo es ceremonia. Entonces pasa la propia clase: new ReportExporter(HtmlFile).
Pruébalo
¿Qué saldrá?
Los tipos vienen de un formulario de la web. ¿Qué imprime el programa?
for (const type of ["office", "Locker", "locker"]) { try { console.log(createDelivery(type).label("María")); } catch (e) { console.log(e.message); }}Pruébalo
Encuentra el error
Otro programador escribió la fábrica así:
const deliveries = new Map([ ["office", OfficeDelivery], ["locker", LockerDelivery],]);
function createDelivery(type) { const Delivery = deliveries.get(type) ?? AddressDelivery; return new Delivery();}El pedido viene de la web de un socio, que envía el tipo con mayúscula inicial:
const delivery = createDelivery("Locker");console.log("Etiqueta: " + delivery.label("María"));console.log(`Envío: ${delivery.price.toFixed(2)} €`);Etiqueta: María, a domicilioEnvío: 5.90 €María eligió un casillero. Pagó 5,90 € de envío en lugar de 2,90 €, y el paquete fue a su dirección. ¿En qué línea está el error?
Elige la línea con el error.
Esta línea convierte en silencio cualquier tipo desconocido en un envío a domicilio. La mayúscula es solo el detonante: mañana será "lockr", o un tipo nuevo que alguien olvidó añadir aquí. La fábrica debe lanzar un error claro ante un tipo desconocido. Si quieres aceptar también "Locker", pasa el tipo a minúsculas en la propia fábrica, en un solo lugar, y no en cada uno que la llama.
La idea
Cuándo no la necesitas
Una fábrica es un salto más al leer: para ver qué clase se crea, vas a otro sitio. No compensa cuando:
- ese lugar siempre necesita la misma clase.
new OfficeDelivery()donde el envío siempre va a una oficina es más claro que una fábrica; - el tipo se conoce mientras escribes el código y no llega como dato (de un pedido, un archivo, un formulario);
-
escribes una clase
DeliveryFactorycon solo un método estático. Bastan una tabla y una función; - un Factory Method tiene una subclase «para el futuro».
Compensa cuando el tipo llega como dato y la elección de la clase, si no, se copiaría en varios sitios. O cuando hay un algoritmo común alrededor de la creación y solo cambia el paso «qué objeto».
Ejercicio
Envío exprés
Resuelve esto en tu editor.
Envío exprés. La tienda añade el envío exprés a domicilio: tipo "express", 9,90 €, etiqueta «…, exprés a domicilio». Añade una clase ExpressDelivery y hazla disponible a través de la fábrica. La caja, la vista previa y la etiqueta no se tocan. Crea un envío exprés para María e imprímelo como en la tarjeta «Un tipo nuevo»:
María, exprés a domicilio: 9.90 €Más tareas
- Mayúsculas y espacios. El formulario envía
"Locker"y" locker ". Haz que la fábrica los acepte cambiando solo la fábrica. Los tipos desconocidos tienen que seguir dando error. - Un informe JSON. Añade un formato JSON al informe de la tarjeta «Factory Method»:
[{"product": "Café", "total": 12.50}, …]. ¿Qué clases escribes y cuáles no tocas? - Precio por peso. Los casilleros admiten paquetes de hasta 20 kg, y por encima de 5 kg el envío a domicilio cuesta más. ¿Dónde va el peso: en la fábrica o en el método del precio? Piensa por qué.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Map y Set · Datos, switch y desestructuración · Patrones de diseño · Strategy
- Continúa en: Builder · La tienda: qué patrón dónde
- Ejercicios sobre el tema: Patrones de diseño