Cinco patrones clave, cada uno con problema típico y solución en Java
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
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
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);
}
}
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
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
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; }
}
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
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
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.
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
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
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...
}
}
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
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
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()); }
}