← Todos os padrões

Factory Method

Creational · ver código-fonte no GitHub

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

Category: Creational

O problema

Uma classe tem um procedimento fixo pra executar, mas um passo desse procedimento — qual objeto concreto criar — precisa variar. Colocar new ConcreteThing() direto no procedimento o amarra a uma subclasse específica, de modo que suportar uma variante nova significa editar código que já funciona, e a lógica do próprio procedimento (validação, configuração compartilhada) acaba duplicada em todo lugar que também precisa escolher uma variante.

A solução

Colocar o procedimento fixo numa classe base, e adiar a decisão de "qual objeto criar" pra um método abstrato que as subclasses sobrescrevem. A classe base chama seu próprio método de fábrica abstrato de forma polimórfica — ela nunca precisa saber qual produto concreto vai realmente receber.

classDiagram
    class Creator {
        +templateOperation()
        #createProduct() Product
    }
    class ConcreteCreatorA
    class ConcreteCreatorB
    class Product {
        <<interface>>
    }
    Creator <|-- ConcreteCreatorA
    Creator <|-- ConcreteCreatorB
    Creator --> Product : creates via factory method

Exemplo clássico

classic/NotificationCreator define send(recipient, message) uma única vez — incluindo um passo de validação que toda subclasse herda de graça — e adia createNotification() pra EmailNotificationCreator e SmsNotificationCreator. Nenhuma das subclasses toca em send(); elas só dizem qual Notification é construída. NotificationCreatorTest verifica que os dois creators concretos roteiam pro tipo certo de notificação, e que a validação compartilhada na classe base se aplica a ambos sem que nenhuma subclasse precise implementá-la.

Exemplo aplicado: seleção de provedor de pagamento

applied/PaymentProviderCreator mantém um passo compartilhado real — validação de valor — e adia createProvider() pra PixPaymentProviderCreator, BoletoPaymentProviderCreator, e CreditCardPaymentProviderCreator. PaymentCheckout busca o creator certo por PaymentMethod e chama charge() nele — um gateway de pagamento real adicionando um quarto método depois significa adicionar uma classe de creator nova, e ela ganha o passo de validação de valor de graça, sem copiá-lo. PaymentCheckoutTest cobre os três provedores, a validação compartilhada disparando independente do método, e o caso de falha de método não registrado.

Quando não usar

Cobertura de testes

100% de cobertura de instrução, 100% de cobertura de branch (JaCoCo). Reproduza você mesmo:

./gradlew :creational:factorymethod:jacocoTestReport

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

Leitura complementar

Testes unitários

src/test/java/com/designpatterns/creational/factorymethod/classic/NotificationCreatorTest.java
package com.designpatterns.creational.factorymethod.classic;

import org.junit.jupiter.api.Test;

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

class NotificationCreatorTest {

    @Test
    void emailCreatorRoutesThroughAnEmailNotification() {
        NotificationCreator creator = new EmailNotificationCreator();

        String result = creator.send("alice@example.com", "your order shipped");

        assertThat(result).isEqualTo("EMAIL to alice@example.com: your order shipped");
    }

    @Test
    void smsCreatorRoutesThroughAnSmsNotification() {
        NotificationCreator creator = new SmsNotificationCreator();

        String result = creator.send("+15551234567", "your order shipped");

        assertThat(result).isEqualTo("SMS to +15551234567: your order shipped");
    }

    @Test
    void theSharedValidationInSendAppliesToEveryCreatorSubclass() {
        NotificationCreator emailCreator = new EmailNotificationCreator();
        NotificationCreator smsCreator = new SmsNotificationCreator();

        assertThatThrownBy(() -> emailCreator.send("alice@example.com", " "))
                .isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> smsCreator.send("+15551234567", ""))
                .isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> emailCreator.send("alice@example.com", null))
                .isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/designpatterns/creational/factorymethod/applied/PaymentCheckoutTest.java
package com.designpatterns.creational.factorymethod.applied;

import org.junit.jupiter.api.Test;

import java.util.Map;

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

class PaymentCheckoutTest {

    private final PaymentCheckout checkout = PaymentCheckout.withDefaultCreators();

    @Test
    void routesToThePixProvider() {
        assertThat(checkout.charge(PaymentMethod.PIX, 5_000L))
                .isEqualTo("PIX charge of 5000 cents processed instantly");
    }

    @Test
    void routesToTheBoletoProvider() {
        assertThat(checkout.charge(PaymentMethod.BOLETO, 12_000L))
                .isEqualTo("Boleto issued for 12000 cents, due in 3 business days");
    }

    @Test
    void routesToTheCreditCardProvider() {
        assertThat(checkout.charge(PaymentMethod.CREDIT_CARD, 8_000L))
                .isEqualTo("Credit card charge of 8000 cents authorized");
    }

    @Test
    void theSharedValidationAppliesRegardlessOfMethod() {
        assertThatThrownBy(() -> checkout.charge(PaymentMethod.PIX, 0L))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsAnUnregisteredPaymentMethod() {
        PaymentCheckout partialCheckout = new PaymentCheckout(Map.of(PaymentMethod.PIX, new PixPaymentProviderCreator()));

        assertThatThrownBy(() -> partialCheckout.charge(PaymentMethod.BOLETO, 1_000L))
                .isInstanceOf(IllegalArgumentException.class);
    }
}

Ver relatório completo de cobertura JaCoCo →