Hola colegas
Le recordamos que no hace mucho tiempo publicamos la tercera edición del legendario libro
"JavaScript expresivo " (JavaScript elocuente), que se imprimió en ruso por primera vez, aunque
se encontraron traducciones de alta calidad de ediciones anteriores en Internet.

Sin embargo, ni JavaScript ni el trabajo de investigación del Sr. Haverbeke, por supuesto, no se detiene. Continuando con el tema de JavaScript expresivo, ofrecemos una traducción del artículo sobre el diseño de extensiones (utilizando el desarrollo de un editor de texto como ejemplo), publicado en el blog del autor a fines de agosto de 2019
Hoy en día, se ha puesto de moda estructurar sistemas grandes en forma de muchos paquetes separados. La idea principal que subyace a este enfoque es que es mejor no limitar a las personas a una función específica (propuesta por usted) mediante la implementación de una función, sino proporcionar esta función como un paquete separado que una persona puede descargar junto con el paquete básico del sistema.
Para hacer esto, en términos generales, necesitará ...
- La capacidad de ni siquiera cargar funciones que no necesita, lo cual es especialmente útil cuando se trabaja con sistemas del lado del cliente.
- La capacidad de reemplazar con otra implementación la funcionalidad que no satisface sus necesidades. De esta manera, la presión también se reduce sobre los módulos nucleares, que de lo contrario tendrían que cubrir todo tipo de casos prácticos.
- Verificación de las interfaces del kernel en condiciones reales, implementando características básicas en la parte superior de la interfaz orientada al cliente; tendrá que hacer que la interfaz sea lo suficientemente potente como para al menos hacer frente al soporte de estas características, y asegurarse de que se puedan ensamblar elementos funcionalmente similares a partir de código de terceros.
- Aislamiento entre componentes del sistema. Los participantes del proyecto deberán buscar el paquete específico que les interese. Los paquetes pueden ser versionados, desaprobados o reemplazados por otros, y todo esto no afectará al núcleo.
Este enfoque implica ciertos costos, que se reducen a una complejidad adicional. Para que los usuarios puedan comenzar, puede proporcionarles un paquete de envoltorio, que es todo incluido, pero en algún momento probablemente tendrán que eliminar este envoltorio y abordar la instalación y configuración de paquetes auxiliares por su cuenta, y esto resulta ser más difícil que incluir Nueva característica entregada en la biblioteca monolítica.
En este artículo, trataré de explorar varias formas de diseñar mecanismos de extensibilidad que involucren "extensibilidad a gran escala" e inmediatamente establecer nuevos puntos para una futura expansión.
Extensibilidad¿Qué necesitamos de un sistema extensible? Primero, por supuesto, necesita la capacidad de construir nuevos comportamientos sobre el código externo.
Sin embargo, esto no es suficiente. Déjame desviarte, hablarte sobre un estúpido problema que una vez encontré. Estoy desarrollando un editor de texto. En una versión anterior del
editor de código, el cliente podía
especificar la apariencia de una línea en particular. Fue excelente: el usuario podía seleccionar selectivamente esta o aquella línea de esta manera.
Además, si intenta iniciar el diseño de una línea a partir de dos fragmentos de código mutuamente independientes, entonces comienzan a pisar los talones del otro. La segunda extensión, que se aplica a una línea en particular, sobrescribe los cambios realizados a través de la primera. O, si en algún momento posterior intentamos eliminar los cambios de diseño que hicimos con la ayuda del primer código, como resultado, sobrescribiremos el diseño hecho del segundo fragmento de código.
La solución fue permitir que el código se
agregue (y
elimine ) en lugar de instalarse, de modo que las dos extensiones puedan interactuar con la misma línea sin interrumpir el trabajo del otro.
En una formulación más general, es necesario asegurarse de que las extensiones se puedan combinar, incluso si son "completamente inconscientes" entre sí, y para que no surjan conflictos durante sus interacciones.
Para que esto funcione, cada extensión debe estar expuesta a cualquier número de agentes a la vez. La forma en que se procesará cada uno de los efectos difiere según el caso específico. Aquí hay algunas estrategias que pueden resultarle útiles:
- Todos los cambios surten efecto. Por ejemplo, si agregamos una clase CSS a un elemento, o mostramos un widget en una posición dada en el texto, estas dos cosas se pueden hacer de inmediato. Por supuesto, se requerirá algún tipo de orden: los widgets deben mostrarse en una secuencia predecible y bien definida.
- Los cambios se alinean en la tubería. Un ejemplo de este enfoque es un controlador que puede filtrar los cambios realizados en un documento antes de que surtan efecto. Cada controlador recibe un cambio realizado por el controlador anterior, y el controlador posterior puede continuar con dichas modificaciones. El pedido aquí no es crucial, pero puede ser significativo.
- El enfoque por orden de llegada. Este enfoque es aplicable, por ejemplo, con controladores de eventos. Cada controlador tiene la oportunidad de jugar con el evento hasta que uno de los controladores anuncie que todo ya está hecho, y luego el siguiente controlador no se verá afectado.
- A veces es necesario seleccionar solo un valor, por ejemplo, determinar el valor de un parámetro de configuración específico. Aquí sería apropiado usar algún tipo de operador (por ejemplo, lógico o, lógico y, mínimo, máximo) para reducir la entrada potencial a un solo valor. Por ejemplo, un editor puede cambiar al modo de solo lectura si al menos una extensión lo requiere, o la longitud máxima de un documento puede ser el mínimo de todos los valores proporcionados para esta opción.
En muchas de esas situaciones, el orden es importante. Aquí me refiero al cumplimiento del orden en que se aplican los efectos, esta secuencia debe ser controlada y predecible.
Esta es una de esas situaciones en las que los sistemas de extensión imperativos generalmente no son muy buenos, cuyo funcionamiento depende de los efectos secundarios. Por ejemplo, la operación
addEventListener
de un modelo DOM del
addEventListener
requiere que los controladores de eventos se llamen en el orden en que están registrados. Esto es normal si todas las llamadas están controladas por un solo sistema, o si el orden de las llamadas no es tan importante. Sin embargo, si tiene muchos componentes de software que agregan controladores independientemente uno del otro, puede ser muy difícil predecir cuáles se llamarán en primer lugar.
Enfoque simplePermíteme darte un ejemplo concreto: primero apliqué una estrategia tan modular mientras desarrollaba ProseMirror, un sistema para editar texto enriquecido. El núcleo de este sistema en sí mismo es esencialmente inútil: se basa en paquetes adicionales para describir la estructura de los documentos, vincular claves y mantener un historial de cancelaciones. Aunque es realmente un poco difícil usar este sistema, ha encontrado aplicación en programas en los que necesita configurar cosas que no son compatibles con los editores clásicos.
El mecanismo de extensión ProseMirror es relativamente sencillo. Al crear un editor, se especifica una única matriz de objetos conectados en el código del cliente. Cada uno de estos objetos de complemento puede afectar varios aspectos del editor y hacer cosas como agregar bits de datos de estado o manejar eventos de interfaz.
Todos estos aspectos fueron diseñados para trabajar con una matriz ordenada de valores de configuración utilizando una de las estrategias descritas anteriormente. Por ejemplo, cuando necesita especificar muchos diccionarios con valores, la prioridad de las siguientes instancias de extensión para la vinculación de teclas depende del orden en el que especifique estas instancias. La primera extensión para la combinación de teclas, saber qué hacer con esta combinación de teclas, lo procesa.
Por lo general, dicho mecanismo resulta ser bastante poderoso, y pueden aprovecharlo. Pero tarde o temprano, el sistema de extensión alcanza tal complejidad que resulta inconveniente usarlo.
- Si el complemento tiene muchos efectos, solo puede esperar que todos necesiten la misma prioridad en relación con otros complementos, o que tenga que dividirlos en complementos más pequeños para organizar correctamente su orden.
- En general, organizar complementos se vuelve muy escrupuloso, porque el usuario final no siempre tiene claro qué complementos pueden interferir con qué otros complementos si obtienen una prioridad más alta. Los errores cometidos en tales casos generalmente ocurren solo en tiempo de ejecución, cuando se usa una oportunidad específica, por lo que son fáciles de pasar por alto.
- Si los complementos se crean sobre la base de otros complementos, entonces este hecho debe documentarse y esperar que los usuarios no olviden incluir las dependencias apropiadas (en ese paso de clasificación, cuando sea necesario).
CodeMirror
versión 6 es una versión reescrita del
editor de código del mismo nombre. En este proyecto, trato de desarrollar un enfoque modular. Para hacer esto, necesito un sistema de extensión más expresivo. Analicemos algunos de los desafíos que tuvimos que enfrentar al diseñar un sistema de este tipo.
OrdenarEs fácil diseñar un sistema que le brinde un control completo sobre el pedido de extensiones. Sin embargo, es mucho más difícil diseñar un sistema con el que sea agradable trabajar y que, al mismo tiempo, le permita combinar el código de varias extensiones sin numerosas intervenciones de la categoría "ahora vigile sus manos".
Cuando se trata de ordenar, sucede, realmente quiero recurrir al trabajo con valores prioritarios. Como ejemplo, la propiedad CSS
z-index
indica el número de la posición ocupada por este elemento en la profundidad de la pila.
Como puede ver en el ejemplo de valores de
z-index
ridículamente grandes, que a veces se encuentran en las hojas de estilo, esta forma de indicar la prioridad es problemática. El módulo en sí mismo no sabe qué valores de prioridad tienen otros módulos. Las opciones son solo puntos en el medio de un rango numérico indiferenciado. Puede establecer un valor enorme (o profundamente negativo) para tratar de llegar a los extremos de este espectro, pero el resto del trabajo se reduce a la adivinación.
La situación se puede mejorar un poco si define un conjunto limitado de categorías de prioridad claramente definidas, de modo que las extensiones puedan caracterizar el "nivel" general de su prioridad. Además, necesitará una cierta forma de romper los enlaces dentro de las categorías.
Agrupación y deduplicaciónComo mencioné anteriormente, tan pronto como comience a depender seriamente de las extensiones, puede surgir una situación en la que algunas extensiones usarán otras cuando funcionen. Si administra dependencias manualmente, entonces este enfoque no escala bien; por lo tanto, sería bueno poder abrir un grupo de extensiones al mismo tiempo.
Sin embargo, este enfoque no solo agrava aún más el problema de prioridad, sino que también presenta otro problema: muchas otras extensiones pueden depender de una extensión en particular, y si las extensiones se presentan como valores, puede ser que la misma extensión se cargue varias veces . Para algunos tipos de extensiones, como los controladores de eventos, esto es normal. En otros casos, como con un historial de cancelación y una biblioteca de información sobre herramientas, este enfoque será un desperdicio e incluso puede romper todo.
Entonces, si permitimos el diseño de extensiones, esto introduce cierta complejidad adicional en nuestro sistema relacionado con la gestión de dependencias. Debe poder reconocer tales extensiones que no deben duplicarse y descargarlas exactamente una a la vez.
Sin embargo, dado que en la mayoría de los casos las extensiones se pueden configurar y, por lo tanto, no todas las instancias de la misma extensión serán exactamente iguales, no podemos simplemente tomar una instancia y trabajar con ella. Tendremos que considerar alguna fusión significativa de tales casos (o informar un error si la fusión de interés para nosotros es imposible).
ProyectoAquí describiré en términos generales lo que estamos haciendo en CodeMirror 6. Esto es solo un boceto, no una solución fallida. Es posible que este sistema se desarrolle aún más cuando la biblioteca se estabilice.
La primitiva principal utilizada en este enfoque se llama comportamiento. Los comportamientos son solo aquellas características en las que puede construir con extensiones, especificando valores para ellas. Un ejemplo es el comportamiento de un campo de estado donde, con la ayuda de extensiones, puede agregar nuevos campos, proporcionando una descripción del campo. Otro ejemplo es el comportamiento de los controladores de eventos en un navegador; en este caso, con la ayuda de extensiones, podemos agregar nuestros propios controladores.
Desde el punto de vista del consumidor de comportamientos, los comportamientos mismos, configurados de cierta manera en una instancia específica del editor, dan una secuencia ordenada de valores, y los valores anteriores tienen mayor prioridad. Cada comportamiento tiene un tipo, y los valores proporcionados deben coincidir con ese tipo.
El comportamiento se representa como un valor utilizado tanto para declarar una instancia del comportamiento como para acceder a los valores que tiene el comportamiento. Hay varios comportamientos integrados en la biblioteca, pero el código externo puede definir sus propios comportamientos. Por ejemplo, en la extensión que define el intervalo entre números de línea, se puede definir un comportamiento que permita que otro código agregue marcadores adicionales en este intervalo.
Una extensión es un valor que se puede usar al configurar el editor. Se pasa una matriz de dichos valores durante la inicialización. Cada extensión está permitida en cero o más comportamientos.
Una extensión tan simple puede considerarse una instancia de comportamiento. Si especificamos un valor para el comportamiento, el código nos devuelve el valor de la extensión que genera este comportamiento.
Una secuencia de extensiones también se puede agrupar en una sola extensión. Por ejemplo, en la configuración del editor para trabajar con un lenguaje de programación específico, puede extraer varias otras extensiones, por ejemplo, gramática para analizar y resaltar texto, información sobre la sangría necesaria, una fuente de autocompletado que mostrará correctamente las indicaciones para completar líneas en este idioma. Por lo tanto, es posible hacer una extensión de lenguaje único en la que simplemente recolectemos todas estas extensiones correspondientes y las agrupemos, lo que da como resultado un valor único.
Al crear una versión simple de dicho sistema, podríamos detenernos en esto simplemente alineando todas las extensiones anidadas en una matriz de extensiones de comportamiento. Luego podrían agruparse por tipo de comportamiento y luego construir secuencias ordenadas de valores de comportamiento.
Sin embargo, queda por hacer frente a la deduplicación y proporcionar un mejor control sobre los pedidos.
Los valores de las extensiones relacionadas con el tercer tipo, extensiones únicas, solo ayudan a lograr la deduplicación. Las extensiones que no deben instanciarse dos veces en el mismo editor son de este tipo. Para determinar dicha extensión, debe especificar el
tipo de especificación , es decir, el tipo de valor de configuración esperado por el constructor de la extensión, y también especificar
una función de instanciación que tome una matriz de dichos valores especificados y devuelva la extensión.
Las extensiones únicas complican el proceso de resolver un conjunto de extensiones en un conjunto de comportamientos. Si hay extensiones únicas en el conjunto alineado de extensiones, entonces el mecanismo de resolución debe seleccionar el tipo de extensión única, recopilar todas sus instancias y llamar a la función de instanciación correspondiente junto con las especificaciones, y luego reemplazarlas todas con el resultado (en una sola copia).
(Hay otro inconveniente: deben resolverse en el orden correcto. Si primero habilita la extensión X única, pero luego obtiene otra X como resultado de la resolución, esto será incorrecto, ya que todas las instancias de X deben estar juntas. Desde la función de instanciación la expansión es limpia, el sistema hace frente a esta situación por prueba y error, reiniciando el proceso y registrando información sobre lo que fue posible estudiar, estando en esta situación).
Finalmente, debe resolver el problema con las siguientes reglas. El enfoque básico sigue siendo el mismo: mantener el orden en que se propusieron las extensiones. Las extensiones compuestas se alinean en el mismo orden en el punto donde ocurren. El resultado de resolver una extensión única se inserta cuando se enciende por primera vez.
Sin embargo, las extensiones pueden relacionar algunas de sus sub extensiones con categorías que tienen una prioridad diferente. El sistema proporciona cuatro categorías:
retroceso (surte efecto después de que sucedan otras cosas),
predeterminado (predeterminado),
extender (mayor prioridad que el volumen) y
anular (probablemente debería ir primero). En la práctica, la clasificación se realiza primero por categoría y luego por posición inicial.
Entonces, una extensión de enlace de teclas, que tiene una prioridad baja, y un controlador de eventos con prioridad regular, se basa en ellos que está de moda obtener una extensión compuesta construida a partir del resultado de una extensión de enlace de teclas (que no requiere saber en qué comportamiento consiste) con un nivel de prioridad de reserva y de una instancia con el comportamiento del controlador de eventos.
Este enfoque, que le permite combinar extensiones sin pensar en lo que hacen "dentro", parece ser un gran logro. Las extensiones que modelamos anteriormente en este artículo incluyen: dos sistemas de análisis que exhiben el mismo comportamiento a nivel de sintaxis, servicio de resaltado de sintaxis, servicio de sangría inteligente, historial de cancelación, servicio de espaciado de línea, corchetes de cierre automático, encuadernación de teclas y selección múltiple, todo funciona bien
Hay varios conceptos nuevos que un usuario debe aprender para usar este sistema. Además, trabajar con un sistema de este tipo es de hecho un poco más complicado que con los sistemas imperativos tradicionales adoptados en la comunidad JavaScript (llamamos a un método para agregar / eliminar un efecto). Sin embargo, si las extensiones se arreglan correctamente, los beneficios de esto superan los costos asociados.