Comunicación entre componentes desacoplados mediante eventos.

8

Tenemos una aplicación web donde tenemos muchos (> 50) pequeños componentes web que interactúan entre sí.

Para mantener todo desacoplado, tenemos como regla que ningún componente puede hacer referencia directa a otro. En cambio, los componentes activan eventos que luego se conectan (en la aplicación "principal") para llamar a los métodos de otro componente.

A medida que pasó el tiempo, más y más componentes se agregaron y el archivo de la aplicación "principal" se llenó de fragmentos de código que se ven así:

buttonsToolbar.addEventListener('request-toggle-contact-form-modal', () => {
  contactForm.toggle()
})

buttonsToolbar.addEventListener('request-toggle-bug-reporter-modal', () => {
  bugReporter.toggle()
})

// ... etc

Para mejorar esto, agrupamos una funcionalidad similar, en una Class, asígnele un nombre relevante, pase los elementos participantes al crear instancias y maneje el "cableado" dentro de Class, de la siguiente manera:

class Contact {
  constructor(contactForm, bugReporter, buttonsToolbar) {
    this.contactForm = contactForm
    this.bugReporterForm = bugReporterForm
    this.buttonsToolbar = buttonsToolbar

    this.buttonsToolbar
      .addEventListener('request-toggle-contact-form-modal', () => {
        this.toggleContactForm()
      })

    this.buttonsToolbar
      .addEventListener('request-toggle-bug-reporter-modal', () => {
        this.toggleBugReporterForm()
      })
  }

  toggleContactForm() {
    this.contactForm.toggle()
  }

  toggleBugReporterForm() {
    this.bugReporterForm.toggle()
  }
}

e instanciamos así:

<html>
  <contact-form></contact-form>
  <bug-reporter></bug-reporter>

  <script>
    const contact = new Contact(
      document.querySelector('contact-form'),
      document.querySelector('bug-form')
    )
  </script>
</html>

Estoy realmente cansado de introducir patrones propios, especialmente los que no son realmente OOP-y ya que los estoy usando Classescomo simples contenedores de inicialización, por falta de una palabra mejor.

¿Existe un patrón definido mejor / más conocido para manejar este tipo de tareas que me falta?

nicholaswmin
fuente
2
Esto en realidad se ve ligeramente asombroso.
Robert Harvey
Cuando recuerdo correctamente, en esta antigua pregunta tuya ya lo llamaste mediador, que también es el nombre del patrón del libro GoF, por lo que este es definitivamente un patrón OOP.
Doc Brown
@RobertHarvey Bueno, tu palabra tiene mucho peso para mí; ¿Lo harías de otra manera? No estoy seguro de pensar demasiado en esto.
nicholaswmin
2
No pienses demasiado en esto. Su clase de "cableado" me parece SÓLIDA, si funciona y está satisfecho con el nombre, no debería importar cómo se llame.
Doc Brown
1
@NicholasKyriakides: mejor pregúntale a uno de tus compañeros de trabajo (que seguramente conoce el sistema mejor que yo) un buen nombre, no un extraño como yo de internet.
Doc Brown

Respuestas:

6

El código que tienes es bastante bueno. Lo que parece un poco desagradable es que el código de inicialización no es parte del objeto en sí. Es decir, puede crear una instancia de un objeto, pero si olvida llamar a su clase de cableado, es inútil.

Considere que un Centro de notificaciones (también conocido como Event Bus) definió algo como esto:

class NotificationCenter(){
    constructor(){
        this.dictionary = {}
    }
    register(message, callback){
        if not this.dictionary.contains(message){
            this.dictionary[message] = []
        }
        this.dictionary[message].append(callback)
    }
    notify(message, payload){
        if this.dictionary.contains(message){
            for each callback in this.dictionary[message]{
                callback(payload)
            }
        }
    }
}

Este es un controlador de eventos de despacho múltiple de bricolaje. Entonces podría hacer su propio cableado simplemente requiriendo un NotificationCenter como argumento de constructor. Enviar mensajes a él y esperar a que pase sus cargas es el único contacto que tiene con el sistema, por lo que es muy SÓLIDO.

