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
- Se não há um procedimento compartilhado real em volta do passo de criação — só "escolher uma implementação e delegar totalmente a ela" — isso é Strategy, não Factory Method. O sinal é se a classe base de fato faz algo por conta própria (validação, configuração compartilhada) além de chamar o método de fábrica.
- Pra um objeto único sem família de variantes, um construtor comum ou um método de fábrica estático é mais simples — o Factory Method só compensa a complexidade quando subclasses genuinamente precisam trocar o produto sem tocar no algoritmo compartilhado.
- Se a "família de objetos relacionados" precisa se manter consistente como um conjunto (não só um produto por vez), isso é Abstract Factory quando chegar, não Factory Method.
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
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of
Reusable Object-Oriented Software. Addison-Wesley. — o Capítulo 3 formaliza o Factory
Method; o próprio exemplo condutor do livro (um editor de documentos adiando qual subclasse
de
Documentcriar) é o ancestral direto da estrutura deste módulo. - Liskov, B., & Wing, J. (1994). "A Behavioral Notion of Subtyping." ACM Transactions on
Programming Languages and Systems, 16(6), 1811–1841. — todo produto concreto retornado por
um método de fábrica precisa ser utilizável em qualquer lugar onde o tipo base
Producté esperado; essa é exatamente a substituibilidade que esse artigo formaliza, e exatamente o que permite queNotificationCreator.send()ePaymentProviderCreator.charge()permaneçam ignorantes de qual tipo concreto receberam de volta.
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);
}
}