Closures en JavaScript: construyendo un sistema de plugins sencillo

Close-up of JavaScript code on a computer screen, showing web development programming.
Foto de Marek Prášil en Pexels

Los closures son una de esas características de JavaScript que, bien aprovechadas, nos permiten diseñar APIs limpias y extensibles. En este artículo vamos a ver cómo utilizarlos para montar un sistema de plugins ligero que cualquier proyecto frontend puede integrar sin depender de librerías externas.

¿Por qué un motor de plugins?

Un plugin es simplemente una pieza de código que se conecta a una aplicación para añadir o modificar funcionalidades. Cuando la arquitectura está bien pensada, los plugins pueden:

  • Encapsular su propio estado.
  • Exponer una interfaz clara para la aplicación anfitriona.
  • Ser añadidos o eliminados en tiempo de ejecución.

Todo esto es posible gracias a los closures: cada plugin puede cerrar sobre sus variables internas y, al mismo tiempo, devolver una función que la aplicación pueda invocar.

La base: una fábrica de plugins

Empezamos definiendo una createPlugin que recibe una configuración y devuelve una función que, cuando se registra, recibe el core de la aplicación.

function createPlugin(options) {
  // Variables privadas del plugin
  const name = options.name || 'plugin';
  let counter = 0;

  // La función que la app llamará para registrar el plugin
  return function register(core) {
    // El closure permite que "name" y "counter" persistan
    core.register(name, () => {
      counter++;
      console.log(`[${name}] llamada ${counter} veces`);
    });
  };
}

Observa que name y counter nunca salen del plugin; sólo la función register las captura. La aplicación no necesita saber nada de su implementación interna.

El núcleo de la aplicación

Para que el ejemplo sea completo, implementemos un core muy sencillo que gestiona los plugins registrados:

function createCore() {
  const registry = {};

  return {
    register(pluginName, fn) {
      if (registry[pluginName]) {
        console.warn(`Plugin "${pluginName}" ya está registrado`);
        return;
      }
      registry[pluginName] = fn;
      console.log(`Plugin "${pluginName}" registrado`);
    },
    run(pluginName) {
      const fn = registry[pluginName];
      if (!fn) {
        console.error(`Plugin "${pluginName}" no encontrado`);
        return;
      }
      fn();
    },
    list() {
      return Object.keys(registry);
    }
  };
}

El core expone tres métodos: register para añadir plugins, run para ejecutarlos y list para inspeccionar qué está disponible.

Uso práctico

Con la fábrica y el núcleo listos, veamos cómo montar todo:

// Creamos el core de la app
const app = createCore();

// Definimos dos plugins con distinta lógica
const loggerPlugin = createPlugin({ name: 'logger' });
const analyticsPlugin = createPlugin({ name: 'analytics' });

// Registramos los plugins
loggerPlugin(app);
analyticsPlugin(app);

// Probamos su ejecución
app.run('logger');      // [logger] llamada 1 veces
app.run('logger');      // [logger] llamada 2 veces
app.run('analytics');   // [analytics] llamada 1 veces

console.log('Plugins activos:', app.list()); // Plugins activos: [ 'logger', 'analytics' ]

Gracias al closure, cada plugin mantiene su propio contador counter sin interferir con los demás. Si quisieras añadir configuración extra (por ejemplo, una URL de endpoint), simplemente la pasas a createPlugin y la guardas en una variable privada.

Ventajas de este enfoque

  • Encapsulamiento total: el estado interno está protegido contra accesos externos.
  • Flexibilidad: puedes registrar o retirar plugins en cualquier momento.
  • Reusabilidad: la fábrica createPlugin puede reutilizarse para cientos de casos diferentes.
  • Sin dependencias: todo el código es puro JavaScript, ideal para proyectos que buscan ligereza.

Extensiones habituales

Una vez tienes la base, puedes añadir funcionalidades como:

  • Hooks de ciclo de vida: permitir que el plugin tenga init y destroy.
  • Prioridad de ejecución: ordenar los plugins según un número de orden.
  • Comunicación entre plugins: exponer un eventBus dentro del core para que los plugins se suscriban a eventos.

En todos esos casos, el patrón sigue siendo el mismo: la función devuelta cierra sobre sus variables y se comunica con el núcleo mediante una API pública y bien definida.

Conclusión

Los closures no son sólo una curiosidad teórica; son la herramienta que nos permite crear módulos de código aislados, con estado propio y una interfaz mínima. Con la pequeña fábrica que hemos visto, puedes montar rápidamente un motor de plugins que escale desde una simple página estática hasta una SPA compleja.

La próxima vez que necesites añadir funcionalidades extensibles a tu proyecto, piensa en los closures como la base de tu arquitectura. ¡A probarlo y a compartir tus propios plugins!