← Todos os padrões

Abstract Factory

Creational · ver código-fonte no GitHub

Leia em: English · Português · Español

Category: Creational

O problema

Alguns produtos só fazem sentido em famílias: um botão do Windows ao lado de um checkbox do Mac fica visualmente e comportamentalmente errado, um documento de apólice nacional pareado com uma taxa de prêmio internacional está simplesmente incorreto. Se cada ponto de chamada constrói cada produto com seu próprio new, nada impede um descompasso de família — o compilador não enxerga que WinButton e MacCheckbox deveriam andar juntos, e um erro de digitação ou uma linha copiada e colada silenciosamente produz um grafo de objetos inconsistente.

A solução

Agrupar os métodos de criação relacionados atrás de uma interface de fábrica, um método por produto da família. Uma implementação concreta da fábrica sempre retorna produtos da mesma família, então um ponto de chamada que depende só da interface da fábrica (nunca das classes concretas de produto) fisicamente não consegue misturar famílias — não sobra nenhuma chamada de construtor pra ele errar.

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

Exemplo clássico

classic/UiFactory é o kit de UI multiplataforma clássico dos livros: um Button e um Checkbox por família, WinUiFactory produzindo WinButton/WinCheckbox e MacUiFactory produzindo MacButton/MacCheckbox. UiFactoryTest verifica que cada fábrica renderiza os dois componentes no estilo daquela plataforma, nunca no da outra.

Exemplo aplicado: emissão de apólice de seguro nacional vs. internacional

applied/InsuranceProductFactory produz um PolicyDocument e um PremiumCalculator como uma única família: DomesticInsuranceProductFactory sempre pareia um documento em formato nacional com a taxa nacional de 2%, InternationalInsuranceProductFactory sempre pareia o documento em formato internacional com a taxa internacional de 3,5%. InsuranceProductIssuer depende só da interface InsuranceProductFactory — trocar a família de produto inteira de uma apólice é um argumento de construtor, nunca um branch dentro da lógica de emissão em si.

Este módulo também é um dos dois do catálogo (junto com Singleton) que traz o Spring Context de propósito: DomesticInsuranceConfig é uma classe @Configuration cujos métodos @Bean são, na prática, os mesmos métodos de criação de DomesticInsuranceProductFactory — só que resolvidos pelo container em vez de chamados manualmente. O padrão é o mesmo dos dois jeitos; só quem invoca os métodos de criação muda. InsuranceProductIssuerTest cobre as duas fábricas artesanais de ponta a ponta, e DomesticInsuranceConfigTest verifica que a família gerenciada pelo Spring é igualmente coerente.

Quando não usar

Cobertura de testes

100% de cobertura de instrução (cobertura de branch reporta "n/a" — nada neste módulo ramifica; é tudo delegação direta pra família certa). Reproduza você mesmo:

./gradlew :creational:abstractfactory:jacocoTestReport

Relatório em creational/abstractfactory/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

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 relatório completo de cobertura JaCoCo →