← Todos los patrones

Factory Method

Creational · ver código fuente en GitHub

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

Category: Creational

El problema

Una clase tiene un procedimiento fijo que ejecutar, pero un paso de ese procedimiento — qué objeto concreto crear — necesita variar. Poner new ConcreteThing() directo en el procedimiento lo ata a una subclase específica, de modo que soportar una variante nueva significa editar código que ya funciona, y la lógica del propio procedimiento (validación, configuración compartida) termina duplicada en todo lugar que también necesita elegir una variante.

La solución

Poner el procedimiento fijo en una clase base, y aplazar la decisión de "qué objeto crear" a un método abstracto que las subclases sobrescriben. La clase base llama a su propio método de fábrica abstracto de forma polimórfica — nunca necesita saber qué producto concreto va a recibir realmente.

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

Ejemplo clásico

classic/NotificationCreator define send(recipient, message) una única vez — incluyendo un paso de validación que cada subclase hereda gratis — y aplaza createNotification() a EmailNotificationCreator y SmsNotificationCreator. Ninguna subclase toca send(); solo dicen qué Notification se construye. NotificationCreatorTest verifica que ambos creators concretos enrutan al tipo de notificación correcto, y que la validación compartida en la clase base se aplica a ambos sin que ninguna subclase tenga que implementarla.

Ejemplo aplicado: selección de proveedor de pago

applied/PaymentProviderCreator mantiene un paso compartido real — validación de monto — y aplaza createProvider() a PixPaymentProviderCreator, BoletoPaymentProviderCreator, y CreditCardPaymentProviderCreator. PaymentCheckout busca el creator correcto por PaymentMethod y le llama charge() — un gateway de pagos real que agrega un cuarto método más adelante significa agregar una clase de creator nueva, y obtiene el paso de validación de monto gratis, sin copiarlo. PaymentCheckoutTest cubre los tres proveedores, la validación compartida disparándose sin importar el método, y el caso de falla de método no registrado.

Cuándo no usarlo

Cobertura de pruebas

100% de cobertura de instrucciones, 100% de cobertura de ramas (JaCoCo). Reprodúzcalo usted mismo:

./gradlew :creational:factorymethod:jacocoTestReport

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

Lecturas adicionales

Pruebas unitarias

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 informe completo de cobertura JaCoCo →