En la lección anterior aprendimos a fabricar productos de uno en uno sin acoplar el cliente a sus clases. Hoy subimos un nivel: hay problemas donde los objetos no viajan solos sino en familias que deben ser coherentes entre sí, y crear una pieza de la familia equivocada es un bug —a veces carísimo—. Es exactamente lo que le pasó a PideYa al expandirse a México: una pasarela de pago mexicana combinada con una calculadora de IVA español. Abstract Factory existe para que esa mezcla sea imposible por construcción.
Contenido
- El problema en PideYa: la expansión internacional
- Intención y estructura del patrón
- Implementación Java completa
- Añadir un mercado y añadir un producto: la asimetría clave
- Relación y diferencia con Factory Method
- Cuándo usarlo y cuándo no
- Errores comunes, ejercicios y conclusión
El problema en PideYa: la expansión internacional
PideYa abre operaciones en México. Cada mercado necesita su propia versión de tres piezas que participan en todo cobro:
| Pieza | España | México |
|---|---|---|
| Pasarela de pago | Redsys (tarjetas, Bizum) | Conekta (tarjetas, OXXO) |
| Calculadora de impuestos | IVA 10% comida a domicilio | IVA 16% general |
| Formateador de tickets | Normativa española (NIF, "IVA") | Normativa mexicana (RFC, "IVA", leyenda CFDI) |
Las tres piezas ya tienen sus interfaces (PasarelaPago, CalculadoraImpuestos, FormateadorTicket) y el checkout programa contra ellas. El primer intento de internacionalización creó cada pieza por separado:
// Primer intento: cada pieza se elige por su cuenta. NO imitar.
PasarelaPago pasarela = switch (pais) {
case ES -> new PasarelaRedsys(config.getClaveRedsys());
case MX -> new PasarelaConekta(config.getClaveConekta());
};
CalculadoraImpuestos impuestos = switch (pais) {
case ES -> new ImpuestosEspana();
case MX -> new ImpuestosMexico();
};
FormateadorTicket ticket = switch (pais) {
case ES -> new TicketEspana();
case MX -> new TicketMexico();
};Además de triplicar el switch (dolor conocido de la lección anterior), este código tiene un defecto más grave y más sutil: la coherencia de la familia depende de la disciplina humana. Nada impide que un refactor despistado, una rama mal fusionada o un copia-pega deje PasarelaConekta conviviendo con ImpuestosEspana. Fue literalmente el incidente de producción: cobros en México con un 10% de IVA español. El compilador no protestó, porque cada pieza era válida por separado; lo inválido era la combinación, y ningún tipo la representaba.
La necesidad, formulada con precisión: queremos crear la familia completa de una vez, garantizando que todas las piezas pertenecen al mismo mercado, y que el checkout no sepa jamás qué mercado está sirviendo.
Intención y estructura del patrón
Intención (GoF): proporcionar una interfaz para crear familias de objetos relacionados o dependientes sin especificar sus clases concretas.
La solución: una interfaz de fábrica con un método de creación por cada tipo de producto de la familia. Cada implementación de la fábrica corresponde a una variante (un mercado) y fabrica todos los productos de esa variante. El cliente recibe una fábrica y le pide todas las piezas: la coherencia queda garantizada porque una FabricaMexico es físicamente incapaz de producir impuestos españoles.
classDiagram
class FabricaMercado {
<<interface>>
+crearPasarelaPago() PasarelaPago
+crearCalculadoraImpuestos() CalculadoraImpuestos
+crearFormateadorTicket() FormateadorTicket
}
class FabricaEspana {
+crearPasarelaPago() PasarelaPago
+crearCalculadoraImpuestos() CalculadoraImpuestos
+crearFormateadorTicket() FormateadorTicket
}
class FabricaMexico {
+crearPasarelaPago() PasarelaPago
+crearCalculadoraImpuestos() CalculadoraImpuestos
+crearFormateadorTicket() FormateadorTicket
}
class PasarelaPago { <<interface>> }
class CalculadoraImpuestos { <<interface>> }
class FormateadorTicket { <<interface>> }
class PasarelaRedsys
class PasarelaConekta
class ImpuestosEspana
class ImpuestosMexico
class TicketEspana
class TicketMexico
class ServicioCheckout
FabricaMercado <|.. FabricaEspana
FabricaMercado <|.. FabricaMexico
PasarelaPago <|.. PasarelaRedsys
PasarelaPago <|.. PasarelaConekta
CalculadoraImpuestos <|.. ImpuestosEspana
CalculadoraImpuestos <|.. ImpuestosMexico
FormateadorTicket <|.. TicketEspana
FormateadorTicket <|.. TicketMexico
FabricaEspana ..> PasarelaRedsys : crea
FabricaEspana ..> ImpuestosEspana : crea
FabricaEspana ..> TicketEspana : crea
FabricaMexico ..> PasarelaConekta : crea
FabricaMexico ..> ImpuestosMexico : crea
FabricaMexico ..> TicketMexico : crea
ServicioCheckout --> FabricaMercado : usa
Los roles GoF sobre PideYa:
| Rol GoF | En PideYa |
|---|---|
| AbstractFactory | FabricaMercado |
| ConcreteFactory (una por variante) | FabricaEspana, FabricaMexico |
| AbstractProduct (uno por tipo de pieza) | PasarelaPago, CalculadoraImpuestos, FormateadorTicket |
| ConcreteProduct | PasarelaRedsys, ImpuestosMexico, TicketEspana, ... |
| Client | ServicioCheckout (solo conoce interfaces) |
Observa la geometría, que es la firma visual del patrón: una matriz de variantes × productos. Las filas son mercados; las columnas, tipos de producto; cada fábrica concreta materializa una fila completa. Y a diferencia de Factory Method, aquí la fábrica es un objeto que se pasa y se inyecta: ámbito de objeto, composición sobre herencia.
Implementación Java completa
Productos abstractos (dos de los tres, por brevedad; FormateadorTicket es análogo):
public interface PasarelaPago {
ResultadoPago cobrar(BigDecimal importe, DatosPago datos);
}
public interface CalculadoraImpuestos {
/** Devuelve el importe del impuesto para una base imponible dada. */
BigDecimal calcular(BigDecimal baseImponible);
}Productos concretos de cada mercado:
public class PasarelaRedsys implements PasarelaPago {
private final String claveComercio;
public PasarelaRedsys(String claveComercio) { this.claveComercio = claveComercio; }
@Override
public ResultadoPago cobrar(BigDecimal importe, DatosPago datos) {
// Integración real con Redsys: firma, TPV virtual, Bizum...
return ResultadoPago.aceptado("redsys-" + UUID.randomUUID());
}
}
public class ImpuestosEspana implements CalculadoraImpuestos {
private static final BigDecimal IVA_COMIDA_DOMICILIO = new BigDecimal("0.10");
@Override
public BigDecimal calcular(BigDecimal base) {
return base.multiply(IVA_COMIDA_DOMICILIO).setScale(2, RoundingMode.HALF_UP);
}
}
public class PasarelaConekta implements PasarelaPago {
private final String apiKey;
public PasarelaConekta(String apiKey) { this.apiKey = apiKey; }
@Override
public ResultadoPago cobrar(BigDecimal importe, DatosPago datos) {
// Integración con Conekta: tarjetas, pagos en efectivo vía OXXO...
return ResultadoPago.aceptado("conekta-" + UUID.randomUUID());
}
}
public class ImpuestosMexico implements CalculadoraImpuestos {
private static final BigDecimal IVA_GENERAL = new BigDecimal("0.16");
@Override
public BigDecimal calcular(BigDecimal base) {
return base.multiply(IVA_GENERAL).setScale(2, RoundingMode.HALF_UP);
}
}La fábrica abstracta y las concretas —el corazón del patrón—:
public interface FabricaMercado {
PasarelaPago crearPasarelaPago();
CalculadoraImpuestos crearCalculadoraImpuestos();
FormateadorTicket crearFormateadorTicket();
}
public class FabricaEspana implements FabricaMercado {
private final ConfiguracionPideYa config;
public FabricaEspana(ConfiguracionPideYa config) { this.config = config; }
@Override
public PasarelaPago crearPasarelaPago() {
return new PasarelaRedsys(config.getClaveRedsys());
}
@Override
public CalculadoraImpuestos crearCalculadoraImpuestos() {
return new ImpuestosEspana();
}
@Override
public FormateadorTicket crearFormateadorTicket() {
return new TicketEspana();
}
}
public class FabricaMexico implements FabricaMercado {
private final ConfiguracionPideYa config;
public FabricaMexico(ConfiguracionPideYa config) { this.config = config; }
@Override
public PasarelaPago crearPasarelaPago() {
return new PasarelaConekta(config.getClaveConekta());
}
@Override
public CalculadoraImpuestos crearCalculadoraImpuestos() {
return new ImpuestosMexico();
}
@Override
public FormateadorTicket crearFormateadorTicket() {
return new TicketMexico();
}
}Fíjate en que cada método de la fábrica es un factory method (en el sentido amplio): Abstract Factory es, en su implementación más común, un paquete de factory methods agrupados por variante. De ahí que el GoF diga que ambos patrones suelen aparecer juntos.
El cliente: recibe la fábrica inyectada y no menciona ningún mercado, ninguna clase concreta, jamás:
public class ServicioCheckout {
private final PasarelaPago pasarela;
private final CalculadoraImpuestos impuestos;
private final FormateadorTicket formateador;
/** Toda la familia sale de LA MISMA fábrica: coherencia por construcción. */
public ServicioCheckout(FabricaMercado fabrica) {
this.pasarela = fabrica.crearPasarelaPago();
this.impuestos = fabrica.crearCalculadoraImpuestos();
this.formateador = fabrica.crearFormateadorTicket();
}
public Ticket confirmarPedido(Pedido pedido) {
BigDecimal base = pedido.getTotalSinImpuestos();
BigDecimal impuesto = impuestos.calcular(base);
BigDecimal total = base.add(impuesto);
ResultadoPago resultado = pasarela.cobrar(total, pedido.getDatosPago());
if (!resultado.esAceptado()) {
throw new PagoRechazadoException(resultado.getMotivo());
}
return formateador.formatear(pedido, base, impuesto, total);
}
}La raíz de composición, único lugar donde el mercado se decide (nota que es el mismo movimiento que aprendimos con los notificadores: un punto único de decisión, aquí elevado de "qué producto" a "qué familia"):
FabricaMercado fabrica = switch (mercado) {
case ES -> new FabricaEspana(config);
case MX -> new FabricaMexico(config);
};
ServicioCheckout checkout = new ServicioCheckout(fabrica);Ahora la mezcla que causó el incidente es imposible de escribir: no existe ninguna secuencia de llamadas que combine Conekta con impuestos españoles, porque las piezas ya no se eligen una a una. La invariante "todo del mismo mercado" ha pasado de ser una convención a ser una propiedad del sistema de tipos. Y de regalo: testear el checkout es trivial con una FabricaDePruebas que devuelva dobles coherentes.
Añadir un mercado y añadir un producto: la asimetría clave
Toda decisión de diseño compra flexibilidad en un eje pagándola en otro. La de Abstract Factory es nítida y debes conocerla antes de adoptarlo:
- Añadir una variante (fila) es barato y limpio. Francia:
FabricaFrancia+PasarelaStripeFrancia+ImpuestosFrancia+TicketFrancia. Todo son clases nuevas; ni el cliente ni las fábricas existentes se tocan. OCP perfecto en el eje "mercados". - Añadir un tipo de producto (columna) es caro. Si cada mercado necesita ahora un
ValidadorDireccionFiscal, hay que añadircrearValidadorDireccionFiscal()a la interfazFabricaMercado... y eso rompe todas las fábricas concretas existentes, que deben implementarlo. Es una modificación en cascada, el precio estructural del patrón.
Regla práctica: Abstract Factory encaja cuando el conjunto de productos de la familia es estable y lo que crece son las variantes. Si esperas lo contrario (productos nuevos a menudo, variantes fijas), el patrón te hará sufrir; considera fábricas más pequeñas o interfaces segregadas (el espíritu de ISP de la lección de principios).
Relación y diferencia con Factory Method
Es la confusión más frecuente de todo el módulo; dejémosla resuelta con una tabla:
| Aspecto | Factory Method | Abstract Factory |
|---|---|---|
| Fabrica | Un producto | Una familia de productos relacionados |
| Mecanismo | Un método (a menudo heredable) dentro de una clase con lógica propia | Un objeto dedicado exclusivamente a fabricar, con un método por producto |
| Ámbito GoF | Clase (herencia decide el producto) | Objeto (se inyecta la fábrica; composición) |
| Garantiza coherencia entre productos | No aplica (solo hay uno) | Sí: es su razón de ser |
| Intención dominante | Abrir un punto de extensión | Blindar una combinación válida |
| Relación mutua | — | Sus métodos suelen implementarse como factory methods |
Dos frases para llevarse: Factory Method es un método; Abstract Factory es un objeto. Y: si tus productos no necesitan ser coherentes entre sí, no tienes una familia: tienes productos sueltos, y con Factory Method (o varios) te basta. La evolución natural de uno a otro la trazaremos en la comparativa del módulo.
Cuándo usarlo y cuándo no
Úsalo cuando:
- El sistema debe funcionar con varias familias intercambiables de productos y la coherencia intra-familia es una invariante de negocio (mercados de PideYa; el ejemplo GoF clásico: look and feel de interfaces gráficas, con botones y menús que deben ser todos del mismo estilo).
- Quieres poder cambiar la familia entera en un solo punto (configuración, arranque, incluso en caliente).
- Quieres blindar por tipos que "las piezas de A no se mezclan con las de B".
No lo uses cuando:
- Solo hay una familia (un mercado): es la sobreingeniería catalogada en la lección de la balanza; el día que llegue el segundo mercado, refactorizar hacia el patrón será natural.
- Los "productos" no guardan relación de coherencia: son fábricas independientes disfrazadas de familia.
- El catálogo de productos de la familia cambia a menudo (la asimetría de la sección anterior te castigará).
Relación con otros patrones (solo mención): las fábricas concretas no suelen tener estado y a menudo se comparten como Singleton; una fábrica puede implementarse internamente con Prototype (clonando ejemplares en vez de instanciar); y los productos complejos que la fábrica devuelve pueden construirse por dentro con un Builder.
Errores Comunes y Consejos
- Filtrar el mercado hacia el cliente. Si dentro de
ServicioCheckoutaparece unif (pais == MX), el patrón ha fracasado: toda variación por mercado debe vivir en los productos concretos o en su fábrica. El cliente ha de ser monolingüe en interfaces. - La fábrica "cajón de sastre". Meter en
FabricaMercadoproductos sin relación de coherencia (¿crearLoggerDePedidos()?) solo porque "ya tenemos la fábrica a mano". Cada método nuevo encarece la interfaz para todas las variantes; la familia debe tener un criterio de pertenencia claro. - Explosión combinatoria silenciosa. Con 5 mercados y 4 productos hay 20 clases concretas + 5 fábricas. Es el coste honesto del patrón; si varias variantes comparten piezas (México y Colombia usan la misma pasarela), extrae clases base o componlas: las fábricas pueden devolver instancias compartidas.
- Olvidar la fábrica de pruebas. Una
FabricaDePruebasque devuelve dobles coherentes es de los mayores regalos del patrón para los tests; si no la escribes, estás pagando el patrón sin cobrar una de sus rentas. - Consejo: nombra las fábricas por la variante, no por la tecnología (
FabricaMexico, noFabricaConekta): la tecnología es un detalle que puede cambiar dentro de la misma variante sin tocar a nadie.
Ejercicios
Ejercicio 1: añadir el mercado Francia
Añade Francia a la implementación de la lección: pasarela Stripe (necesita apiKeyStripe de configuración), IVA del 10% para comida a domicilio y ticket con normativa francesa (SIRET). Escribe la fábrica concreta y enumera los ficheros modificados.
Ejercicio 2: detectar la familia rota
Un compañero propone esta "mejora" para ahorrar clases. ¿Qué garantía del patrón destruye y qué error concreto vuelve a ser posible?
public class FabricaFlexible implements FabricaMercado {
private final Pais paisPasarela;
private final Pais paisImpuestos;
private final Pais paisTicket;
public FabricaFlexible(Pais paisPasarela, Pais paisImpuestos, Pais paisTicket) { /*...*/ }
@Override
public PasarelaPago crearPasarelaPago() {
return paisPasarela == Pais.ES ? new PasarelaRedsys(...) : new PasarelaConekta(...);
}
// ... análogo para impuestos y ticket, cada uno con SU país
}Ejercicio 3: ¿Factory Method o Abstract Factory?
Para cada situación de PideYa, decide cuál de los dos patrones encaja y justifícalo en una frase:
- Crear el objeto
InformeVentasadecuado (PDF, Excel, HTML) según lo que pida el restaurante. - Los repartidores de flota propia y los autónomos requieren, cada régimen, su
Contrato, suPolizaSeguroy suCalculadoraLiquidacion, siempre del mismo régimen. - Cada tipo de promoción (2x1, envío gratis, descuento porcentual) necesita crear su propio objeto
ReglaValidacion.
Soluciones
Solución 1:
public class PasarelaStripe implements PasarelaPago { /* cobrar con apiKeyStripe */ }
public class ImpuestosFrancia implements CalculadoraImpuestos {
private static final BigDecimal TVA_COMIDA = new BigDecimal("0.10");
@Override
public BigDecimal calcular(BigDecimal base) {
return base.multiply(TVA_COMIDA).setScale(2, RoundingMode.HALF_UP);
}
}
public class TicketFrancia implements FormateadorTicket { /* SIRET, mentions légales */ }
public class FabricaFrancia implements FabricaMercado {
private final ConfiguracionPideYa config;
public FabricaFrancia(ConfiguracionPideYa config) { this.config = config; }
@Override public PasarelaPago crearPasarelaPago() { return new PasarelaStripe(config.getApiKeyStripe()); }
@Override public CalculadoraImpuestos crearCalculadoraImpuestos() { return new ImpuestosFrancia(); }
@Override public FormateadorTicket crearFormateadorTicket() { return new TicketFrancia(); }
}Ficheros modificados: solo la raíz de composición (el switch de arranque gana el caso FR). Cliente, interfaces y fábricas existentes: intactos.
Solución 2: destruye la invariante de coherencia de familia, que es la única razón de existir del patrón. Al parametrizar cada producto con su propio país, vuelve a ser expresable la combinación PasarelaConekta + ImpuestosEspana (basta construir new FabricaFlexible(MX, ES, MX)): exactamente el incidente de producción que motivó el patrón, ahora con más ceremonia. Es una Abstract Factory de fachada con la seguridad de tres switch sueltos.
Solución 3:
- Factory Method (o incluso simple factory): un solo producto (
InformeVentas), sin coherencia con otros objetos. - Abstract Factory: tres productos por régimen que deben ser mutuamente coherentes (contrato de autónomo con seguro de flota sería un problema legal): familia de manual.
- Factory Method: cada promoción (creador) fabrica su producto (
ReglaValidacion); un producto por creador, sin familias.
Conclusión
Abstract Factory eleva el movimiento de la lección anterior del producto a la familia: una interfaz con un método de creación por pieza, una implementación por variante, y la garantía —por tipos, no por disciplina— de que las piezas nunca se mezclan entre variantes. Has visto su geometría de matriz (variantes × productos), su asimetría de costes (variante nueva barata, producto nuevo caro) y la frontera con Factory Method: un método frente a un objeto, un producto frente a una familia coherente.
Los dos patrones de fábrica resuelven qué clase instanciar. El siguiente reto es distinto: a veces la clase está clarísima, y lo endiablado es el proceso de montarla, con diez parámetros, la mitad opcionales, y reglas de validación cruzadas. Es la historia del constructor telescópico de Pedido que dejamos pendiente en la introducción, y se resuelve construyendo paso a paso: nos vemos en Builder.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
