Guía Práctica de Design Patterns
Elige el design pattern correcto para cada problema: Singleton, Factory, Observer, Strategy y más, con código ejecutable y trade-offs honestos.
Introducción
Tu código lleva seis meses en producción y la clase OrderService acaba de pasar las 900 líneas. Cada nueva funcionalidad implica tocar código que ya funcionaba, y los últimos tres bugs vinieron de cambios que se filtraron hacia lógica que no tenía nada que ver. Ese es el momento para el que se inventaron los design patterns: son respuestas con nombre, ya probadas, a problemas de diseño que aparecen en casi cualquier codebase.
El término viene de Design Patterns: Elements of Reusable Object-Oriented Software (Gamma, Helm, Johnson y Vlissides, 1994), el libro de la “Gang of Four” que catalogó 23 patrones en tres familias: creacionales (cómo se construyen los objetos), estructurales (cómo encajan entre sí) y de comportamiento (cómo se comunican). El catálogo tiene 30 años y sigue vigente, pero su valor nunca fue la lista en sí. Es el vocabulario compartido. “Envuélvelo en un adapter” cierra una discusión que de otro modo ocuparía una pizarra entera.
Esta guía es para developers que ya escriben clases o módulos y se preguntan si hay una forma más limpia de hacer algo. Cubre los 12 patrones que más usarás, cada uno con código ejecutable, la situación que lo pide y el caso en el que empeora las cosas. Para profundizar en un patrón concreto, cada sección enlaza a su recipe o página dedicada, y Refactoring.Guru mantiene un buen catálogo interactivo con los 23.
Cómo elegir un patrón
La forma fiable de elegir un patrón es nombrar el dolor antes de nombrar la solución. “Necesito patrones” no produce nada; “el generador de informes se rompe cada vez que toco el exportador” apunta a una respuesta real. La mayoría de decisiones sobre patrones se reducen a una de tres preguntas: cómo se crean los objetos, cómo se conectan las piezas o cómo fluye el comportamiento entre ellas.
Un segundo filtro útil: prefiere el patrón que elimina más acoplamiento por línea añadida. Un Factory que esconde un switch de tres líneas cuesta más de lo que ahorra; un Decorator que reemplaza cinco flags booleanos se paga solo desde el primer día.
Conviene tener clara una distinción: un patrón no es un principio ni un idiom. SOLID te dice cómo se ve un buen diseño; los idioms son trucos propios del lenguaje (el with de Python, el destructuring de JavaScript); los patrones son las estructuras reutilizables que quedan en medio. Aplicas principios para escribir el código, idioms para escribirlo bien, y patrones para organizar las piezas.
Patrones Creacionales
Los patrones creacionales controlan cómo nacen los objetos, sobre todo para que el código que los usa deje de preocuparse por las clases concretas. Los tres de esta sección cubren los casos que concentran casi todos los problemas de creación en la práctica: compartir una instancia, elegir la clase en runtime y ensamblar un objeto pieza a pieza.
Singleton
Úsalo cuando todo el proceso debería compartir un único objeto, como un pool de conexiones, un registro de configuración o un recolector de métricas.
class DatabasePool:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.connections = []
return cls._instance
# Cada llamada devuelve el mismo objeto
pool_a = DatabasePool()
pool_b = DatabasePool()
assert pool_a is pool_b
Python ofrece una alternativa más tranquila: pon la instancia en un módulo e impórtala. Los módulos ya son singletons, así que pool = DatabasePool() a nivel de módulo te da la misma garantía sin el truco de __new__. Cuándo no usarlo: cuando el objeto “único” guarda estado mutable que los tests o los tenants necesitan aislar. Un pool global que no puedes reiniciar se convierte en la razón por la que tu suite de tests pasa en local y falla en CI. Para las variantes (lazy, thread-safe, metaclass), consulta el patrón Singleton.
Factory Method
Usa una factory cuando el caller sabe qué quiere (“un notifier”) pero no qué clase debe proporcionarlo, y la elección ocurre en runtime.
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 SMSNotification(Notification):
def send(self, message: str):
print(f"SMS: {message}")
class NotificationFactory:
@staticmethod
def create(kind: str) -> Notification:
if kind == "email": return EmailNotification()
if kind == "sms": return SMSNotification()
raise ValueError(f"Notificación desconocida: {kind}")
notifier = NotificationFactory.create("email")
notifier.send("Tu pedido fue enviado")
Cuándo no usarlo: cuando hay exactamente una implementación y ninguna segunda a la vista. Una factory con un solo producto es un if que aprendió vocabulario orientado a objetos. Se justifica cuando el conjunto de variantes está abierto: nuevos proveedores de pago, nuevos formatos de exportación, nuevos canales de notificación.
Builder
Builder brilla cuando la firma del constructor es el problema: diez parámetros, ocho opcionales, y cada llamada es una adivinanza sobre el orden de los argumentos.
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;
}
where(condition: string): this {
this.parts.push(`WHERE ${condition}`);
return this;
}
build(): string {
return this.parts.join(' ') + ';';
}
}
const query = new QueryBuilder()
.select(['id', 'name', 'email'])
.from('users')
.where('active = true')
.build();
Cuándo no usarlo: para objetos con dos o tres campos requeridos. Un builder alrededor de new User(name, email) es ceremonia, y la API fluida no esconde nada que los parámetros con nombre no resuelvan ya en Python o TypeScript.
Patrones Estructurales
Los patrones estructurales deciden cómo se componen clases y objetos, normalmente para que dos piezas cooperen sin conocer los detalles de la otra. Cuatro de ellos resuelven los casos del día a día: traducir interfaces, añadir comportamiento al vuelo, ocultar complejidad e interceptar el acceso.
Adapter
Adapter es el patrón diplomático: una clase existente hace el trabajo, pero su interfaz no coincide con la que tu código espera, y no puedes o no debes cambiarla.
class LegacyPrinter:
def old_print(self, text):
print(f"OLD: {text}")
class PrinterAdapter:
def __init__(self, printer: LegacyPrinter):
self._printer = printer
def print(self, text): # la interfaz que tu código espera
self._printer.old_print(text)
adapter = PrinterAdapter(LegacyPrinter())
adapter.print("Hola") # funciona a través de la nueva interfaz
Te lo vas a encontrar constantemente al envolver SDKs de terceros o al migrar una versión de API: el adapter aísla la forma del proveedor para que una migración futura toque un archivo en vez de cuarenta. Cuándo no usarlo: cuando controlas ambas interfaces. Si los dos lados son tuyos, corrige uno de ellos en lugar de añadir una capa de traducción que nadie pidió.
Decorator
Decorator añade comportamiento alrededor de un objeto o función sin crear subclases. Python lo convirtió en sintaxis: la palabra clave @ es el patrón integrado en el lenguaje.
import time
from functools import wraps
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
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():
time.sleep(0.1)
return {"rows": 42}
data = fetch_data()
La llamada a functools.wraps importa: sin ella la función decorada pierde su nombre y docstring, lo que rompe la introspección y algunas herramientas de testing. Cuándo no usarlo: cuando el comportamiento añadido es parte del trabajo del objeto. Los decoradores son para concerns ortogonales (logging, caching, retries, timing), no para terminar una clase incompleta. Y mantén la pila corta: más de dos o tres capas hace difícil razonar sobre el orden de llamada.
Facade
Un facade le da a un subsistema complicado un único punto de entrada aburrido. El subsistema conserva su flexibilidad; tus callers dejan de necesitar un máster en él.
class VideoConverter:
def convert(self, source: str, target: str) -> bytes:
raw = self._read(source)
frames = self._decode(raw)
compressed = self._encode(frames, codec="h264")
return compressed
def _read(self, source): ...
def _decode(self, raw): ...
def _encode(self, frames, codec): ...
# Los callers ven un método, no el pipeline de codecs
converter = VideoConverter()
mp4 = converter.convert("input.mov", "output.mp4")
Los frameworks hacen esto constantemente: fetch() es un facade sobre DNS, TLS, sockets y streams. Cuándo no usarlo: cuando el subsistema ya es simple. Un facade sobre dos métodos es una función disfrazada de clase.
Proxy
Un proxy se interpone delante de un objeto y controla el acceso a él: carga perezosa, caché, comprobación de permisos, u ocultar que el objeto real vive en otra máquina.
class ExpensiveReport:
def generate(self) -> str:
return "informe calculado en 40 segundos"
class CachedReportProxy:
def __init__(self):
self._real = ExpensiveReport()
self._cache = None
def generate(self) -> str:
if self._cache is None:
self._cache = self._real.generate()
return self._cache
Para el caller, el proxy es indistinguible del objeto real, que es justo el punto: añades el comportamiento transversal sin tocar ninguno de los dos lados. Cuándo no usarlo: cuando la interceptación añade un modo de fallo que no necesitas. Un proxy con caché delante de datos que cambian cada segundo sirve resultados obsoletos con total confianza.
Patrones de Comportamiento
Los patrones de comportamiento tratan sobre comunicación: quién llama a quién, quién reacciona a qué, y cómo intercambiar algoritmos sin reescribir al que los llama. Estos seis aparecen en sistemas de eventos, arquitecturas de plugins, motores de workflow y en todo lo que necesita deshacer.
Observer
Observer es publish-subscribe a escala de objeto: un sujeto emite eventos, cualquier número de listeners reacciona, y ninguno de los dos lados sabe que el otro existe.
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);
}
off(event: string, callback: Function): void {
const cbs = this.listeners.get(event) ?? [];
this.listeners.set(event, cbs.filter(cb => cb !== callback));
}
emit(event: string, data: unknown): void {
this.listeners.get(event)?.forEach(cb => cb(data));
}
}
const emitter = new EventEmitter();
const logLogin = (user: any) => console.log(`${user.name} inició sesión`);
emitter.on('user:login', logLogin);
emitter.emit('user:login', { name: 'Alice' });
Fíjate en el método off: la versión original del GoF y la mayoría de ejemplos de manual omiten la desuscripción, y ese olvido es una fuente clásica de memory leaks en aplicaciones longevas. Cuándo no usarlo: para llamadas uno-a-uno donde el caller necesita el resultado de vuelta. Los eventos te dicen que algo ocurrió; son una pésima forma de pedir un valor. La recipe de Observer muestra una versión con forma de producción.
Strategy
Strategy empaqueta algoritmos intercambiables detrás de una única interfaz, para que el caller elija uno en runtime en lugar de hardcodear una rama.
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)
cart = ShoppingCart(PayPalPayment())
cart.checkout(99.99)
En JavaScript o Python a menudo no necesitas la clase: un dict de funciones { "card": pay_card, "paypal": pay_paypal } es el mismo patrón con menos ceremonia. Cuándo no usarlo: cuando el “algoritmo” nunca cambia. Strategy sobre un cálculo fijo es indirección sin beneficio. La recipe de Strategy muestra una implementación más completa.
Command
Command convierte una llamada a método en un objeto, lo que suena abstracto hasta que quieres encolarla, registrarla, reintentarla o deshacerla. Todo eso es el mismo patrón.
interface Command { execute(): Promise<void>; undo(): 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" });
}
async undo() {
await this.inventory.reserve(this.order.items);
await this.order.update({ status: "active" });
}
}
// Los commands son datos: guárdalos, encólalos, repítelos
const history: Command[] = [];
const cmd = new CancelOrderCommand(order, inventory);
await cmd.execute();
history.push(cmd);
Cuándo no usarlo: para llamadas fire-and-forget que nunca necesitan deshacer, encolar o auditar. Y si implementas Command, implementa undo honestamente o no lo hagas: un undo() que lanza una excepción es peor que no tener undo, porque los callers construyen flujos sobre esa promesa.
Template Method
Template Method fija el esqueleto de un algoritmo en una clase base y deja que las subclases sobrescriban los pasos que varían. Es el patrón detrás de los “hooks” de la mayoría de frameworks.
from abc import ABC, abstractmethod
class DataImporter(ABC):
def import_file(self, path: str): # la plantilla
raw = self.read(path)
rows = self.parse(raw)
self.validate(rows)
self.save(rows)
def read(self, path: str):
with open(path) as f:
return f.read()
def validate(self, rows):
if not rows:
raise ValueError("archivo vacío")
@abstractmethod
def parse(self, raw): ... # el paso variable
@abstractmethod
def save(self, rows): ...
class CSVImporter(DataImporter):
def parse(self, raw):
return [line.split(",") for line in raw.strip().split("\n")]
def save(self, rows):
print(f"{len(rows)} filas guardadas")
Las subclases rellenan parse y save; el orden (leer, parsear, validar, guardar) vive en un solo lugar. Cuándo no usarlo: cuando los pasos cambian de orden u opcionalidad. Template Method es rígido por diseño; si el flujo mismo varía, lo que necesitas es composición (Strategy o un pipeline) en lugar de herencia.
State
State es para objetos cuyo comportamiento cambia con su estado interno: un pedido que está pending, paid, shipped o cancelled, cada uno con sus propias reglas sobre qué puede pasar después.
class Order:
def __init__(self):
self._state = PendingState()
def cancel(self):
self._state = self._state.cancel(self)
class PendingState:
def cancel(self, order):
return CancelledState()
class ShippedState:
def cancel(self, order):
raise ValueError("no se puede cancelar un pedido enviado")
Sin el patrón esto se convierte en un switch sobre self.status en cada método, y cada nuevo estado obliga a auditarlos todos. Cuándo no usarlo: para dos estados y una transición. Un flag booleano gana a una jerarquía de clases hasta que las reglas de transición empiezan a multiplicarse; eso suele ocurrir alrededor de tres estados con restricciones entre ellos.
Dónde ya usas patrones
La mitad del valor de aprender patrones está en reconocer que tus herramientas ya funcionan con ellos:
- El middleware de Express y Django es Chain of Responsibility: cada handler procesa la request o la pasa al siguiente.
- El modelo de re-render de React es Observer: los componentes se suscriben al estado, y un cambio de estado notifica a cada suscriptor.
- Los ORMs son Repository más Unit of Work: pides objetos, el ORM traduce a SQL y agrupa las escrituras.
- Los frameworks de inyección de dependencias (NestJS, Spring, el
Dependsde FastAPI) son Factory más Singleton gestionados por ti, que es la razón por la que las llamadasgetInstance()escritas a mano casi desaparecieron del código moderno. - Los callbacks y props en JavaScript son Strategy sin la ceremonia de la interfaz: pasarle
onSorta un componente de tabla es elegir un algoritmo en runtime. - Los decoradores de Python, los decoradores de TypeScript y los wrappers de middleware son el patrón Decorator con distinta sintaxis.
Ver el patrón dentro del framework importa para las decisiones de diseño: cuando reconoces tu stack de middleware como Chain of Responsibility, preguntas como “¿debería este handler correr antes que auth?” se responden solas.
Cómo elegir entre patrones parecidos
La mayoría de errores de selección ocurren entre patrones que se parecen vistos de lejos:
- Adapter vs Facade vs Proxy. Los tres envuelven algo. Adapter traduce una interfaz para que dos piezas incompatibles hablen. Facade simplifica una interfaz para que los callers necesiten saber menos. Proxy conserva la interfaz e interpone control (caché, auth, carga perezosa) entre caller y objeto. La pista: si las firmas de los métodos cambian, es un adapter o un facade; si se quedan idénticas, es un proxy.
- Strategy vs State. Los dos intercambian comportamiento a través de una interfaz común. Strategy la elige el caller y normalmente las estrategias no se conocen entre sí; State la dirige el ciclo de vida del propio objeto y cada estado sabe cuál viene después. Los métodos de pago son Strategy; el estado del pedido es State.
- Factory Method vs Abstract Factory. Factory Method crea un producto elegido en runtime. Abstract Factory crea una familia de productos relacionados que deben mantenerse consistentes: el botón, el diálogo y el input del tema claro no pueden mezclarse con los del tema oscuro. La versión abstracta solo se justifica cuando la restricción “no mezcles estos” es real.
- Decorator vs Proxy. Los dos envuelven un objeto con la misma interfaz. Decorator añade responsabilidades (logging, caching como feature); Proxy controla el acceso (lazy loading, permisos). Un wrapper con caché es técnicamente ambos, así que nómbralo por su intención: comportamiento añadido es Decorator, control de acceso es Proxy.
Patrones y testabilidad
La elección de patrón decide lo fácil que es testear tu código. Todo lo que entra por constructor (Strategy, Command, las dependencias de Adapter o Facade) se reemplaza por un stub en una línea. Todo lo que se alcanza por vía global (Singleton, factories estáticas) hay que parchearlo o reiniciarlo entre tests, y de ahí salen las suites frágiles. Si una clase no se puede testear en unitario sin tocar la red o el filesystem, el arreglo casi nunca es un test mejor: es inyectar la dependencia que la clase estaba yendo a buscar.
Cheat Sheet de Selección de Patterns
| Problema | Pattern |
|---|---|
| Necesito exactamente una instancia | Singleton |
| Creo objetos según un string o tipo | Factory Method |
| Este objeto tiene diez parámetros opcionales | Builder |
| Una familia de objetos debe ser consistente | Abstract Factory |
| El código legacy no coincide con mi interfaz | Adapter |
| Necesito añadir logging a todo | Decorator |
| Un subsistema es demasiado complejo para llamarlo | Facade |
| Necesito lazy loading o control de acceso | Proxy |
| Los componentes deben reaccionar a eventos | Observer |
| Quiero intercambiar algoritmos en runtime | Strategy |
| Necesito registrar, encolar o deshacer operaciones | Command |
| El algoritmo es fijo pero algunos pasos varían | Template Method |
| El comportamiento depende del estado del ciclo de vida | State |
| Necesito abstraer el acceso a base de datos | Repository |
| Necesito recorrer sin exponer el interior | Iterator |
Lo que funciona
- No fuerces patrones. No todo problema necesita uno, y un patrón aplicado a un problema trivial es cómo los codebases se ganan la fama de “sobre-ingeniería”. Refactoriza hacia el patrón cuando la duplicación o el acoplamiento aparecen de verdad.
- Empieza simple, refactoriza hacia el patrón. Escribe el
ifprimero. Cuando llegue la tercera variante, el patrón a extraer suele nombrarse solo. Consulta código limpio para la disciplina de refactor. - Que el nombre lleve el patrón.
UserRepository,EmailStrategy,PricingFactoryle dicen al siguiente lector qué reglas aplican antes de abrir el archivo. - Documenta el porqué, no el qué. “Aquí usamos Strategy” es ruido; “usamos Strategy para que facturación pueda añadir proveedores de pago sin tocar checkout” es un registro de decisión.
- Respeta el lenguaje. La mitad del catálogo GoF existe porque C++ no tenía closures, módulos ni decoradores. En Python o JavaScript varios patrones colapsan en features del lenguaje, y usar el feature es la decisión correcta.
Errores Comunes
- Singleton para todo. Las instancias globales acoplan cada módulo que las toca y hacen los tests dependientes del orden. La mayoría de singletons deberían ser dependencias inyectadas por constructor.
- Explosión de patrones. Un módulo con cuatro o cinco patrones suele estar haciendo varios trabajos; el arreglo es dividir el módulo, no añadir otro patrón.
- Ignorar los idioms del lenguaje. Escribir un singleton estilo Java con
getInstance()en Python lucha contra el sistema de módulos; escribir una Strategy con clases donde basta una función añade peso sin beneficio. - Observer sin desuscripción. Los listeners que nunca se desconectan mantienen vivo al sujeto y todas sus dependencias para siempre. Provee siempre
off/unsubscribey úsalo en las rutas de limpieza. - Adapter donde toca arreglar. Si controlas ambas interfaces, un adapter congela el desajuste en su sitio en lugar de eliminarlo.
Todo junto: patrones en un sistema de pedidos
El pipeline de pedidos de abajo muestra cómo se apilan los patrones en un sistema casi real. Cada uno entra porque un dolor concreto lo pidió, que es la única razón por la que un patrón debería aparecer.
// Strategy: el costo de envío varía según método, elegido en runtime
interface ShippingStrategy {
calculate(weightKg: number): number;
}
const strategies: Record<string, ShippingStrategy> = {
standard: { calculate: w => w * 0.5 },
express: { calculate: w => w * 1.5 },
sameDay: { calculate: w => w * 3.0 },
};
const shippingCost = strategies[order.shippingMethod].calculate(order.weightKg);
// Factory: el pedido necesita un notifier, tipo elegido por canal
class NotificationFactory {
static create(kind: string): Notifier {
switch (kind) {
case "email": return new EmailNotifier();
case "sms": return new SMSNotifier();
case "push": return new PushNotifier();
default: throw new Error(`Canal no soportado: ${kind}`);
}
}
}
// Observer: un evento, tres reacciones independientes
bus.on("order.created", updateInventory);
bus.on("order.created", sendConfirmation);
bus.on("order.created", trackAnalytics);
// Decorator: logging y cache envolviendo el fetch de producto
const fetchProductFast = withCache(withLogging(fetchProduct), 60_000);
// Command: cancelaciones como objetos para deshacerlas y auditarlas
await new CancelOrderCommand(order, inventory).execute();
Fíjate en lo que falta: nada de Abstract Factory, ni Visitor, ni herencia de cuatro niveles. Cinco patrones, cinco dolores. Esa proporción es más o menos la que se ve en sistemas de producción: un puñado de patrones sosteniendo el diseño, elegidos porque una restricción real pidió cada uno. Los equipos que lo hacen mal van en dirección contraria: empiezan con un diagrama lleno de patrones y luego buscan problemas que los justifiquen. La lección del catálogo GoF no son las 23 soluciones; es que los developers con experiencia se chocan una y otra vez con el mismo puñado de problemas, y nombrar el problema es la mayor parte del trabajo. El pipeline completo es ejecutable en el repositorio complementario — seis archivos, cinco patrones, un sistema de pedidos funcionando.
Lecturas adicionales
- Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides (1994). El catálogo original; sigue siendo la referencia para intención y trade-offs.
- Refactoring.Guru — Design Patterns — catálogo interactivo con diagramas UML y ejemplos en varios lenguajes.
Preguntas frecuentes
¿Cuándo debería usar un design pattern?
Cuando te topes con el problema que resuelve, no antes. Un patrón es una respuesta a duplicación, acoplamiento o complejidad concretos que puedes señalar en el código. Si no puedes nombrar el dolor, el patrón es prematuro, y los patrones prematuros son solo indirección con nombre famoso. Combina este criterio con los principios SOLID, que describen las metas de fondo (clases pequeñas, cambios locales, dependencias reemplazables) a las que sirven los patrones.
¿Siguen siendo relevantes los design patterns en lenguajes modernos?
Sí, pero varios se mudaron al lenguaje. Las Promise y los event emitters de JavaScript son maquinaria Observer, la sintaxis @decorator de Python es el patrón Decorator, y los iteradores están integrados en los bucles for de todas partes. El valor que sobrevive es el vocabulario y el análisis de trade-offs, no el boilerplate, así que aprende la intención aunque el lenguaje escriba el código por ti.
¿Cuál es la diferencia entre Factory Method y Builder?
Factory responde "¿qué clase instancio?" y Builder responde "¿cómo ensamblo un objeto complicado?". Una factory elige entre productos intercambiables (notifier por email o SMS); un builder te guía paso a paso en la construcción de un único producto complejo (una query, una request, un objeto de configuración). Si tu problema es una lista larga de parámetros, quieres Builder; si es selección en runtime entre variantes, quieres Factory.
¿Cómo se relacionan los design patterns con SOLID?
SOLID enuncia las metas; los patrones son tácticas recurrentes para alcanzarlas. Strategy te da Open/Closed porque las nuevas estrategias extienden el sistema sin cambiar el código existente. Factory mantiene la construcción en un solo lugar, que es Single Responsibility. Observer depende de una abstracción (la interfaz de evento) y no de un suscriptor concreto, que es Dependency Inversion. Aprender SOLID primero hace que elegir patrones se sienta menos como memorizar.
¿Cuántos patterns debería usar en un módulo?
Los que los problemas justifiquen, y ni uno más. Cada patrón añade una indirección que el lector tiene que decodificar, así que pasados dos o tres en un mismo módulo, pregúntate si el módulo está haciendo demasiado en lugar de si necesita otro patrón.
Recursos Relacionados
Patrón Singleton: Ejemplos en Java, Python y TypeScript
Garantiza que una clase tenga una única instancia y ofrece acceso global a ella — el patrón creacional para controlar la creación de objetos, con implementaciones listas para producción.
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.
GuidePrincipios SOLID Explicados con Ejemplos
Aprende los cinco principios SOLID con ejemplos de código malo vs bueno y un refactor completo antes/después de un servicio de pagos en TypeScript.