← dev-notes
GOF · COMPORTAMIENTO

Patrones de
Comportamiento

Cinco patrones clave, cada uno con problema típico y solución en Java

01Strategy
DEFINICIÓN

Define una familia de algoritmos, los encapsula y los hace intercambiables. El cliente trabaja contra la estrategia abstracta, no contra ramas por tipo.

Cuándo se rompe. Un método de precio o envío crece con switch (customerTier) o if (region) cada vez que negocio añade una regla.

Sin patrón — lógica de descuento centralizada con condicionales

SWITCH QUE CRECE SIN CONTROL
public final class QuoteService {

    public Money discount(Money subtotal, Customer customer) {
        return switch (customer.tier()) {
            case VIP -> subtotal.multiply(0.85);
            case EMPLOYEE -> subtotal.multiply(0.90);
            default -> seasonalPercentOff(subtotal, customer.region());
        };
    }

    private Money seasonalPercentOff(Money subtotal, Region region) {
        if (region == Region.APAC && isHolidaySeason()) return subtotal.multiply(0.93);
        return subtotal;
    }
}

Con Strategy — una política por clase, selección en un solo sitio

ESTRATEGIA INTERCAMBIABLE
public interface PricingStrategy {
    Money discounted(Money subtotal, Customer customer);
}

public final class VipPricing implements PricingStrategy {
    public Money discounted(Money subtotal, Customer c) {
        return subtotal.multiply(0.85);
    }
}

public final class EmployeePricing implements PricingStrategy {
    public Money discounted(Money subtotal, Customer c) {
        return subtotal.multiply(0.90);
    }
}

public final class SeasonalRegionalPricing implements PricingStrategy {
    public Money discounted(Money subtotal, Customer c) {
        if (c.region() == Region.APAC && isHolidaySeason()) return subtotal.multiply(0.93);
        return subtotal;
    }
    private boolean isHolidaySeason() { return true; }
}

public final class QuoteService {
    private final PricingStrategy pricing;

    public QuoteService(PricingStrategy pricing) {
        this.pricing = pricing;
    }

    public Money discount(Money subtotal, Customer customer) {
        return pricing.discounted(subtotal, customer);
    }
}
02State
DEFINICIÓN

Permite que un objeto altere su comportamiento cuando su estado interno cambia; parece cambiar de clase. Sustituye condicionales masivos sobre el estado por polimorfismo.

Cuándo se rompe. Métodos como ship() con if (status == PAID) y transiciones ilegales detectadas tarde o con excepciones genéricas.

Sin patrón — transiciones mezcladas en la entidad

ORDEN CON ENUM Y MÉTODOS QUE MIRAN EL ESTADO
public final class Order {

    public enum Status { DRAFT, PAID, SHIPPED }
    private Status status = Status.DRAFT;

    public void pay() {
        if (status != Status.DRAFT) throw new IllegalStateException();
        status = Status.PAID;
    }

    public void ship() {
        if (status != Status.PAID) throw new IllegalStateException();
        status = Status.SHIPPED;
    }
}

Con State — cada estado encapsula qué operaciones son válidas

OBJETO CONTEXTO + ESTADOS POLIMÓRFICOS
public interface OrderState {
    OrderState pay(OrderContext ctx);
    OrderState ship(OrderContext ctx);
}

public final class DraftState implements OrderState {
    public OrderState pay(OrderContext ctx) { return new PaidState(); }
    public OrderState ship(OrderContext ctx) {
        throw new IllegalStateException("No enviar en borrador");
    }
}

public final class PaidState implements OrderState {
    public OrderState pay(OrderContext ctx) {
        throw new IllegalStateException("Ya pagada");
    }
    public OrderState ship(OrderContext ctx) { return new ShippedState(); }
}

public final class ShippedState implements OrderState {
    public OrderState pay(OrderContext ctx) {
        throw new IllegalStateException("Ya enviada");
    }
    public OrderState ship(OrderContext ctx) {
        throw new IllegalStateException("Ya enviada");
    }
}

public final class OrderContext {
    private OrderState state = new DraftState();

    public void pay() { state = state.pay(this); }
    public void ship() { state = state.ship(this); }
    public OrderState state() { return state; }
}
03Observer
DEFINICIÓN

Define una dependencia uno-a-muchos entre objetos: cuando el sujeto cambia, notifica a todos los observadores registrados sin acoplarlos al emisor del evento.

Cuándo se rompe. Tras crear un pedido, el servicio llama en línea a correo, analytics, antifraude y Slack; cada nuevo canal modifica la misma clase.

Sin patrón — orquestación rígida en el agregado

DOMINIO CONOCE TODOS LOS EFECTOS SECUNDARIOS
public final class OrderService {

    public void place(Order order) {
        orders.save(order);
        emailClient.sendOrderConfirmation(order.customerEmail());
        analytics.track("order_placed", order.id());
        fraudScoring.review(order);
        slack.notifyWarehouse(order.id());
    }
}

