Lección 7 de 915 min
Patrones de diseño clásicos
Factory, Singleton, Observer, Strategy, Adapter, Decorator, Facade, Mediator y Command.
En 1994, cuatro autores (Gamma, Helm, Johnson y Vlissides, conocidos como la «banda de los cuatro» o GoF, por Gang of Four) publicaron un catálogo de 23 soluciones a problemas que se repetían en el diseño orientado a objetos. Les pusieron nombre, y esos nombres se convirtieron en vocabulario común: decir «aquí va un Adapter» ahorra cinco minutos de explicación.
Aquí verás los nueve que más aparecen en el desarrollo web, cada uno con un ejemplo mínimo en TypeScript y el sitio donde ya te lo encuentras sin saberlo. En JavaScript, muchos se simplifican bastante gracias a las funciones y los módulos.
Creacionales: cómo se crean los objetos
Factory
Una función o método que decide qué objeto crear, para que quien lo pide no lo sepa.
- En la web:
- document.createElement()
Una factory encapsula la creación: en lugar de hacer new de una clase concreta, pides el objeto a una función que elige la implementación adecuada según los parámetros, la configuración o el entorno. El catálogo GoF distingue el Factory Method (una subclase decide qué crear) y la Abstract Factory (crea familias de objetos relacionados), pero en el día a día se llama factory a cualquier función que crea y devuelve objetos.
En la web la ves en document.createElement("button"), en createRoot() de React o en tantas funciones crearAlgo() de las bibliotecas: te entregan un objeto listo sin enseñarte cómo se construye.
Úsalo cuando
Cuando la clase concreta depende de algo que se decide al ejecutar, o cuando construir el objeto es complicado y no quieres repetir ese código.
Ten en cuenta
Si solo hay una implementación y siempre la habrá, una factory es una capa de más: un new o un objeto literal bastan.
interface Notificador {
enviar(mensaje: string): Promise<void>;
}
// Quien llama no sabe qué clase concreta recibe
export function crearNotificador(canal: "email" | "sms" | "push"): Notificador {
switch (canal) {
case "email": return new NotificadorEmail(config.smtp);
case "sms": return new NotificadorSms(config.proveedorSms);
case "push": return new NotificadorPush();
}
}
await crearNotificador(usuario.canalPreferido).enviar("Tu pedido ha salido");Singleton
Garantizar que una clase tenga una sola instancia, accesible desde toda la app.
- En JavaScript:
- Un objeto exportado por un módulo
Un singleton asegura que solo exista un objeto de cierto tipo: una conexión a la base de datos, la configuración, el registro de logs. La versión clásica oculta el constructor y expone un método obtener() que siempre devuelve la misma instancia.
En JavaScript casi nunca hace falta esa ceremonia: un módulo se evalúa una sola vez, así que un objeto exportado ya es único para todos los que lo importen. En Angular, un servicio con providedIn: "root" es, en la práctica, un singleton que gestiona el inyector.
Úsalo cuando
Para recursos que de verdad deben ser únicos y son caros de crear: un pool de conexiones, un cliente de caché, la configuración leída al arrancar.
Ten en cuenta
Es estado global con otro nombre: acopla el código y complica los tests. Y en el servidor es peligroso: en una app con SSR, un singleton que guarde datos del usuario se comparte entre todas las peticiones, y un usuario podría ver los datos de otro.
import { Pool } from "pg";
// El módulo se evalúa una sola vez: todos los que importen
// «db» reciben el mismo pool de conexiones
export const db = new Pool({ connectionString: process.env.DATABASE_URL });
// La versión clásica, con más ceremonia
class Configuracion {
private static instancia?: Configuracion;
private constructor(readonly valores: Record<string, string>) {}
static obtener() {
return (this.instancia ??= new Configuracion(leerConfig()));
}
}Estructurales: cómo se combinan
Adapter
Un envoltorio que traduce una interfaz que no encaja a la que tu código espera.
- En la web:
- Envolver SDKs y APIs antiguas
Un adapter funciona como el adaptador de enchufe de un viaje: el aparato no cambia, la pared tampoco, y una pieza intermedia traduce entre ambos. En código, envuelve un objeto con una interfaz incómoda o ajena (el SDK de un proveedor, una API antigua) y lo expone con la interfaz que tu aplicación ya usa.
Es la pieza central de la arquitectura hexagonal que viste en la lección de capas: cada adaptador conecta un puerto del núcleo con una tecnología concreta. Si cambias de proveedor, escribes otro adaptador y el resto del código ni se entera.
Úsalo cuando
Al integrar bibliotecas o servicios externos, al migrar de una API antigua a una nueva, o cuando dos partes de tu sistema hablan «idiomas» distintos.
Ten en cuenta
Un adapter traduce; si empieza a tomar decisiones de negocio, ya es otra cosa. Y no envuelvas por costumbre bibliotecas que no piensas cambiar nunca.
// La interfaz que usa tu aplicación
interface PasarelaPago {
cobrar(importe: Dinero): Promise<{ id: string }>;
}
// El SDK del proveedor habla otro idioma: nombres en inglés, otros campos
class AdaptadorPagosAcme implements PasarelaPago {
constructor(private sdk: AcmeSdk) {}
async cobrar(importe: Dinero) {
const cargo = await this.sdk.createCharge({ amount: importe.centimos, currency: "eur" });
return { id: cargo.chargeId };
}
}Decorator
Envolver un objeto con otro de la misma interfaz que le añade comportamiento.
- Clave:
- La misma interfaz que el original
Un decorator recibe un objeto y devuelve otro con la misma interfaz que hace algo más antes o después de delegar en el original: registrar un log, cachear, reintentar, medir tiempos. Como la interfaz no cambia, puedes apilar varios decoradores como capas, en el orden que quieras.
Es una alternativa a la herencia: en vez de crear RepositorioConCacheYLogs, combinas piezas pequeñas. En JavaScript se hace muy a menudo con funciones de orden superior: una función que recibe otra y devuelve una versión mejorada, como las utilidades memoize de muchas bibliotecas.
Úsalo cuando
Para añadir aspectos transversales (caché, logs, métricas, reintentos) a algo que ya existe sin modificar su código.
Ten en cuenta
Con muchas capas, depurar se complica: la pila de llamadas atraviesa todos los envoltorios. Y el orden importa: no es lo mismo cachear y luego reintentar que al revés.
interface Repositorio {
buscar(id: string): Promise<Producto>;
}
// Recibe un repositorio y devuelve otro con la misma interfaz
function conCache(repo: Repositorio): Repositorio {
const cache = new Map<string, Producto>();
return {
async buscar(id) {
if (!cache.has(id)) cache.set(id, await repo.buscar(id));
return cache.get(id)!;
},
};
}
const productos = conLogs(conCache(new RepositorioSql())); // capas apiladasFacade
Una interfaz sencilla delante de un subsistema complejo.
- En la web:
- fetch(), servicios de Angular
Una facade (fachada) ofrece una puerta de entrada simple a algo que por dentro tiene muchas piezas. Quien la usa llama a checkout.pagar(carrito) y no necesita saber que eso implica comprobar el stock, cobrar, crear el pedido y enviar un correo.
La fachada no prohíbe usar las piezas internas a quien de verdad las necesite: solo ofrece un atajo para el caso común. En la web la ves en fetch(), que esconde conexiones, cabeceras y flujos de datos, o en los servicios de Angular que reúnen varias llamadas a la API detrás de un método.
Úsalo cuando
Cuando varios sitios del código repiten la misma secuencia de llamadas a un subsistema, o para aislar una parte compleja o heredada detrás de una interfaz limpia.
Ten en cuenta
Una fachada que crece sin control se convierte en una «clase dios» que lo sabe y lo hace todo. Mantenla delgada: coordina, pero no contiene la lógica de cada pieza.
export class Checkout {
constructor(
private stock: ServicioStock,
private pagos: PasarelaPago,
private pedidos: RepositorioPedidos,
private correo: ServicioCorreo,
) {}
// Un solo método para el caso común; dentro, cuatro subsistemas
async pagar(carrito: Carrito) {
await this.stock.reservar(carrito.lineas);
const cobro = await this.pagos.cobrar(carrito.total);
const pedido = await this.pedidos.crear(carrito, cobro.id);
await this.correo.enviarConfirmacion(pedido);
return pedido;
}
}Los decoradores con @ no son exactamente el patrón Decorator
Cuando escribes @Component o @Injectable en Angular usas una función del lenguaje: los decoradores de TypeScript, que también son una propuesta para el estándar de JavaScript. Se aplican a clases, métodos o propiedades en el momento de definirlos y suelen añadir metadatos o modificar esa definición. Se parecen en espíritu (envolver para añadir algo), pero el patrón Decorator envuelve objetos mientras el programa se ejecuta y conserva su interfaz. Un decorador @ puede servir para implementar el patrón, pero no son sinónimos.
De comportamiento: cómo se comunican
Observer
Un objeto avisa a todos sus suscriptores cuando algo cambia, sin conocerlos.
- En la web:
- addEventListener, RxJS, signals
En el patrón observer, un sujeto mantiene una lista de observadores interesados y los avisa cuando ocurre algo. Los observadores se suscriben y se dan de baja cuando quieren; el sujeto no sabe nada de ellos, salvo que tienen una función a la que llamar.
Es probablemente el patrón más presente en la web: addEventListener es un observer, los Observables de RxJS (que Angular usa en HttpClient y en los formularios) son observers con superpoderes, y los signals y los almacenes de estado avisan a la interfaz cuando cambian sus datos.
Úsalo cuando
Cuando varias partes deben reaccionar a un cambio sin que quien cambia tenga que conocerlas: eventos de la interfaz, cambios de estado, notificaciones en tiempo real.
Ten en cuenta
Olvidar darse de baja provoca fugas de memoria: el sujeto sigue guardando referencias a componentes que ya no existen. Y con muchas cadenas de avisos, el flujo se vuelve difícil de seguir.
class Sujeto<T> {
private observadores = new Set<(valor: T) => void>();
suscribir(fn: (valor: T) => void) {
this.observadores.add(fn);
return () => this.observadores.delete(fn); // para darse de baja
}
emitir(valor: T) {
this.observadores.forEach((fn) => fn(valor));
}
}
const carrito = new Sujeto<number>();
const darDeBaja = carrito.suscribir((n) => (icono.textContent = String(n)));
carrito.emitir(3); // el icono muestra 3
darDeBaja();Strategy
Una familia de algoritmos intercambiables que se eligen al ejecutar.
- En la web:
- array.sort(), Passport.js
El patrón strategy saca un algoritmo que puede variar (cómo calcular el envío, cómo ordenar, cómo validar) a piezas intercambiables con la misma forma. El código que lo usa recibe la estrategia y la ejecuta sin saber cuál es, en lugar de llenarse de if y switch.
En JavaScript, una estrategia suele ser simplemente una función. Ya la usas: la función que pasas a array.sort() es una estrategia de comparación, y las «estrategias» de Passport.js para iniciar sesión con Google, GitHub o usuario y contraseña siguen exactamente este patrón.
Úsalo cuando
Cuando tienes varias formas de hacer lo mismo que se eligen según el contexto, o cuando un switch crece con cada caso nuevo.
Ten en cuenta
Con dos casos que nunca van a cambiar, un if es más claro. La estrategia compensa cuando las variantes crecen o vienen de fuera.
type EstrategiaEnvio = (pesoKg: number) => number;
const envios: Record<string, EstrategiaEnvio> = {
estandar: (peso) => 4.99 + peso * 0.5,
urgente: (peso) => 9.99 + peso * 1.2,
recogida: () => 0,
};
function totalPedido(subtotal: number, peso: number, envio: EstrategiaEnvio) {
return subtotal + envio(peso); // no sabe qué estrategia es
}
totalPedido(50, 2, envios[formulario.metodoEnvio]);Mediator
Un objeto central coordina a varios componentes para que no se hablen directamente.
- En la web:
- Componente padre, servicio compartido
Cuando muchos objetos se comunican todos con todos, las conexiones crecen sin control: con diez componentes puede haber decenas de relaciones. El mediator pone un coordinador en medio: cada componente solo habla con el mediador, y este decide a quién avisar y qué hacer.
Piensa en una torre de control: los aviones no negocian entre sí quién aterriza; todos hablan con la torre. En la web hacen de mediadores una sala de chat que reparte los mensajes, un componente padre que coordina a sus hijos o un servicio compartido de Angular que sincroniza varios componentes.
Úsalo cuando
En formularios y pantallas con muchos controles que dependen unos de otros, o cuando quieres que los componentes sean reutilizables sin conocerse entre sí.
Ten en cuenta
El mediador puede convertirse en un objeto enorme que concentra toda la lógica. Si crece demasiado, divídelo por áreas.
type Receptor = (de: string, texto: string) => void;
class SalaChat {
private miembros = new Map<string, Receptor>();
unirse(nombre: string, recibir: Receptor) {
this.miembros.set(nombre, recibir);
}
// Los usuarios no se conocen entre sí: todo pasa por la sala
enviar(de: string, texto: string) {
for (const [nombre, recibir] of this.miembros) {
if (nombre !== de) recibir(de, texto);
}
}
}Command
Convertir una acción en un objeto que se puede guardar, encolar, repetir o deshacer.
- En la web:
- Deshacer y rehacer en editores
El patrón command encapsula una petición («poner en negrita», «mover la forma 10 píxeles») en un objeto con todo lo necesario para ejecutarla. Como la acción pasa a ser un dato, puedes guardarla en un historial, ponerla en una cola, enviarla por la red o, si también sabe revertirse, deshacerla.
Es la base del deshacer y rehacer de cualquier editor: cada cambio es un comando con ejecutar() y deshacer(), y el historial es una pila de comandos. Las acciones de Redux o NgRx, objetos que describen qué ha pasado, están muy emparentadas con esta idea.
Úsalo cuando
Para deshacer y rehacer, colas de tareas, macros, operaciones que quieres registrar o repetir, y paletas de comandos como la de VS Code.
Ten en cuenta
Cada acción necesita su propio objeto, y deshacer bien puede ser difícil cuando la acción tiene efectos externos, como un correo ya enviado.
interface Comando {
ejecutar(): void;
deshacer(): void;
}
const historial: Comando[] = [];
function moverForma(forma: Forma, dx: number): Comando {
return {
ejecutar: () => { forma.x += dx; },
deshacer: () => { forma.x -= dx; },
};
}
function hacer(comando: Comando) {
comando.ejecutar();
historial.push(comando);
}
const deshacerUltimo = () => historial.pop()?.deshacer(); // Ctrl+ZNo los fuerces
Los patrones son soluciones a problemas concretos, no una lista de comprobación. Aplicar uno «por si acaso» añade capas que alguien tendrá que entender después. Úsalos cuando reconozcas el problema que resuelven.
Interactivo
Adapter, Decorator y Facade: los tres envuelven algo
Se confunden a menudo porque los tres ponen una pieza delante de otra. La diferencia está en lo que hacen con la interfaz.
| Característica | Adapter | Decorator | Facade |
|---|---|---|---|
| Qué envuelve | Un objeto con una interfaz que no encaja | Un objeto que ya tiene la interfaz correcta | Un subsistema de varias piezas |
| ¿Cambia la interfaz? | Sí: la traduce a la que esperas | No: conserva la misma | Ofrece una nueva, más simple |
| Para qué | Hacer compatible lo incompatible | Añadir comportamiento: caché, logs, reintentos | Simplificar el uso habitual |
| Ejemplo en la web | Envolver el SDK de un proveedor de pagos | Una función que añade caché a un repositorio | Un servicio con un método pagar() |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Según el país del usuario, la app debe usar un proveedor de envío de SMS u otro, y el resto del código no debería saber cuál le ha tocado.
Para recordar
- Los patrones de diseño son un vocabulario común: nombran soluciones a problemas que se repiten.
- Los creacionales (Factory, Singleton) deciden cómo se crean los objetos; los estructurales (Adapter, Decorator, Facade), cómo se combinan; los de comportamiento (Observer, Strategy, Mediator, Command), cómo se comunican.
- Adapter traduce una interfaz, Decorator la conserva y añade comportamiento, y Facade ofrece una más simple.
- En JavaScript, muchos patrones se reducen a funciones y módulos: una estrategia es una función y un singleton, un módulo.
- Aplica un patrón cuando reconozcas su problema, no por costumbre.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Qué diferencia a un Decorator de un Adapter?
2.En una aplicación con SSR, guardas el usuario actual en un singleton del servidor. ¿Qué riesgo corres?
3.¿Qué patrón aplicas al pasar una función de comparación a
array.sort()?4.Un componente se suscribe a un observable al crearse, pero nunca se da de baja. ¿Qué puede pasar?
5.¿Por qué el patrón Command facilita deshacer?