Los patrones de diseño clásicos (Gang of Four) se diseñaron para lenguajes orientados a objetos con tipado estático. En JavaScript moderno con ESM, muchos se implementan de formas más simples y naturales aprovechando los closures, los módulos y las características del lenguaje. Este artículo muestra cómo implementar los más útiles sin boilerplate innecesario.
Singleton con ESM: sin getInstance()
Los módulos ES son singletons por diseño: el motor evalúa cada módulo una sola vez y cachea el resultado. No necesitas el patrón clásico con una variable estática e getInstance():
// config.js este objeto se crea una sola vez en toda la app
const config = {
apiUrl: 'https://api.ejemplo.com',
timeout: 5000,
intentos: 3,
};
Object.freeze(config); // inmutable
export default config;
// cualquier módulo que importe config obtiene el mismo objeto
import config from './config.js';
import config from './config.js'; // mismo objeto, no se re-evalúa
// conexion-db.js singleton de conexión
let conexion = null;
export async function obtenerConexion() {
if (!conexion) {
conexion = await crearConexion(config.dbUrl);
}
return conexion;
}
Pub/Sub propio con función de limpieza
El patrón publicador/suscriptor desacopla emisores de oyentes. Con closures y un Map es trivial de implementar, y devolver la función de limpieza desde subscribe facilita la gestión del ciclo de vida:
function crearEventBus() {
const oyentes = new Map();
return {
on(evento, callback) {
if (!oyentes.has(evento)) oyentes.set(evento, new Set());
oyentes.get(evento).add(callback);
return () => oyentes.get(evento)?.delete(callback); // función de limpieza
},
off(evento, callback) {
oyentes.get(evento)?.delete(callback);
},
emit(evento, datos) {
oyentes.get(evento)?.forEach(fn => fn(datos));
},
once(evento, callback) {
const limpiar = this.on(evento, (datos) => {
callback(datos);
limpiar();
});
return limpiar;
},
};
}
export const bus = crearEventBus();
// Uso
const limpiar = bus.on('usuario:login', ({ nombre }) => {
console.log(`Bienvenido, ${nombre}`);
});
bus.emit('usuario:login', { nombre: 'Ana' }); // "Bienvenido, Ana"
limpiar(); // eliminar el listener
WeakMap para estado privado
Antes de los campos privados con #, el patrón con WeakMap era la forma de tener estado verdaderamente privado en objetos JavaScript. Sigue siendo útil para añadir estado privado a instancias sin modificar la clase:
const _privado = new WeakMap();
export class CuentaBancaria {
constructor(saldoInicial) {
_privado.set(this, { saldo: saldoInicial, historial: [] });
}
depositar(cantidad) {
const p = _privado.get(this);
p.saldo += cantidad;
p.historial.push({ tipo: 'deposito', cantidad, fecha: new Date() });
return this; // chainable
}
retirar(cantidad) {
const p = _privado.get(this);
if (cantidad > p.saldo) throw new Error('Saldo insuficiente');
p.saldo -= cantidad;
p.historial.push({ tipo: 'retiro', cantidad, fecha: new Date() });
return this;
}
get saldo() {
return _privado.get(this).saldo;
}
}
const cuenta = new CuentaBancaria(1000);
cuenta.depositar(500).retirar(200);
console.log(cuenta.saldo); // 1300
// _privado no es accesible desde fuera del módulo
Observer con WeakRef para evitar memory leaks
Un observador que retiene referencias fuertes a sus callbacks impide que el GC libere los componentes desmontados. Usar WeakRef permite que los suscriptores se liberen automáticamente:
class ObservableDebil {
#oyentes = new Set();
suscribir(callback) {
const ref = new WeakRef(callback);
this.#oyentes.add(ref);
return () => this.#oyentes.delete(ref);
}
notificar(datos) {
for (const ref of this.#oyentes) {
const fn = ref.deref();
if (fn) {
fn(datos);
} else {
this.#oyentes.delete(ref); // limpiar referencias muertas
}
}
}
}
EventTarget nativo como event emitter
La clase EventTarget está disponible en Node.js desde la versión 14 y en todos los navegadores. Extenderla te da un event emitter con la misma API que los elementos del DOM, sin librerías externas:
class TiendaEstado extends EventTarget {
#estado;
constructor(estadoInicial) {
super();
this.#estado = estadoInicial;
}
get estado() {
return { ...this.#estado }; // copia defensiva
}
actualizar(cambios) {
const anterior = this.#estado;
this.#estado = { ...this.#estado, ...cambios };
this.dispatchEvent(
new CustomEvent('cambio', {
detail: { anterior, actual: this.#estado, cambios },
})
);
}
}
const tienda = new TiendaEstado({ usuario: null, tema: 'claro', idioma: 'es' });
tienda.addEventListener('cambio', (e) => {
console.log('Estado actualizado:', e.detail.cambios);
});
tienda.actualizar({ tema: 'oscuro' });
// "Estado actualizado: { tema: 'oscuro' }"
// También funciona con removeEventListener y opciones como { once: true }
tienda.addEventListener('cambio', handler, { once: true });
La tendencia en JavaScript moderno es preferir funciones y closures sobre clases cuando el patrón no requiere herencia, usar el sistema de módulos como contenedor de singletons, y aprovechar las APIs nativas (EventTarget, WeakRef, campos # privados) en lugar de implementar desde cero abstracciones que el lenguaje ya proporciona.