Con Observer — publicar evento; reacciones desacopladas

SUJETO + LISTENERS
public interface OrderListener {
    void onOrderPlaced(Order order);
}

public final class OrderEventPublisher {
    private final List<OrderListener> listeners = new CopyOnWriteArrayList<>();

    public void subscribe(OrderListener l) { listeners.add(l); }

    public void publishPlaced(Order order) {
        for (OrderListener l : listeners) l.onOrderPlaced(order);
    }
}

public final class OrderService {

    private final OrderRepository orders;
    private final OrderEventPublisher events;

    public OrderService(OrderRepository orders, OrderEventPublisher events) {
        this.orders = orders;
        this.events = events;
    }

    public void place(Order order) {
        orders.save(order);
        events.publishPlaced(order);
    }
}

// Registro al arranque: publisher.subscribe(o -> emailClient.send...); etc.
04Command
DEFINICIÓN

Encapsula una solicitud como objeto, permitiendo parametrizar clientes con colas, registros, operaciones reversibles o ejecución diferida.

Cuándo se rompe. Un worker procesa “mensajes” genéricos con un switch (type) enorme; no hay trazabilidad ni reutilización de la acción como objeto.

Sin patrón — dispatcher con switch por tipo de trabajo

PROCESADOR PROCEDIMENTAL
public final class JobWorker {

    public void handle(InboxMessage msg) {
        switch (msg.type()) {
            case "CREATE_INVOICE" -> invoiceService.create(msg.payload());
            case "REFUND" -> payments.refund(msg.payload());
            default -> throw new IllegalArgumentException(msg.type());
        }
    }
}

Con Command — cada trabajo es un objeto ejecutable

COMANDO + COLA / INVOKER
public interface JobCommand {
    void run();
}

public final class CreateInvoiceCommand implements JobCommand {
    private final InvoiceService invoices;
    private final String payload;

    public CreateInvoiceCommand(InvoiceService invoices, String payload) {
        this.invoices = invoices;
        this.payload = payload;
    }
    public void run() { invoices.create(payload); }
}

public final class RefundCommand implements JobCommand {
    private final PaymentService payments;
    private final String payload;

    public RefundCommand(PaymentService payments, String payload) {
        this.payments = payments;
        this.payload = payload;
    }
    public void run() { payments.refund(payload); }
}

public final class JobWorker {

    public void handle(JobCommand command) {
        command.run(); // aquí podrías envolver retry, métricas, auditoría...
    }
}
05Template Method
DEFINICIÓN

Define el esqueleto de un algoritmo en una operación, delegando algunos pasos a subclases. Las subclases redefinen pasos sin cambiar la estructura del flujo.

Cuándo se rompe. Varios jobs de importación copian el mismo flujo (abrir, parsear, validar, guardar) con pequeñas variaciones en cada copia.

Sin patrón — copiar-pegar el pipeline con diferencias mínimas

DUPLICACIÓN DEL FLUJO
public final class CsvUserImportJob {
    public void run(Path file) {
        var lines = Files.readAllLines(file);
        for (String line : lines) {
            var row = line.split(",");
            if (row.length < 3) throw new IllegalArgumentException();
            users.save(new User(row[0], row[1], row[2]));
        }
    }
}

public final class JsonUserImportJob {
    public void run(Path file) {
        String json = Files.readString(file);
        // parse JSON array...
        for (/* each object */) {
            // validar campos...
            users.save(/* ... */);
        }
    }
}

Con Template Method — estructura fija, ganchos variables

CLASE ABSTRACTA CON PASOS FINALES Y HOOKS
public abstract class UserImportJob {

    private final UserRepository users;

    protected UserImportJob(UserRepository users) { this.users = users; }

    public final void run(Path file) throws IOException {
        List<String> rawRows = readRaw(file);
        for (String raw : rawRows) {
            UserRow row = parseRow(raw);
            validate(row);
            users.save(toUser(row));
        }
        onComplete();
    }

    protected abstract List<String> readRaw(Path file) throws IOException;
    protected abstract UserRow parseRow(String raw);
    protected void validate(UserRow row) { /* por defecto no-op */ }
    protected abstract User toUser(UserRow row);
    protected void onComplete() { }

    protected record UserRow(String id, String email, String name) {}
}

public final class CsvUserImportJob extends UserImportJob {
    public CsvUserImportJob(UserRepository users) { super(users); }

    protected List<String> readRaw(Path file) throws IOException {
        return Files.readAllLines(file);
    }
    protected UserRow parseRow(String line) {
        var p = line.split(",");
        if (p.length < 3) throw new IllegalArgumentException(line);
        return new UserRow(p[0], p[1], p[2]);
    }
    protected User toUser(UserRow r) { return new User(r.id(), r.email(), r.name()); }
}