Patrones de diseño en JavaScript: Factory, Strategy, Observer y Command con ES6+

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP