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 |
Buenas Prácticas
- No fuerces patterns: No todo problema necesita un pattern
- Empieza simple: Refactoriza hacia un pattern cuando aparezca duplicación
- 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
Preguntas Frecuentes
Cuándo debería usar un design pattern?
Usa un design pattern cuando encuentres un problema que resuelve, no antes. 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.