class Toolbar{
    constructor(notificationCenter){
        this.NC = notificationCenter
        this.NC.register('request-toggle-contact-form-modal', (payload) => {
            this.toggleContactForm(payload)
          }
    }
    toolbarButtonClicked(e){
        this.NC.notify('toolbar-button-click-event', e)
    }
}

Nota: Utilicé literales de cadena in situ para que las claves sean consistentes con el estilo utilizado en la pregunta y por simplicidad. Esto no es aconsejable debido al riesgo de errores tipográficos. En su lugar, considere usar una enumeración o constantes de cadena.

En el código anterior, la barra de herramientas es responsable de informar al NotificationCenter en qué tipo de eventos está interesado y de publicar todas sus interacciones externas a través del método de notificación. Cualquier otra clase interesada en el toolbar-button-click-eventsimplemente se registraría en su constructor.

Las variaciones interesantes en este patrón incluyen:

  • Uso de múltiples NC para manejar diferentes partes del sistema
  • Tener el método de notificación genera un hilo para cada notificación, en lugar de bloquear en serie
  • Usar una lista de prioridad en lugar de una lista regular dentro del NC para garantizar un pedido parcial sobre qué componentes se notifican primero
  • Regístrese devolviendo una identificación que se puede usar para cancelar el registro más tarde
  • Omita el argumento del mensaje y solo envíe en función de la clase / tipo del mensaje

Las características interesantes incluyen:

  • Instrumentar el NC es tan fácil como registrar registradores para imprimir cargas útiles
  • Probar la interacción de uno o más componentes es simplemente una cuestión de instanciarlos, agregar oyentes para los resultados esperados y enviar mensajes
  • Agregar componentes nuevos para escuchar mensajes antiguos es trivial
  • Agregar nuevos componentes enviando mensajes a los antiguos es trivial

Los trucos interesantes y los posibles remedios incluyen:

  • Los eventos que desencadenan otros eventos pueden ser confusos.
    • Incluya una ID de remitente en el evento para identificar el origen de un evento inesperado.
  • Cada componente no tiene idea de si alguna parte determinada del sistema está en funcionamiento antes de recibir un evento, por lo que se pueden descartar los primeros mensajes.
    • Esto puede ser manejado por el código que crea los componentes enviando un mensaje de 'sistema listo', que por supuesto los componentes interesados ​​tendrían que registrarse.
  • El bus de eventos crea una interfaz implícita entre componentes, lo que significa que no hay forma de que el compilador se asegure de haber implementado todo lo que debería.
    • Los argumentos estándar entre estático y dinámico se aplican aquí.
  • Este enfoque agrupa componentes, no necesariamente comportamientos. El seguimiento de eventos a través del sistema puede requerir más trabajo aquí que el enfoque del OP. Por ejemplo, OP podría tener todos los oyentes relacionados con el ahorro configurados juntos y los oyentes relacionados con la eliminación configurados juntos en otro lugar.
    • Esto puede mitigarse con una buena denominación de eventos y documentación, como un diagrama de flujo. (Sí, la documentación está fuera de sintonía con el código). También puede agregar listas de controladores previos y posteriores a la captura general que reciben todos los mensajes e imprimen quién envió qué en qué orden.
Joel Harmon
fuente
Este enfoque parece una reminiscencia de la arquitectura de Event Bus. La única diferencia es que su registro es una cadena de tema en lugar de un tipo de mensaje. La principal debilidad del uso de cadenas es que están sujetas a errores de escritura, lo que significa que la notificación o el oyente podrían estar mal escritos y sería difícil de depurar.
Berin Loritsch
Por lo que puedo decir, este es un ejemplo clásico de un mediador . Un problema con este enfoque es que combina un componente con el bus de eventos / mediador. ¿Qué buttonsToolbarsucede si quiero mover un componente, por ejemplo, a otro proyecto que no usa un bus de eventos?
Nicholaswmin
+1 El beneficio del mediador es que le permite registrarse contra cadenas / enumeraciones y tener el acoplamiento flexible dentro de la clase. Si mueve el cableado fuera del objeto a su clase principal / configuración, entonces conoce todos los objetos y podría conectarlos directamente a eventos / funciones sin preocuparse por el acoplamiento. @NicholasKyriakides Elija uno u otro en lugar de tratar de usar ambos
Ewan
Con la arquitectura clásica del bus de eventos, el único acoplamiento es con el mensaje mismo. El mensaje es típicamente un objeto inmutable. El objeto que envía mensajes solo necesita la interfaz del editor para enviar los mensajes. Si usa el tipo de objeto de mensaje, entonces solo necesita publicar el objeto de mensaje. Si usa una cadena, debe proporcionar tanto la cadena del tema como la carga útil del mensaje (a menos que la cadena sea la carga útil). El uso de cadenas significa que solo debe ser meticuloso con los valores en ambos lados.
Berin Loritsch
@NicholasKyriakides ¿Qué sucede si mueve su código original a una nueva solución? Debe traer su clase de configuración y cambiarla por su nuevo contexto. Lo mismo se aplica a este patrón.
Joel Harmon
3

Solía ​​introducir un "bus de eventos" de algún tipo, y en años posteriores comencé a confiar cada vez más en el Modelo de Objetos del Documento para comunicar eventos para el código de la interfaz de usuario.

En un navegador, el DOM es la única dependencia que siempre está presente, incluso durante la carga de la página. La clave es utilizar eventos personalizados en JavaScript y confiar en el burbujeo de eventos para comunicar esos eventos.

Antes de que la gente empiece a gritar sobre "esperar a que el documento esté listo" antes de adjuntar suscriptores, la document.documentElementpropiedad hace referencia al <html>elemento desde el momento en que JavaScript comienza a ejecutarse, sin importar dónde se importa el script o el orden en que aparece en su marcado.

Aquí es donde puedes comenzar a escuchar eventos.

Es muy común tener un componente (o widget) de JavaScript en vivo dentro de una determinada etiqueta HTML en la página. El elemento "raíz" del componente es donde puede activar sus eventos burbujeantes. Los suscriptores del <html>elemento recibirán estas notificaciones al igual que cualquier otro evento generado por el usuario.

Solo un ejemplo de código de placa de caldera:

(function (window, document, html) {
    html.addEventListener("custom-event-1", function (event) {
        // ...
    });
    html.addEventListener("custom-event-2", function (event) {
        // ...
    });

    function someOperation() {
        var customData = { ... };
        var event = new CustomEvent("custom-event-3", { detail : customData });

        event.dispatchEvent(componentRootElement);
    }
})(this, this.document, this.document.documentElement);

Entonces el patrón se convierte en:

