Habilidad de Patrones de Diseño
Referencia práctica de patrones de diseño para Java con ejemplos modernos.
Cuándo usar
- El usuario solicita implementar un patrón específico
- Diseño de componentes extensibles/flexibles
- Refactorización de estructuras de código rígidas
- Revisión de código que sugiere uso de patrones
Referencia rápida: Cuándo usar qué
| Problema | Patrón |
|---|---|
| Construcción de objetos complejos | Builder |
| Crear objetos sin especificar la clase | Factory |
| Múltiples algoritmos, intercambiables en tiempo de ejecución | Strategy |
| Agregar comportamiento sin modificar la clase | Decorator |
| Notificar a múltiples objetos sobre cambios | Observer |
| Garantizar una única instancia | Singleton |
| Convertir interfaces incompatibles | Adapter |
| Definir un esqueleto de algoritmo | Template Method |
Patrones creacionales
Builder
Úsalo cuando: El objeto tiene muchos parámetros, algunos opcionales.
// ❌ Antipatrón de constructores telescópicos
public class User {
public User(String name) { }
public User(String name, String email) { }
public User(String name, String email, int age) { }
public User(String name, String email, int age, String phone) { }
// ... explosión de constructores
}
// ✅ Patrón Builder
public class User {
private final String name; // requerido
private final String email; // requerido
private final int age; // opcional
private final String phone; // opcional
private final String address; // opcional
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public static Builder builder(String name, String email) {
return new Builder(name, email);
}
public static class Builder {
// Requeridos
private final String name;
private final String email;
// Opcionales con valores predeterminados
private int age = 0;
private String phone = "";
private String address = "";
private Builder(String name, String email) {
this.name = name;
this.email = email;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
// Uso
User user = User.builder("Juan", "[email protected]")
.age(30)
.phone("+1234567890")
.build();
Con Lombok:
@Builder
@Getter
public class User {
private final String name;
private final String email;
@Builder.Default private int age = 0;
private String phone;
}
Factory Method
Úsalo cuando: Necesitas crear objetos sin especificar la clase exacta.
// ✅ Patrón Factory Method
public interface Notification {
void send(String message);
}
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Push: " + message);
}
}
// Fábrica
public class NotificationFactory {
public static Notification create(String type) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification();
case "SMS" -> new SmsNotification();
case "PUSH" -> new PushNotification();
default -> throw new IllegalArgumentException("Tipo desconocido: " + type);
};
}
}
// Uso
Notification notification = NotificationFactory.create("EMAIL");
notification.send("¡Hola!");
Con Spring (recomendado):
public interface NotificationSender {
void send(String message);
String getType();
}
@Component
public class EmailSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "EMAIL"; }
}
@Component
public class SmsSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "SMS"; }
}
@Component
public class NotificationFactory {
private final Map<String, NotificationSender> senders;
public NotificationFactory(List<NotificationSender> senderList) {
this.senders = senderList.stream()
.collect(Collectors.toMap(
NotificationSender::getType,
Function.identity()
));
}
public NotificationSender getSender(String type) {
return Optional.ofNullable(senders.get(type))
.orElseThrow(() -> new IllegalArgumentException("Desconocido: " + type));
}
}
Singleton
Úsalo cuando: Se necesita exactamente una instancia (¡úsalo con moderación!).
// ✅ Singleton moderno (basado en enum, seguro para hilos)
public enum DatabaseConnection {
INSTANCE;
private Connection connection;
DatabaseConnection() {
// Inicializar conexión
}
public Connection getConnection() {
return connection;
}
}
// Uso
Connection conn = DatabaseConnection.INSTANCE.getConnection();
Con Spring (recomendado):
@Component // El ámbito predeterminado es singleton
public class DatabaseConnection {
// Spring gestiona la instancia única
}
Advertencia: Los Singletons pueden ser problemáticos:
- Difíciles de probar (estado global)
- Dependencias ocultas
- Considera la inyección de dependencias en su lugar
Patrones de comportamiento
Strategy
Úsalo cuando: Múltiples algoritmos para la misma operación, necesidad de intercambiarlos en tiempo de ejecución.
// ✅ Patrón Strategy
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Pagado " + amount + " con tarjeta " + cardNumber);
}
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Pagado " + amount + " vía PayPal: " + email);
}
}
public class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Pagado " + amount + " a la billetera: " + walletAddress);
}
}
// Contexto
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(BigDecimal total) {
paymentStrategy.pay(total);
}
}
// Uso
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("4111-1111-1111-1111"));
cart.checkout(new BigDecimal("99.99"));
// Cambiar estrategia en tiempo de ejecución
cart.setPaymentStrategy(new PayPalPayment("[email protected]"));
cart.checkout(new BigDecimal("49.99"));
Con Java 8+ (funcional):
// Estrategia como interfaz funcional
@FunctionalInterface
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
// Uso con lambdas
PaymentStrategy creditCard = amount ->
System.out.println("Pago con tarjeta: " + amount);
PaymentStrategy paypal = amount ->
System.out.println("Pago con PayPal: " + amount);
cart.setPaymentStrategy(creditCard);
Observer
Úsalo cuando: Los objetos necesitan ser notificados sobre cambios en otro objeto.
// ✅ Patrón Observer (Java moderno)
public interface OrderObserver {
void onOrderPlaced(Order order);
}
public class OrderService {
private final List<OrderObserver> observers = new ArrayList<>();
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void removeObserver(OrderObserver observer) {
observers.remove(observer);
}
public void placeOrder(Order order) {
// Procesar pedido
saveOrder(order);
// Notificar a todos los observadores
observers.forEach(observer -> observer.onOrderPlaced(order));
}
}
// Observadores
public class InventoryService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
// Reducir inventario
order.getItems().forEach(item ->
reduceStock(item.getProductId(), item.getQuantity())
);
}
}
public class EmailNotificationService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
sendConfirmationEmail(order.getCustomerEmail(), order);
}
}
public class AnalyticsService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
trackOrderEvent(order);
}
}
// Configuración
OrderService orderService = new OrderService();
orderService.addObserver(new InventoryService());
orderService.addObserver(new EmailNotificationService());
orderService.addObserver(new AnalyticsService());
Con Eventos de Spring (recomendado):
// Evento
public record OrderPlacedEvent(Order order) {}
// Publicador
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public void placeOrder(Order order) {
saveOrder(order);
eventPublisher.publishEvent(new OrderPlacedEvent(order));
}
}
// Oyentes (observadores)
@Component
public class InventoryListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// Reducir inventario
}
}
@Component
public class EmailListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// Enviar correo
}
@EventListener
@Async // Procesamiento asíncrono
public void handleOrderPlacedAsync(OrderPlacedEvent event) {
// Enviar correo de forma asíncrona
}
}
Template Method
Úsalo cuando: Definir un esqueleto de algoritmo y dejar que las subclases completen los pasos.
// ✅ Patrón Template Method
public abstract class DataProcessor {
// Método plantilla - define el algoritmo
public final void process() {
readData();
processData();
writeData();
if (shouldNotify()) {
notifyCompletion();
}
}
// Pasos a implementar por las subclases
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
// Gancho - sobrescritura opcional
protected boolean shouldNotify() {
return true;
}
protected void notifyCompletion() {
System.out.println("¡Procesamiento completado!");
}
}
public class CsvDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Leyendo archivo CSV...");
}
@Override
protected void processData() {
System.out.println("Procesando datos CSV...");
}
@Override
protected void writeData() {
System.out.println("Escribiendo en la base de datos...");
}
}
public class ApiDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Obteniendo datos de la API...");
}
@Override
protected void processData() {
System.out.println("Transformando respuesta de la API...");
}
@Override
protected void writeData() {
System.out.println("Escribiendo en la caché...");
}
@Override
protected boolean shouldNotify() {
return false; // Sobrescribir gancho
}
}
// Uso
DataProcessor csvProcessor = new CsvDataProcessor();
csvProcessor.process();
DataProcessor apiProcessor = new ApiDataProcessor();
apiProcessor.process();
Patrones estructurales
Decorator
Úsalo cuando: Agregar comportamiento dinámicamente sin modificar las clases existentes.
// ✅ Patrón Decorator
public interface Coffee {
String getDescription();
BigDecimal getCost();
}
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Café";
}
@Override
public BigDecimal getCost() {
return new BigDecimal("2.00");
}
}
// Decorador base
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public BigDecimal getCost() {
return coffee.getCost();
}
}
// Decoradores concretos
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Leche";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.50"));
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Azúcar";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.20"));
}
}
public class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Crema batida";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.75"));
}
}
// Uso
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " -> " + coffee.getCost());
// Agregar decoradores dinámicamente
Coffee milkCoffee = new MilkDecorator(coffee);
Coffee milkSugarCoffee = new SugarDecorator(milkCoffee);
Coffee fullyLoaded = new WhippedCreamDecorator(milkSugarCoffee);
System.out.println(fullyLoaded.getDescription() + " -> " + fullyLoaded.getCost());
// Salida: Café, Leche, Azúcar, Crema batida -> 3.45
Con interfaces funcionales (para operaciones simples):
// Función de decorado simple
@FunctionalInterface
public interface CoffeeMaker {
Coffee make();
default CoffeeMaker withMilk() {
return () -> new MilkDecorator(this.make());
}
default CoffeeMaker withSugar() {
return () -> new SugarDecorator(this.make());
}
default CoffeeMaker withWhippedCream() {
return () -> new WhippedCreamDecorator(this.make());
}
}
// Uso
CoffeeMaker maker = SimpleCoffee::new;
Coffee coffee = maker.withMilk().withSugar().withWhippedCream().make();
System.out.println(coffee.getDescription()); // Café, Leche, Azúcar, Crema batida
Resumen de mejores prácticas
Cuándo usar cada patrón
| Contexto | Patrón | Razón |
|---|---|---|
| El constructor es complejo | Builder | Mejor legibilidad, campos inmutables, parámetros opcionales |
| La creación es compleja | Factory | Encapsula la lógica de creación, desacopla el código del cliente |
| Comportamiento intercambiable | Strategy | Cumple el Principio Abierto/Cerrado, algoritmos intercambiables en tiempo de ejecución |
| Agregar características dinámicamente | Decorator | Composición sobre herencia, comportamiento en tiempo de ejecución |
| Notificaciones uno a muchos | Observer | Bajo acoplamiento, facilita la extensión |
| Fragmentos de algoritmo | Template Method | Reutiliza la estructura del algoritmo, permite personalización |
Anti-patrones a evitar
- ❌ Constructores telescópicos → Usa Builder
- ❌ Declaraciones switch masivas para la creación → Usa Factory con registro
- ❌ Herencia para comportamiento → Usa Strategy/Decorator
- ❌ Estado global mutable → Cuestiona Singleton, prefiere DI
- ❌ Llamadas de encadenamiento profundas → Mantén los decoradores simples
Enfoque preferido para Spring
Siempre que sea posible, utiliza las capacidades integradas de Spring:
- Inyección de dependencias en lugar de Factory manual
- Ámbito Singleton predeterminado en lugar de Singleton manual
- Eventos de aplicación en lugar de Observer manual
- AOP para decoradores transversales
- @Conditional para selección de estrategias