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

  1. El problema en PideYa: la expansión internacional
  2. Intención y estructura del patrón
  3. Implementación Java completa
  4. Añadir un mercado y añadir un producto: la asimetría clave
  5. Relación y diferencia con Factory Method
  6. Cuándo usarlo y cuándo no
  7. 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ñadir crearValidadorDireccionFiscal() a la interfaz FabricaMercado... 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 ServicioCheckout aparece un if (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 FabricaMercado productos 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 FabricaDePruebas que 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, no FabricaConekta): 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:

  1. Crear el objeto InformeVentas adecuado (PDF, Excel, HTML) según lo que pida el restaurante.
  2. Los repartidores de flota propia y los autónomos requieren, cada régimen, su Contrato, su PolizaSeguro y su CalculadoraLiquidacion, siempre del mismo régimen.
  3. 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:

  1. Factory Method (o incluso simple factory): un solo producto (InformeVentas), sin coherencia con otros objetos.
  2. 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.
  3. 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

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados