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:
interface Delivery { double price();
String label(String recipient);}
class AddressDelivery implements Delivery { @Override public double price() { return 5.90; }
@Override public String label(String recipient) { return recipient + ", a domicilio"; }}
class OfficeDelivery implements Delivery { @Override public double price() { return 3.90; }
@Override public String label(String recipient) { return recipient + ", en una oficina de mensajería"; }}
class LockerDelivery implements Delivery { @Override public double price() { return 2.90; }
@Override public String label(String 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:
static double checkout(String type, double items) { Delivery delivery = switch (type) { case "address" -> new AddressDelivery(); case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); default -> throw new IllegalArgumentException(type); }; return items + delivery.price();}
static double previewPrice(String type) { Delivery delivery = switch (type) { case "address" -> new AddressDelivery(); case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); default -> throw new IllegalArgumentException(type); }; return delivery.price();}
static String printLabel(String type, String recipient) { Delivery delivery = switch (type) { case "office" -> new OfficeDelivery(); default -> new AddressDelivery(); }; return delivery.label(recipient);}System.out.printf("Pedido: %.2f €%n", checkout("locker", 40));System.out.printf("Vista previa: envío %.2f €%n", previewPrice("locker"));System.out.println("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 default 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 la interfaz.
Los roles:
- el producto: la forma común, la interfaz
Delivery; - los productos concretos:
AddressDelivery,OfficeDelivery,LockerDelivery; - la fábrica:
Deliveries.create(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.
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
class Deliveries { public static Delivery create(String type) { return switch (type) { case "address" -> new AddressDelivery(); case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); default -> throw new IllegalArgumentException("Tipo de envío desconocido: " + type); }; }}La caja, la vista previa y la etiqueta ya no eligen, preguntan:
static double checkout(String type, double items) { return items + Deliveries.create(type).price();}
static double previewPrice(String type) { return Deliveries.create(type).price();}
static String printLabel(String type, String recipient) { return Deliveries.create(type).label(recipient);}System.out.printf("Pedido: %.2f €%n", checkout("locker", 40));System.out.printf("Vista previa: envío %.2f €%n", previewPrice("locker"));System.out.println("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
La fábrica también puede ser una tabla: Map<String, Supplier<Delivery>> con valores como AddressDelivery::new. Una tabla es cómoda cuando los tipos se añaden desde fuera (cada módulo registra el suyo). Un switch es más claro cuando todos los tipos se conocen en un solo lugar, y el compilador comprueba que haya un default (mira la tarjeta «Tipo desconocido»).
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 implements Delivery { @Override public double price() { return 0.00; }
@Override public String label(String recipient) { return recipient + ", en la tienda"; }}y una línea en la fábrica, resaltada aquí:
class Deliveries { public static Delivery create(String type) { return switch (type) { case "address" -> new AddressDelivery(); case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); case "pickup" -> new PickupDelivery(); default -> throw new IllegalArgumentException("Tipo de envío desconocido: " + type); }; }}Delivery home = Deliveries.create("address");System.out.printf("%s: %.2f €%n", home.label("María"), home.price());
Delivery locker = Deliveries.create("locker");System.out.printf("%s: %.2f €%n", locker.label("María"), locker.price());
Delivery pickup = Deliveries.create("pickup");System.out.printf("%s: %.2f €%n", pickup.label("María"), pickup.price());María, a domicilio: 5.90 €María, en un casillero: 2.90 €María, en la tienda: 0.00 €Delivery home = Deliveries.create("address");System.out.printf("%s: %.2f €%n", home.label("María"), home.price());
Delivery locker = Deliveries.create("locker");System.out.printf("%s: %.2f €%n", locker.label("María"), locker.price());
Delivery pickup = Deliveries.create("pickup");System.out.printf("%s: %.2f €%n", pickup.label("María"), pickup.price());Paso a paso, en palabras
- Deliveries.create("address") recibe solo el texto del pedido. La fábrica busca el molde que le corresponde.
- Del molde sale un AddressDelivery nuevo, pero home tiene el tipo Delivery: quien llama ve 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 Deliveries.create 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 { Delivery delivery = Deliveries.create("drone"); System.out.println(delivery.label("María"));} catch (IllegalArgumentException e) { System.out.println(e.getMessage());}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 si la fábrica no tienedefault? Java no te deja olvidarlo: una expresión switch tiene que cubrir todos los valores, o el programa no compila:class Deliveries { public static Delivery create(String type) { return switch (type) { case "address" -> new AddressDelivery(); case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); }; }}error: the switch expression does not cover all possible input valuesMí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:
interface ReportFile { void add(String product, double total);
String text();}
class CsvFile implements ReportFile { private final List<String> lines = new ArrayList<>(List.of("producto;total"));
@Override public void add(String product, double total) { lines.add(product + ";" + String.format("%.2f", total)); }
@Override public String text() { return String.join("\n", lines); }}
class HtmlFile implements ReportFile { private final List<String> rows = new ArrayList<>();
@Override public void add(String product, double total) { rows.add("<tr><td>" + product + "</td><td>" + String.format("%.2f", total) + "</td></tr>"); }
@Override public String text() { return "<table>\n" + String.join("\n", rows) + "\n</table>"; }}record Sale(String product, double total) {}
abstract class ReportExporter { protected abstract ReportFile createFile();
public String export(List<Sale> sales) { ReportFile file = createFile(); double overall = 0; for (Sale sale : sales) { file.add(sale.product(), sale.total()); overall += sale.total(); } file.add("Total", overall); return file.text(); }}
class CsvExporter extends ReportExporter { @Override protected ReportFile createFile() { return new CsvFile(); }}
class HtmlExporter extends ReportExporter { @Override protected ReportFile createFile() { return new HtmlFile(); }}List<Sale> sales = List.of(new Sale("Café", 12.50), new Sale("Té", 8.00));System.out.println(new CsvExporter().export(sales));System.out.println(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 (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 el constructor como un Supplier<ReportFile>: new ReportExporter(CsvFile::new).
Pruébalo
¿Qué saldrá?
Los tipos vienen de un formulario de la web. ¿Qué imprime el programa?
String[] types = { "office", "Locker", "locker" };for (String type : types) { try { System.out.println(Deliveries.create(type).label("María")); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); }}Pruébalo
Encuentra el error
Otro programador escribió la fábrica así:
class Deliveries { public static Delivery create(String type) { return switch (type) { case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); default -> new AddressDelivery(); }; }}El pedido viene de la web de un socio, que envía el tipo con mayúscula inicial:
Delivery delivery = Deliveries.create("Locker");System.out.println("Etiqueta: " + delivery.label("María"));System.out.printf("Envío: %.2f €%n", delivery.price());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);
-
hay una fábrica de la fábrica.
DeliveryFactoryFactorycasi siempre significa que una de las dos sobra; - 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: HashMap y HashSet · record, switch y patrones · Patrones de diseño · Strategy
- Continúa en: Builder · La tienda: qué patrón dónde
- Ejercicios sobre el tema: Patrones de diseño