← Todos los patrones

Abstract Factory

Creational · ver código fuente en GitHub

Leer en: English · Português · Español

Category: Creational

El problema

Algunos productos solo tienen sentido en familias: un botón de Windows junto a un checkbox de Mac se ve y se comporta mal, un documento de póliza nacional emparejado con una tasa de prima internacional es simplemente incorrecto. Si cada punto de llamada construye cada producto con su propio new, nada impide un desajuste de familia — el compilador no puede ver que WinButton y MacCheckbox debían ir juntos, y un error de tipeo o una línea copiada y pegada produce silenciosamente un grafo de objetos inconsistente.

La solución

Agrupar los métodos de creación relacionados detrás de una interfaz de fábrica, un método por producto de la familia. Una implementación concreta de la fábrica siempre devuelve productos de la misma familia, así que un punto de llamada que depende solo de la interfaz de la fábrica (nunca de las clases concretas de producto) físicamente no puede mezclar familias — no queda ninguna llamada a constructor que pueda equivocarse.

classDiagram
    class AbstractFactory {
        <<interface>>
        +createProductA() ProductA
        +createProductB() ProductB
    }
    class ConcreteFactory1
    class ConcreteFactory2
    class ProductA1
    class ProductA2
    class ProductB1
    class ProductB2
    AbstractFactory <|.. ConcreteFactory1
    AbstractFactory <|.. ConcreteFactory2
    ConcreteFactory1 ..> ProductA1 : creates
    ConcreteFactory1 ..> ProductB1 : creates
    ConcreteFactory2 ..> ProductA2 : creates
    ConcreteFactory2 ..> ProductB2 : creates

Ejemplo clásico

classic/UiFactory es el kit de UI multiplataforma clásico de los libros: un Button y un Checkbox por familia, WinUiFactory produciendo WinButton/WinCheckbox y MacUiFactory produciendo MacButton/MacCheckbox. UiFactoryTest verifica que cada fábrica renderiza sus dos componentes en el estilo propio de esa plataforma, nunca en el de la otra.

Ejemplo aplicado: emisión de póliza de seguro nacional vs. internacional

applied/InsuranceProductFactory produce un PolicyDocument y un PremiumCalculator como una sola familia: DomesticInsuranceProductFactory siempre empareja un documento en formato nacional con la tasa nacional del 2%, InternationalInsuranceProductFactory siempre empareja el documento en formato internacional con la tasa internacional del 3,5%. InsuranceProductIssuer depende solo de la interfaz InsuranceProductFactory — cambiar toda la familia de producto de una póliza es un argumento de constructor, nunca una rama dentro de la lógica de emisión misma.

Este módulo también es uno de los dos del catálogo (junto con Singleton) que incorpora Spring Context a propósito: DomesticInsuranceConfig es una clase @Configuration cuyos métodos @Bean son, en la práctica, los mismos métodos de creación que DomesticInsuranceProductFactory — solo que resueltos por el contenedor en vez de llamados a mano. El patrón es el mismo en ambos casos; solo cambia quién invoca los métodos de creación. InsuranceProductIssuerTest cubre ambas fábricas artesanales de punta a punta, y DomesticInsuranceConfigTest verifica que la familia gestionada por Spring sea igual de coherente.

Cuándo no usarlo

Cobertura de pruebas

100% de cobertura de instrucciones (la cobertura de ramas reporta "n/a" — nada en este módulo ramifica; es todo delegación directa a la familia correcta). Reprodúzcalo usted mismo:

./gradlew :creational:abstractfactory:jacocoTestReport

Informe en creational/abstractfactory/build/reports/jacoco/test/html/index.html.

Lecturas adicionales

Pruebas unitarias

src/test/java/com/designpatterns/creational/abstractfactory/classic/UiFactoryTest.java
package com.designpatterns.creational.abstractfactory.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class UiFactoryTest {

    @Test
    void theWindowsFactoryProducesOnlyWindowsComponents() {
        UiFactory factory = new WinUiFactory();

        assertThat(factory.createButton().render()).isEqualTo("[Windows Button]");
        assertThat(factory.createCheckbox().render()).isEqualTo("[Windows Checkbox]");
    }

    @Test
    void theMacFactoryProducesOnlyMacComponents() {
        UiFactory factory = new MacUiFactory();

        assertThat(factory.createButton().render()).isEqualTo("(Mac Button)");
        assertThat(factory.createCheckbox().render()).isEqualTo("(Mac Checkbox)");
    }
}
src/test/java/com/designpatterns/creational/abstractfactory/applied/DomesticInsuranceConfigTest.java
package com.designpatterns.creational.abstractfactory.applied;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

class DomesticInsuranceConfigTest {

    private AnnotationConfigApplicationContext context;

    @AfterEach
    void closeContext() {
        if (context != null) {
            context.close();
        }
    }

    @Test
    void theConfigurationClassProducesACoherentDomesticBeanFamily() {
        context = new AnnotationConfigApplicationContext(DomesticInsuranceConfig.class);

        PolicyDocument document = context.getBean(PolicyDocument.class);
        PremiumCalculator calculator = context.getBean(PremiumCalculator.class);

        assertThat(document).isInstanceOf(DomesticPolicyDocument.class);
        assertThat(calculator).isInstanceOf(DomesticPremiumCalculator.class);
        assertThat(calculator.calculatePremiumCents(100_000_00L)).isEqualTo(200000L);
    }
}
src/test/java/com/designpatterns/creational/abstractfactory/applied/InsuranceProductIssuerTest.java
package com.designpatterns.creational.abstractfactory.applied;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class InsuranceProductIssuerTest {

    @Test
    void theDomesticFactoryIssuesADomesticPolicyAtTheDomesticRate() {
        InsuranceProductIssuer issuer = new InsuranceProductIssuer(new DomesticInsuranceProductFactory());

        String result = issuer.issuePolicy("Maria Silva", 100_000_00L);

        assertThat(result).isEqualTo("APOLICE NACIONAL - Segurado: Maria Silva | Premium: 200000 cents");
    }

    @Test
    void theInternationalFactoryIssuesAnInternationalPolicyAtTheInternationalRate() {
        InsuranceProductIssuer issuer = new InsuranceProductIssuer(new InternationalInsuranceProductFactory());

        String result = issuer.issuePolicy("John Smith", 100_000_00L);

        assertThat(result).isEqualTo("INTERNATIONAL POLICY - Insured: John Smith | Premium: 350000 cents");
    }
}

Ver informe completo de cobertura JaCoCo →