  1. Usar eventos personalizados
  2. Suscríbase a estos eventos en la document.documentElementpropiedad (no es necesario esperar a que el documento esté listo)
  3. Publique eventos en un elemento raíz para su componente, o el document.documentElement.

Esto debería funcionar para bases de código funcionales y orientadas a objetos.

Greg Burghardt
fuente
1

Utilizo este mismo estilo con mi desarrollo de videojuegos con Unity 3D. Creo componentes como Salud, Entrada, Estadísticas, Sonido, etc. y los agrego a un Objeto de juego para construir lo que es ese objeto de juego. Unity ya tiene mecanismos para agregar componentes a los objetos del juego. Sin embargo, lo que encontré fue que casi todo el mundo consultaba componentes o hacía referencia directa a componentes dentro de otros componentes (incluso si usaban interfaces, todavía estaba más acoplado, gracias, preferí). Quería que los componentes se pudieran crear de forma aislada con cero dependencias de cualquier otro componente. Así que hice que los componentes dispararan eventos cuando los datos cambiaban (específicos del componente) y declaraba métodos para cambiar básicamente los datos. Luego, el objeto del juego creé una clase y pegué todos los eventos componentes a otros métodos componentes.

Lo que me gusta de esto es que para ver todas las interacciones de los componentes de un objeto de juego, puedo mirar esta clase 1. Parece que tu clase de contacto se parece mucho a mis clases de objetos de juego (nombro objetos del juego al objeto que deberían ser como MainPlayer, Orc, etc.).

Estas clases son una especie de clases de administrador. Ellos mismos realmente no tienen nada excepto instancias de componentes y el código para conectarlos. No estoy seguro de por qué creas métodos aquí que solo llaman a otros métodos componentes cuando puedes conectarlos directamente. El objetivo de esta clase es realmente organizar el evento enganchado.

Como nota al margen para mis registros de eventos, agregué una devolución de llamada de filtro y una devolución de llamada de argumentos. Cuando se activa el evento (hice mi propia clase de evento personalizado), llamará a la devolución de llamada de filtro si existe y si devuelve verdadero, pasará a la devolución de llamada de arg. El objetivo de la devolución de llamada del filtro era dar flexibilidad. Un evento puede desencadenarse por varias razones, pero solo quiero llamar a mi evento conectado si una verificación es verdadera. Un ejemplo podría ser un componente de entrada que tiene un evento OnKeyHit. Si tengo un componente de Movimiento que tiene métodos como MoveForward () MoveBackward (), etc., puedo conectar OnKeyHit + = MoveForward pero obviamente no me gustaría avanzar con ninguna tecla presionada. Solo me gustaría hacerlo si la clave fuera 'w'. Dado que OnKeyHit está completando argumentos para pasar y uno de esos es la clave que fue golpeada,

Para mí, la suscripción para una clase específica de administrador de objetos de juego se parece más a:

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' });

Como los componentes se pueden desarrollar de forma aislada, varios programadores podrían haberlos codificado. Con el ejemplo anterior, el codificador de entrada le dio al objeto de argumento una variable llamada Clave. Sin embargo, el desarrollador del componente Movimiento puede no haber usado Clave (si fuera necesario para mirar los argumentos, en este caso probablemente no, pero en otros usan los valores de argumento pasados). Para eliminar este requisito de comunicación, la devolución de llamada args actúa como un mapeo de los argumentos entre componentes. Entonces, la persona que crea esta clase de administrador de objetos de juego es la que necesita saber los nombres de las variables arg entre los 2 clientes cuando los conectan y realizan el mapeo en ese punto. Este método se llama después de la función de filtro.

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' }, (args) => { args.keyPressed = args.Key });

Entonces, en la situación anterior, la persona de entrada nombró una variable dentro del objeto args 'Key' pero el Movimiento lo llamó 'keyPressed'. Esto ayuda a un mayor aislamiento entre los componentes a medida que se desarrollan y lo pone en el implementador de la clase de administrador para que se conecte correctamente.

usuario441521
fuente
0

Por lo que vale, estoy haciendo algo como parte de un proyecto de back-end y he tomado un enfoque similar:

