Diseño de un sistema de complementos de JavaScript
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 funcionalidades de una manera segura y escalable. Esto hace que el proyecto principal sea más valioso y construya una comunidad, todo sin crear una carga de mantenimiento adicional. ¡Qué gran oferta!
Entonces, ¿cómo se construye un sistema de complementos? Respondemos esa pregunta creando uno propio, en JavaScript.
Estoy usando la palabra “complemento”, pero estas cosas a veces reciben otros nombres, como “extensiones”, “complementos” o “módulos”. Como quiera que los llames, el concepto (y el beneficio) es el mismo.
Construyamos un sistema de complementos.
Comenzamos 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 Calculatorconst 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 calculatorbetaCalc.setValue(3); // = 3betaCalc.plus(3); // = 6betaCalc.minus(2); // = 4
Estamos definiendo nuestra calculadora como un objeto literal para simplificar las cosas. La calculadora funciona imprimiendo su resultado mediante console.log
.
La funcionalidad es realmente limitada en este momento. Tenemos un setValue
método que toma un número y lo muestra en la “pantalla”. También tenemos plus
métodos minus
que realizarán una operación sobre el valor mostrado actualmente.
Es hora de agregar más funciones. Comenzamos a crear un sistema de complementos.
El sistema de complementos más pequeño del mundo.
Comenzaremos creando un register
método que otros desarrolladores puedan usar para registrar un complemento con BetaCalc. El trabajo de este método es simple: tomar el complemento externo, tomar su exec
función y adjuntarlo a nuestra calculadora como un nuevo método:
// The Calculatorconst 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 pluginconst squaredPlugin = { name: 'squared', exec: function() { this.setValue(this.currentValue * this.currentValue) }};
// Register the pluginbetaCalc.register(squaredPlugin);
En muchos sistemas de complementos, es común que los complementos tengan dos partes:
- Código de ejecución
- Metadatos (como nombre, descripción, número de versión, dependencias, etc.)
En nuestro complemento, la exec
función contiene nuestro código y name
son nuestros metadatos. Cuando se registra el complemento, la función ejecutiva se adjunta directamente a nuestro betaCalc
objeto como método, dándole acceso a BetaCalc this
.
Ahora BetaCalc tiene un nuevo botón “cuadrado”, que se puede llamar directamente:
betaCalc.setValue(3); // = 3betaCalc.plus(2); // = 5betaCalc.squared(); // = 25betaCalc.squared(); // = 625
Hay muchas cosas que me gustan 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 algunos defectos.
Al dar acceso a los complementos a BetaCalc this
, obtiene acceso de lectura/escritura a todo el código de BetaCalc. Si bien esto es útil para obtener y configurar el archivo 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 a la extensión pero cerrada a la modificación.
Además, la función “al cuadrado” funciona produciendo efectos secundarios. Esto no es raro en JavaScript, pero no se siente muy bien, especialmente cuando otros complementos podrían estar ahí alterando 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.
Demos otro vistazo a una mejor arquitectura de complementos. El siguiente ejemplo cambia tanto la calculadora como su API de complemento:
// The Calculatorconst 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 Pluginconst squaredPlugin = { name: 'squared', exec: function(currentValue) { return currentValue * currentValue; }};
betaCalc.register(squaredPlugin);
// Using the calculatorbetaCalc.setValue(3); // = 3betaCalc.press('plus', 2); // = 5betaCalc.press('squared'); // = 25betaCalc.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 sus propios objetos de complementos. Almacenar nuestros complementos en un plugin
objeto 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 un press
método que busca la función del botón por nombre y luego lo llama. Ahora, cuando llamamos a la exec
función de un complemento, le pasamos el valor actual de la calculadora ( currentValue
) y esperamos que devuelva el nuevo valor de la calculadora.
Básicamente, este nuevo press
método convierte todos los botones de nuestra calculadora en funciones puras. Toman un valor, realiza 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 establecido barreras de seguridad para los autores de complementos, restringiéndolos únicamente 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 archivo currentValue
. Si el autor de un complemento quisiera agregar funciones avanzadas como un botón de “memoria” o una forma de realizar un seguimiento del historial, no podría hacerlo.
Quizás eso esté bien. La cantidad de poder que les da a los autores de complementos es un equilibrio delicado. Darles demasiado poder podría afectar la estabilidad de su proyecto. Pero darles muy poca potencia les dificulta resolver sus problemas; En ese caso, es mejor que no tenga complementos.
¿Qué más podríamos hacer?
Hay mucho más que podríamos hacer 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 nuestro sistema podría fallar 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 ¿qué pasaría si también pudiera registrar devoluciones de llamadas 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 se pudiera registrar un complemento con algunos ajustes iniciales? ¿Podría eso hacer que los complementos sean más flexibles? ¿Qué pasaría 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 punto de partida 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 y un grado de conexión diferente, lo que le brinda muchas buenas opciones de arquitectura de complementos para elegir. Ser consciente de estas opciones le ayudará a equilibrar mejor las necesidades de todos los que utilizan su proyecto.
Además de los patrones en sí, existen muchos buenos principios de desarrollo de software a los que puede recurrir para tomar este tipo de decisiones. He mencionó algunos a lo largo del camino (como el principio 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 debes investigar. No hay nada más doloroso que hacer que todos reescriban sus complementos porque es necesario 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! Es necesario equilibrar muchas consideraciones para crear un sistema que satisfaga las necesidades de todos. ¿Es lo suficientemente simple? ¿Lo suficientemente potente? ¿Funcionará a largo plazo?
Aunque vale la pena el esfuerzo. Tener un buen sistema de complementos ayuda a todos. Los desarrolladores tienen la libertad de resolver sus problemas. Los usuarios finales obtienen una gran cantidad de funciones de suscripción para elegir. Y podrás hacer crecer un ecosistema y una comunidad en torno a tu proyecto. Es una situación en la que todos ganan.
Deja un comentario