Los patrones de diseño son soluciones reutilizables a problemas recurrentes en el desarrollo de software. Con JavaScript moderno y ES6+, muchos de ellos se implementan de forma más concisa y expresiva que en los ejemplos clásicos de Java o C++. Esta guía muestra Factory, Strategy, Observer y Command con ejemplos reales.
Factory: centralizar la creación de objetos
El patrón Factory encapsula la lógica de creación de objetos. En lugar de usar new directamente disperso por el código, una función o clase centralizada decide qué tipo de objeto crear según el contexto:
// Factory de clientes HTTP según el entorno
class ClienteHTTP {
async get(url) { /* ... */ }
async post(url, body) { /* ... */ }
}
class ClienteConRetry extends ClienteHTTP {
constructor(opciones) {
super();
this.maxReintentos = opciones.reintentos ?? 3;
this.delay = opciones.delay ?? 1000;
}
async get(url) {
for (let i = 0; i <= this.maxReintentos; i++) {
try {
return await super.get(url);
} catch (err) {
if (i === this.maxReintentos) throw err;
await new Promise(r => setTimeout(r, this.delay * (i + 1)));
}
}
}
}
class ClienteConCache extends ClienteHTTP {
#cache = new Map();
async get(url) {
if (this.#cache.has(url)) return this.#cache.get(url);
const res = await super.get(url);
this.#cache.set(url, res);
return res;
}
}
// La Factory decide qué cliente crear
function crearCliente(tipo = 'base', opciones = {}) {
switch (tipo) {
case 'retry': return new ClienteConRetry(opciones);
case 'cache': return new ClienteConCache();
case 'base':
default: return new ClienteHTTP();
}
}
const cliente = crearCliente('retry', { reintentos: 5, delay: 2000 });
Strategy: algoritmos intercambiables
El patrón Strategy define una familia de algoritmos, los encapsula y los hace intercambiables. En JavaScript, las funciones como valores de primera clase hacen que a veces sea tan simple como pasar una función:
// Estrategias de ordenación
const estrategias = {
precio: (a, b) => a.precio - b.precio,
precioDesc: (a, b) => b.precio - a.precio,
nombre: (a, b) => a.nombre.localeCompare(b.nombre),
stock: (a, b) => b.stock - a.stock,
};
class CatalogProductos {
#productos;
#estrategiaOrden;
constructor(productos) {
this.#productos = productos;
this.#estrategiaOrden = estrategias.nombre;
}
setOrden(clave) {
if (!estrategias[clave]) throw new Error(`Estrategia desconocida: ${clave}`);
this.#estrategiaOrden = estrategias[clave];
return this;
}
listar() {
return [...this.#productos].sort(this.#estrategiaOrden);
}
}
const catalogo = new CatalogProductos([
{ nombre: 'Ratón', precio: 29, stock: 45 },
{ nombre: 'Teclado', precio: 89, stock: 12 },
{ nombre: 'Monitor', precio: 299, stock: 5 },
]);
console.log(catalogo.setOrden('precio').listar().map(p => p.nombre));
// ['Ratón', 'Teclado', 'Monitor']
console.log(catalogo.setOrden('stock').listar().map(p => p.nombre));
// ['Ratón', 'Teclado', 'Monitor']
Observer: sistema de eventos con suscriptores
El patrón Observer (publicador-suscriptor) desacopla a quien produce un evento de quien lo consume. Es la base de la mayoría de sistemas de eventos en JavaScript:
class EventEmitter {
#listeners = new Map();
on(evento, fn) {
if (!this.#listeners.has(evento)) {
this.#listeners.set(evento, new Set());
}
this.#listeners.get(evento).add(fn);
// Devuelve función para desuscribirse
return () => this.off(evento, fn);
}
once(evento, fn) {
const wrapper = (...args) => {
fn(...args);
this.off(evento, wrapper);
};
return this.on(evento, wrapper);
}
off(evento, fn) {
this.#listeners.get(evento)?.delete(fn);
}
emit(evento, ...datos) {
this.#listeners.get(evento)?.forEach(fn => fn(...datos));
}
}
// Ejemplo: carrito de compra observable
class Carrito extends EventEmitter {
#items = [];
añadir(producto) {
this.#items.push(producto);
this.emit('cambio', this.#items);
this.emit('producto:añadido', producto);
}
eliminar(id) {
const anterior = this.#items.length;
this.#items = this.#items.filter(i => i.id !== id);
if (this.#items.length !== anterior) {
this.emit('cambio', this.#items);
}
}
get total() {
return this.#items.reduce((s, i) => s + i.precio, 0);
}
}
const carrito = new Carrito();
const desuscribir = carrito.on('cambio', items => {
console.log(`Carrito actualizado: ${items.length} productos`);
});
carrito.once('producto:añadido', prod => {
console.log(`¡Primer producto añadido: ${prod.nombre}!`);
});
carrito.añadir({ id: 1, nombre: 'Teclado', precio: 89 });
// "¡Primer producto añadido: Teclado!"
// "Carrito actualizado: 1 productos"
carrito.añadir({ id: 2, nombre: 'Ratón', precio: 29 });
// "Carrito actualizado: 2 productos"
desuscribir(); // Cancela la suscripción al evento 'cambio'
Command: encapsular operaciones con soporte para deshacer
El patrón Command encapsula una operación como un objeto, lo que permite registrar el historial de acciones y deshacer/rehacer:
class GestorComandos {
#historial = [];
#futuro = [];
ejecutar(comando) {
comando.ejecutar();
this.#historial.push(comando);
this.#futuro = []; // Al ejecutar nuevo comando, se borra el redo
return this;
}
deshacer() {
const comando = this.#historial.pop();
if (!comando) return this;
comando.deshacer();
this.#futuro.push(comando);
return this;
}
rehacer() {
const comando = this.#futuro.pop();
if (!comando) return this;
comando.ejecutar();
this.#historial.push(comando);
return this;
}
}
// Comandos para un editor de texto
class InsertarTextoComando {
constructor(editor, texto, posicion) {
this.editor = editor;
this.texto = texto;
this.posicion = posicion;
}
ejecutar() {
this.editor.insertar(this.posicion, this.texto);
}
deshacer() {
this.editor.eliminar(this.posicion, this.texto.length);
}
}
class Editor {
#contenido = '';
insertar(pos, texto) {
this.#contenido =
this.#contenido.slice(0, pos) + texto + this.#contenido.slice(pos);
}
eliminar(pos, longitud) {
this.#contenido =
this.#contenido.slice(0, pos) + this.#contenido.slice(pos + longitud);
}
get contenido() { return this.#contenido; }
}
const editor = new Editor();
const gestor = new GestorComandos();
gestor.ejecutar(new InsertarTextoComando(editor, 'Hola', 0));
gestor.ejecutar(new InsertarTextoComando(editor, ' mundo', 4));
console.log(editor.contenido); // "Hola mundo"
gestor.deshacer();
console.log(editor.contenido); // "Hola"
gestor.rehacer();
console.log(editor.contenido); // "Hola mundo"
Estos cuatro patrones resuelven problemas muy distintos pero todos comparten el objetivo de hacer el código más mantenible y extensible. En JavaScript moderno, la combinación de clases con campos privados, funciones de primera clase y el sistema de eventos nativo hace que su implementación sea limpia y expresiva.