  • Mi sistema no involucra widgets (componentes web) sino 'adaptadores' abstractos, implementaciones concretas que manejan diferentes protocolos.
  • Un protocolo se modela como un conjunto de posibles 'conversaciones'. El adaptador de protocolo desencadena estas conversaciones en función de un evento entrante.
  • Hay un bus de eventos que es básicamente un sujeto Rx.
  • El bus de eventos se suscribe a la salida de todos los adaptadores, y todos los adaptadores se suscriben a la salida del bus de eventos.
  • El 'adaptador' se modela como la secuencia agregada de todas sus 'conversaciones'. Una conversación es un flujo suscrito a la salida del bus de eventos, que genera mensajes al bus de eventos, impulsado por una máquina de estado.

Cómo manejé sus desafíos de construcción / cableado:

  • Un protocolo (implementado por el adaptador) define los criterios de inicio de conversación como filtros sobre el flujo de entrada al que está suscrito. En C #, estas son consultas LINQ sobre secuencias. En ReactJS estos serían operadores .Where o .Filter.
  • Una conversación decide qué es un mensaje relevante utilizando sus propios filtros.
  • En general, cualquier cosa suscrita al bus es un flujo, y el bus está suscrito a esos flujos.

La analogía con tu barra de herramientas:

  • La clase de barra de herramientas es un .Mapa de una entrada observable (el bus), que es observable de los eventos de la barra de herramientas, a los que está suscrito el bus
  • Un observable de barras de herramientas (si tiene varias barras de subherramientas) significa que puede tener múltiples observables, por lo que su barra de herramientas es observable de observables. Estos serían RxJs. Combinados en una sola salida al bus.

Problemas que puede enfrentar:

  • Asegurarse de que los eventos no sean cíclicos y suspender el proceso.
  • Concurrencia (no sé si esto es relevante para WebComponents): para operaciones asíncronas u operaciones que pueden estar ejecutándose durante mucho tiempo, su controlador de eventos puede bloquear el hilo observable si no se ejecuta como una tarea en segundo plano. Los planificadores RxJS pueden abordar esto (de manera predeterminada, puede .ObserveOn un planificador predeterminado para todas las suscripciones de bus, por ejemplo)
  • Escenarios más complejos que no pueden modelarse sin alguna noción de conversación (por ejemplo: manejar un evento enviando un mensaje y esperando una respuesta, que en sí misma es un evento). En este caso, una máquina de estado es útil para especificar dinámicamente qué eventos desea manejar (conversación en mi modelo). Hago esto teniendo la secuencia de conversación .filtersegún el estado (en realidad, la implementación es más funcional: la conversación es un mapa plano de observables de un observable de eventos de cambio de estado).

Entonces, en resumen, puede ver todo su dominio del problema como observables, o 'funcionalmente' / 'declarativamente' y considerar sus componentes web como flujos de eventos, como observables, derivados del bus (un observable), al cual el bus (un observador) también está suscrito. La creación de instancias de observables (por ejemplo, una nueva barra de herramientas) es declarativa, ya que todo el proceso puede verse como un observable de observables .map'd desde el flujo de entrada.

Centinela
fuente