Guía Práctica de Design Patterns
Guía para seleccionar y aplicar el design pattern correcto para problemas comunes de ingeniería de software.
Resumen
Los design patterns son soluciones reutilizables a problemas comunes de diseño de software. Saber cuándo aplicar un pattern es tan importante como saber cómo hacerlo. Esta guía te ayuda a elegir el pattern correcto para cada situación.
Patrones Creacionales
Los patrones creacionales manejan mecanismos de creación de objetos.
Factory Method
Usar cuando: Necesitas crear objetos sin especificar la clase exacta.
from abc import ABC, abstractmethod
class Notification(ABC):
@abstractmethod
def send(self, message: str): pass
class EmailNotification(Notification):
def send(self, message: str):
print(f"Email: {message}")
class NotificationFactory:
@staticmethod
def create(type: str):
if type == "email": return EmailNotification()
raise ValueError(f"Tipo desconocido: {type}")
# Uso
notifier = NotificationFactory.create("email")
notifier.send("Hola!")
Cuándo usar: Múltiples implementaciones de una interfaz, elegidas en runtime.
Builder
Usar cuando: Necesitas construir objetos complejos paso a paso.
class QueryBuilder {
private parts: string[] = [];
select(columns: string[]): this {
this.parts.push(`SELECT ${columns.join(', ')}`);
return this;
}
from(table: string): this {
this.parts.push(`FROM ${table}`);
return this;
}
build(): string {
return this.parts.join(' ') + ';';
}
}
// Uso
const query = new QueryBuilder()
.select(['id', 'name', 'email'])
.from('users')
.build();
Cuándo usar: Objetos con muchos parámetros opcionales, o lógica de construcción compleja.
Patrones Estructurales
Los patrones estructurales manejan la composición de objetos.
Adapter
Usar cuando: Necesitas hacer que interfaces incompatibles funcionen juntas.
class OldPrinter:
def old_print(self, text):
print(f"OLD: {text}")
class PrinterAdapter:
def __init__(self, old_printer):
self._printer = old_printer
def print(self, text):
self._printer.old_print(text)
# Uso
adapter = PrinterAdapter(OldPrinter())
adapter.print("Hola") # Funciona con nueva interfaz
Cuándo usar: Integrando código legacy, librerías de terceros, o APIs con interfaces diferentes.
Decorator
Usar cuando: Necesitas agregar responsabilidades a objetos dinámicamente.
from functools import wraps
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} tomó {time.time() - start:.2f}s")
return result
return wrapper
@timing
def fetch_data():
# ... operación lenta
return data
Cuándo usar: Extender funcionalidad sin herencia (logging, caching, validación, retries).
Patrones de Comportamiento
Los patrones de comportamiento se enfocan en la comunicación entre objetos.
Observer
Usar cuando: Necesitas un mecanismo publish-subscribe.
class EventEmitter {
private listeners: Map<string, Function[]> = new Map();
on(event: string, callback: Function): void {
if (!this.listeners.has(event)) this.listeners.set(event, []);
this.listeners.get(event)!.push(callback);
}
emit(event: string, data: any): void {
this.listeners.get(event)?.forEach(cb => cb(data));
}
}
// Uso
const emitter = new EventEmitter();
emitter.on('user:login', (user) => console.log(`${user.name} inició sesión`));
emitter.emit('user:login', { name: 'Alice' });
Cuándo usar: Arquitecturas event-driven, actualizaciones en tiempo real, sistemas desacoplados.
Strategy
Usar cuando: Necesitas algoritmos intercambiables.
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float): pass
class CreditCardPayment(PaymentStrategy):
def pay(self, amount: float):
print(f"Pagado ${amount} con tarjeta de crédito")
class PayPalPayment(PaymentStrategy):
def pay(self, amount: float):
print(f"Pagado ${amount} con PayPal")
class ShoppingCart:
def __init__(self, strategy: PaymentStrategy):
self.strategy = strategy
def checkout(self, amount: float):
self.strategy.pay(amount)
# Uso
cart = ShoppingCart(PayPalPayment())
cart.checkout(99.99)
Cuándo usar: Diferentes algoritmos para la misma tarea (sorting, payment, reglas de validación).
Cheat Sheet de Selección de Patterns
| Problema | Pattern |
|---|---|
| ”Necesito exactamente una instancia” | Singleton |
| ”Creo objetos basados en un string/tipo” | Factory Method |
| ”Este objeto tiene 10 parámetros opcionales” | Builder |
| ”El código legacy no coincide con mi interfaz” | Adapter |
| ”Necesito agregar logging a todo” | Decorator |
| ”Los componentes necesitan reaccionar a eventos” | Observer |
| ”Quiero intercambiar algoritmos en runtime” | Strategy |
| ”Necesito abstraer el acceso a base de datos” | Repository |
| ”Necesito rastrear y deshacer cambios” | Command + Memento |
Lo que funciona
- No fuerces patterns: No todo problema necesita un pattern
- Empieza simple: Refactoriza hacia un pattern cuando aparezca duplicación. Consulta código limpio.
- El nombre importa: Usa nombres de pattern en clases (
UserRepository,EmailStrategy) - Documenta la intención: Explica por qué elegiste un pattern, no solo qué hace
Errores Comunes
- Over-engineering: aplicar patterns a problemas triviales
- Explosión de patterns: usar demasiados patterns en un módulo
- Ignorar los idiomas del lenguaje: no todos los patterns encajan en todos los lenguajes
Temas Avanzados
Escenario: Patrones en un Sistema de Pedidos
Sistema: Procesamiento de pedidos e-commerce
Patrones aplicados: Strategy, Factory, Observer, Decorator, Command
1. Strategy - Calculo de envio:
interface ShippingStrategy {
calculate(weight: number): number;
}
class StandardShipping implements ShippingStrategy {
calculate(weight: number) { return weight * 0.5; }
}
class ExpressShipping implements ShippingStrategy {
calculate(weight: number) { return weight * 1.5; }
}
class SameDayShipping implements ShippingStrategy {
calculate(weight: number) { return weight * 3.0; }
}
// Uso: seleccionar estrategia en runtime
const shipping = strategies[order.shippingMethod];
const cost = shipping.calculate(order.totalWeight);
2. Factory - Creacion de notificaciones:
class NotificationFactory {
create(type: string): Notification {
switch (type) {
case "email": return new EmailNotification();
case "sms": return new SMSNotification();
case "push": return new PushNotification();
default: throw new Error("Tipo no soportado");
}
}
}
3. Observer - Eventos de pedido:
class OrderEventBus {
private handlers: Map<string, Function[]> = new Map();
on(event: string, handler: Function) {
if (!this.handlers.has(event)) this.handlers.set(event, []);
this.handlers.get(event).push(handler);
}
emit(event: string, data: any) {
this.handlers.get(event)?.forEach(h => h(data));
}
}
// Suscriptores: inventario, email, analytics
bus.on("order.created", updateInventory);
bus.on("order.created", sendConfirmation);
bus.on("order.created", trackAnalytics);
4. Decorator - Logging y cache:
function withLogging(fn: Function) {
return async (...args: any[]) => {
console.log("Calling:", fn.name, args);
const result = await fn(...args);
console.log("Result:", result);
return result;
};
}
function withCache(fn: Function, ttl: number) {
const cache = new Map();
return async (...args: any[]) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = await fn(...args);
cache.set(key, result);
setTimeout(() => cache.delete(key), ttl);
return result;
};
}
// Composicion: logging + cache
const cachedLoggedFetch = withCache(withLogging(fetchProduct), 60000);
5. Command - Operaciones de pedido:
interface Command { execute(): Promise<void>; }
class CancelOrderCommand implements Command {
constructor(private order: Order, private inventory: Inventory) {}
async execute() {
await this.inventory.release(this.order.items);
await this.order.update({ status: "cancelled" });
}
}
// Permite undo, queue, y logging de operaciones
Anti-patrones a evitar:
- Singleton para todo (acoplamiento global)
- Factory cuando un constructor basta
- Observer sin unsubscribe (memory leaks)
- Decorator stacking excesivo (> 3 niveles)
- Command sin undo (pierde la mitad del valor)
Lecciones:
- Aplica patrones cuando el problema lo requiere, no antes
- Los lenguajes modernos absorben patrones en su stdlib
- Composicion > herencia en la mayoria de casos
- Cada patron agrega complejidad: midela contra el valor
- Refactoriza hacia patrones, no los disenes desde el inicio
Como se relacionan los patrones con SOLID?
Strategy implementa Open/Closed (nuevas estrategias sin cambiar codigo existente). Factory implementa Single Responsibility (creacion separada de uso). Observer implementa Dependency Inversion (depende de abstraccion, no concrecion). Decorator implementa Open/Closed (extiende sin modificar). Command implementa Single Responsibility (cada comando una responsabilidad).
Preguntas frecuentes
Cuándo debería usar un design pattern?
Usa un design pattern cuando encuentres un problema que resuelve, no antes. Combina con principios SOLID. Empieza con código simple y refactoriza hacia un pattern cuando veas duplicación, complejidad o acoplamiento que un pattern resolvería.
Son relevantes los design patterns en lenguajes modernos?
Sí, pero los lenguajes modernos a menudo absorben patterns en sus librerías estándar. Por ejemplo, Promise de JavaScript es el pattern Observer, y los decorators de Python implementan el pattern Decorator nativamente.
Cuántos patterns debería usar en un módulo?
Usa tantos como necesites, pero no más. Cada pattern agrega carga cognitiva. Si un módulo usa más de 2-3 patterns, considera si está haciendo demasiado y debería dividirse.
Recursos Relacionados
Asegurar una Única Instancia con el Singleton Pattern
Cómo garantizar exactamente una instancia de una clase en una aplicación usando inicialización perezosa, creación thread-safe y singletons basados en registro.
RecipeImplementar Sistemas Reactivos con el Observer Pattern
Cómo construir sistemas event-driven y reactivos usando el observer pattern con pub/sub, event emitters y reactive streams en JavaScript, Java y Python.
RecipeIntercambiar Algoritmos en Runtime con el Strategy Pattern
Cómo encapsular algoritmos y comportamientos intercambiables usando el strategy pattern con inyección de dependencias, function pointers y lambda strategies en Java, TypeScript y Python.
PatternPatrón Repository
Abstrae la lógica de acceso a datos detrás de una interfaz limpia. Patrón de diseño arquitectural para capas de datos testeables y mantenibles.
GuideDomain-Driven Design (DDD): Guía Práctica
Aprende los fundamentos de DDD: bounded contexts, entidades, value objects, aggregates, y cómo modelar dominios de negocio complejos en código.
GuideLo que Funciona en Code Review — Para Autores y Revisores
Una guía práctica para revisiones de código útiles: cómo escribir código revisable, dar feedback constructivo y mantener las revisiones rápidas y enfocadas.