WordPress tiene complementos. jQuery tiene complementos. Gatsby, Eleventy y Vue también lo hacen.
Los complementos son una característica común de las bibliotecas y los marcos, y por una buena razón: permiten a los desarrolladores agregar funcionalidad, de una manera segura y escalable. Esto hace que el proyecto principal sea más valioso y crea una comunidad, todo sin crear una carga de mantenimiento adicional. ¡Qué gran oferta!
Entonces, ¿cómo se hace para construir un sistema de complementos? Respondamos esa pregunta construyendo uno propio, en JavaScript.
Estoy usando la palabra “complemento”, pero estas cosas a veces se denominan con otros nombres, como “extensiones”, “complementos” o “módulos”. Como sea que los llame, el concepto (y el beneficio) es el mismo.
Construyamos un sistema de complementos
Comencemos con un proyecto de ejemplo llamado BetaCalc. El objetivo de BetaCalc es ser una calculadora JavaScript minimalista a la que otros desarrolladores puedan agregar “botones”. Aquí hay un código básico para comenzar:
// The Calculator
const betaCalc = {
currentValue: 0,
setValue(newValue) {
this.currentValue = newValue;
console.log(this.currentValue);
},
plus(addend) {
this.setValue(this.currentValue + addend);
},
minus(subtrahend) {
this.setValue(this.currentValue - subtrahend);
}
};
// Using the calculator
betaCalc.setValue(3); // => 3
betaCalc.plus(3); // => 6
betaCalc.minus(2); // => 4
Estamos definiendo nuestra calculadora como un objeto literal para simplificar las cosas. La calculadora funciona imprimiendo su resultado a través de console.log
.
La funcionalidad es realmente limitada en este momento. Tenemos una setValue
método, que toma un número y lo muestra en la “pantalla”. También tenemos plus
y minus
métodos, que realizarán una operación en el valor mostrado actualmente.
Es hora de agregar más funciones. Comencemos por crear un sistema de complementos.
El sistema de complementos más pequeño del mundo
Empezaremos creando un register
método que otros desarrolladores pueden utilizar para registrar un complemento con BetaCalc. El trabajo de este método es simple: tome el complemento externo, tome su exec
y adjuntarlo a nuestra calculadora como un nuevo método:
// The Calculator
const betaCalc = {
// ...other calculator code up here
register(plugin) {
const { name, exec } = plugin;
this[name] = exec;
}
};
Y aquí hay un complemento de ejemplo, que le da a nuestra calculadora un botón “cuadrado”:
// Define the plugin
const squaredPlugin = {
name: 'squared',
exec: function() {
this.setValue(this.currentValue * this.currentValue)
}
};
// Register the plugin
betaCalc.register(squaredPlugin);
En muchos sistemas de complementos, es común que los complementos tengan dos partes:
- Código a ejecutar
- Metadatos (como nombre, descripción, número de versión, dependencias, etc.)
En nuestro complemento, el exec
La función contiene nuestro código, y la name
son nuestros metadatos. Cuando se registra el complemento, la función exec se adjunta directamente a nuestro betaCalc
objeto como método, dándole acceso a BetaCalc this
.
Entonces, BetaCalc tiene un nuevo botón “cuadrado”, que se puede llamar directamente:
betaCalc.setValue(3); // => 3
betaCalc.plus(2); // => 5
betaCalc.squared(); // => 25
betaCalc.squared(); // => 625
Hay mucho que me gusta de este sistema. El complemento es un objeto literal simple que se puede pasar a nuestra función. Esto significa que los complementos se pueden descargar a través de npm e importar como módulos ES6. ¡La fácil distribución es muy importante!
Pero nuestro sistema tiene algunas fallas.
Al dar acceso a los complementos a BetaCalc this
, obtienen acceso de lectura / escritura a todo el código de BetaCalc. Si bien esto es útil para obtener y configurar el currentValue
, también es peligroso. Si un complemento redefiniera una función interna (como setValue
), podría producir resultados inesperados para BetaCalc y otros complementos. Esto viola el principio abierto-cerrado, que establece que una entidad de software debe estar abierta para extensión pero cerrada para modificación.
Además, la función “al cuadrado” actúa produciendo efectos secundarios. Eso no es raro en JavaScript, pero no se siente muy bien, especialmente cuando otros complementos podrían estar jugando con el mismo estado interno. Un enfoque más funcional contribuiría en gran medida a hacer que nuestro sistema sea más seguro y predecible.
Una mejor arquitectura de complementos
Echemos otro vistazo a una mejor arquitectura de complementos. El siguiente ejemplo cambia tanto la calculadora como su API de complemento:
// The Calculator
const betaCalc = {
currentValue: 0,
setValue(value) {
this.currentValue = value;
console.log(this.currentValue);
},
core: {
'plus': (currentVal, addend) => currentVal + addend,
'minus': (currentVal, subtrahend) => currentVal - subtrahend
},
plugins: {},
press(buttonName, newVal) {
const func = this.core[buttonName] || this.plugins[buttonName];
this.setValue(func(this.currentValue, newVal));
},
register(plugin) {
const { name, exec } = plugin;
this.plugins[name] = exec;
}
};
// Our Plugin
const squaredPlugin = {
name: 'squared',
exec: function(currentValue) {
return currentValue * currentValue;
}
};
betaCalc.register(squaredPlugin);
// Using the calculator
betaCalc.setValue(3); // => 3
betaCalc.press('plus', 2); // => 5
betaCalc.press('squared'); // => 25
betaCalc.press('squared'); // => 625
Tenemos algunos cambios notables aquí.
Primero, hemos separado los complementos de los métodos de calculadora “principales” (como plus
y minus
), colocándolos en su propio objeto de complementos. Almacenar nuestros complementos en un plugin
hace que nuestro sistema sea más seguro. Ahora los complementos que acceden a esto no pueden ver las propiedades de BetaCalc, solo pueden ver las propiedades de betaCalc.plugins
.
En segundo lugar, hemos implementado una press
, que busca la función del botón por su nombre y luego lo llama. Ahora, cuando llamamos a un complemento exec
función, le pasamos el valor actual de la calculadora (currentValue
), y esperamos que devuelva el nuevo valor de la calculadora.
Esencialmente, este nuevo press
El método convierte todos los botones de nuestra calculadora en funciones puras. Toman un valor, realizan una operación y devuelven el resultado. Esto tiene muchos beneficios:
- Simplifica la API.
- Facilita las pruebas (tanto para BetaCalc como para los propios complementos).
- Reduce las dependencias de nuestro sistema, haciéndolo más débilmente acoplado.
Esta nueva arquitectura es más limitada que el primer ejemplo, pero en el buen sentido. Básicamente, hemos puesto barreras para los autores de complementos, restringiéndolos solo al tipo de cambios que queremos que realicen.
De hecho, ¡podría ser demasiado restrictivo! Ahora nuestros complementos de calculadora solo pueden realizar operaciones en el currentValue
. Si el autor de un complemento quisiera agregar una funcionalidad avanzada como un botón de “memoria” o una forma de rastrear el historial, no podría hacerlo.
Quizás eso esté bien. La cantidad de poder que le da a los autores de complementos es un equilibrio delicado. Darles demasiado poder podría afectar la estabilidad de su proyecto. Pero darles muy poco poder hace que sea difícil para ellos resolver sus problemas; en ese caso, es posible que no tenga complementos.
¿Qué más podemos hacer?
Podemos hacer mucho más para mejorar nuestro sistema.
Podríamos agregar manejo de errores para notificar a los autores de complementos si olvidan definir un nombre o devolver un valor. Es bueno pensar como un desarrollador de control de calidad e imaginar cómo podría fallar nuestro sistema para que podamos manejar esos casos de manera proactiva.
Podríamos ampliar el alcance de lo que puede hacer un complemento. Actualmente, un complemento BetaCalc puede agregar un botón. Pero, ¿y si también pudiera registrar devoluciones de llamada para ciertos eventos del ciclo de vida, como cuando la calculadora está a punto de mostrar un valor? ¿O qué pasaría si hubiera un lugar dedicado para almacenar una parte del estado en múltiples interacciones? ¿Eso abriría algunos nuevos casos de uso?
También podríamos ampliar el registro de complementos. ¿Qué pasaría si un complemento pudiera registrarse con algunas configuraciones iniciales? ¿Eso podría hacer que los complementos sean más flexibles? ¿Qué pasa si el autor de un complemento quisiera registrar un conjunto completo de botones en lugar de uno solo, como un “Paquete de estadísticas BetaCalc”? ¿Qué cambios serían necesarios para respaldar eso?
Tu sistema de complementos
Tanto BetaCalc como su sistema de complementos son deliberadamente simples. Si su proyecto es más grande, entonces querrá explorar otras arquitecturas de complementos.
Un buen lugar para comenzar es buscar en proyectos existentes ejemplos de sistemas de complementos exitosos. Para JavaScript, eso podría significar jQuery, Gatsby, D3, CKEditor u otros.
Es posible que también desee familiarizarse con varios patrones de diseño de JavaScript. (Addy Osmani tiene un libro sobre el tema). Cada patrón proporciona una interfaz diferente y un grado de acoplamiento, lo que le brinda una gran cantidad de buenas opciones de arquitectura de complementos para elegir. Conocer estas opciones le ayuda a equilibrar mejor las necesidades de todos los que utilizan su proyecto.
Además de los patrones en sí, hay muchos buenos principios de desarrollo de software en los que puede basarse para tomar este tipo de decisiones. He mencionado algunos a lo largo del camino (como el principio de abierto-cerrado y el acoplamiento flexible), pero algunos otros relevantes incluyen la Ley de Demeter y la inyección de dependencia.
Sé que parece mucho, pero tienes que investigar. Nada es más doloroso que hacer que todos reescriban sus complementos porque necesita cambiar la arquitectura del complemento. Es una forma rápida de perder la confianza y disuadir a las personas de contribuir en el futuro.
Conclusión
¡Escribir una buena arquitectura de complementos desde cero es difícil! Debe equilibrar muchas consideraciones para construir un sistema que satisfaga las necesidades de todos. ¿Es lo suficientemente simple? ¿Lo suficientemente poderoso? ¿Funcionará a largo plazo?
Sin embargo, vale la pena el esfuerzo. Tener un buen sistema de complementos ayuda a todos. Los desarrolladores obtienen la libertad de resolver sus problemas. Los usuarios finales obtienen una gran cantidad de funciones opcionales para elegir. Y puedes desarrollar un ecosistema y una comunidad en torno a tu proyecto. Es una situación en la que todos ganan.