Ir al contenido
wannacode

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"));
Salida
Pedido: 42.90 €
Vista previa: envío 2.90 €
Etiqueta: María, a domicilio

El 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"));
Salida
Pedido: 42.90 €
Vista previa: envío 2.90 €
Etiqueta: María, en un casillero

La 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());
Salida
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
  1. Deliveries.create("address") recibe solo el texto del pedido. La fábrica busca el molde que le corresponde.
  2. Del molde sale un AddressDelivery nuevo, pero home tiene el tipo Delivery: quien llama ve solo price() y label().
  3. La misma fábrica, otro texto: "locker" pasa de largo los otros moldes y se detiene en LockerDelivery.
  4. 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.
  5. 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.
  6. "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());
}
Salida
Tipo de envío desconocido: drone

El 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 tiene default? 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
error: the switch expression does not cover all possible input values

Mí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>";
}
}
El algoritmo común está en la clase base. El paso «crear un archivo» es un método abstracto que aporta cada subclase:
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));
Salida
producto;total
Café;12.50
Té;8.00
Total;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());
}
}
Respuesta

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());
Salida
Etiqueta: María, a domicilio
Enví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.

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. DeliveryFactoryFactory casi 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»:

Salida
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

1. La tienda añade el envío por dron, tipo "drone". ¿Qué escribes con Simple Factory?
2. La fábrica recibe un tipo que no conoce. ¿Qué debe hacer?
3. ¿Cuándo es mejor Factory Method que Simple Factory?
4. Para ahorrar memoria, la fábrica guarda un objeto por cada tipo y lo devuelve en cada create. El objeto guarda el destinatario en un campo. ¿Qué pasa con dos pedidos seguidos a casillero?
5. ¿Cuándo no necesitas una fábrica?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·