¿Cómo facilitar el mantenimiento del código impulsado por eventos?

14

Cuando uso un componente basado en eventos, a menudo siento algo de dolor en la fase de mantenimiento.

Dado que el código ejecutado está dividido, puede ser bastante difícil determinar cuál será toda la parte del código que estará involucrada en el tiempo de ejecución.

Esto puede llevar a problemas sutiles y difíciles de depurar cuando alguien agrega algunos nuevos controladores de eventos.

Editar desde comentarios: Incluso con algunas buenas prácticas a bordo, como tener un autobús de eventos y controladores para todas las aplicaciones que delegan negocios a otra parte de la aplicación, hay un momento en que el código comienza a ser difícil de leer porque hay muchos controladores registrados de muchos Diferentes lugares (especialmente cierto cuando hay un autobús).

Luego, el diagrama de secuencia comienza a parecer complejo, el tiempo que se dedica a descubrir qué está sucediendo está aumentando y la sesión de depuración se vuelve desordenada (punto de interrupción en el administrador de manejadores mientras se repite en los manejadores, especialmente alegre con el manejador asíncrono y algo de filtrado en la parte superior ).

//////////////
Ejemplo

Tengo un servicio que está recuperando algunos datos en el servidor. En el cliente tenemos un componente básico que está llamando a este servicio mediante una devolución de llamada. Para proporcionar un punto de extensión a los usuarios del componente y para evitar el acoplamiento entre los diferentes componentes, estamos activando algunos eventos: uno antes del envío de la consulta, uno cuando la respuesta regresa y otro en caso de falla. Tenemos un conjunto básico de manejadores que están pre-registrados que proporcionan el comportamiento predeterminado del componente.

Ahora los usuarios del componente (y nosotros también somos usuarios del componente) pueden agregar algunos manejadores para realizar algún cambio en el comportamiento (modificar la consulta, los registros, el análisis de datos, el filtrado de datos, el masaje de datos, la animación UI de fantasía, la cadena consultas secuenciales múltiples, lo que sea). Por lo tanto, algunos manejadores deben ejecutarse antes / después de otros y se registran desde muchos puntos de entrada diferentes en la aplicación.

Después de un tiempo, puede suceder que una docena o más de manejadores estén registrados, y trabajar con ellos puede ser tedioso y peligroso.

Este diseño surgió porque el uso de la herencia estaba empezando a ser un completo desastre. El sistema de eventos se utiliza en una clase de composición en la que aún no se sabe cuáles serán sus compuestos.

Fin del ejemplo
//////////////

Así que me pregunto cómo otras personas están abordando este tipo de código. Tanto al escribirlo como a leerlo.

¿Tiene algún método o herramienta que le permita escribir y mantener dicho código sin mucho dolor?

    
pregunta Guillaume 16.07.2012 - 15:14

7 respuestas

6

Descubrí que el procesamiento de eventos mediante una pila de eventos internos (más específicamente, una cola LIFO con eliminación arbitraria) simplifica enormemente la programación dirigida por eventos. Le permite dividir el procesamiento de un "evento externo" en varios "eventos internos" más pequeños, con un estado bien definido en el medio. Para obtener más información, consulte mi respuesta a esta pregunta .

Aquí presento un ejemplo simple que se resuelve con este patrón.

Supongamos que está utilizando el objeto A para realizar algún servicio, y le da una devolución de llamada para informarle cuando está listo. Sin embargo, A es tal que después de llamar a su devolución de llamada, es posible que tenga que hacer un poco más de trabajo. Surge un peligro cuando, dentro de esa devolución de llamada, decides que ya no necesitas A, y lo destruyes de una forma u otra. Pero se le está llamando desde A: si A, después de que su devolución de llamada regresa, no puede descubrir con seguridad que fue destruido, podría producirse una falla cuando intente realizar el trabajo restante.

NOTA: Es cierto que puedes hacer la "destrucción" de alguna otra manera, como disminuir un refcount, pero eso solo lleva a estados intermedios, y el código y los errores adicionales de su manejo; es mejor que A deje de trabajar por completo después de que ya no lo necesite, aparte de continuar en algún estado intermedio.

En mi patrón, A simplemente programará el trabajo adicional que debe realizar al insertar un evento interno (trabajo) en la cola LIFO del bucle de eventos, luego proceder a llamar a la devolución de llamada, y volver al bucle de eventos inmediatamente . Esta pieza de código ya no es un peligro, ya que A simplemente regresa. Ahora, si la devolución de llamada no destruye A, la tarea empujada finalmente será ejecutada por el bucle de eventos para realizar su trabajo adicional (después de que se realiza la devolución de llamada, y todas sus tareas empujadas, recursivamente). Por otro lado, si la devolución de llamada destruye A, el destructor o la función deinit pueden eliminar el trabajo enviado de la pila de eventos, impidiendo implícitamente la ejecución del trabajo enviado.

    
respondido por el Ambroz Bizjak 29.07.2012 - 11:16
7

Creo que un registro adecuado puede ayudar bastante. Asegúrese de que todos los eventos lanzados / manejados se registren en algún lugar (puede usar marcos de registro para esto). Cuando realiza la depuración, puede consultar los registros para ver el exacto orden de ejecución de su código cuando ocurrió el error. A menudo, esto realmente ayudará a reducir la causa del problema.

    
respondido por el Oleksi 16.07.2012 - 15:55
3
  

Así que me pregunto cómo otras personas están abordando este tipo de código. Tanto al escribirlo como a leerlo.

El modelo de programación dirigida por eventos simplifica la codificación en cierta medida. ¡Probablemente haya evolucionado como un reemplazo de las grandes declaraciones Select (o estuches) utilizadas en lenguajes antiguos y ganó popularidad en los primeros entornos de desarrollo visual como VB 3 (no me cites en el historial, no lo comprobé)!

El modelo se convierte en una molestia si la secuencia de eventos es importante y cuando una acción empresarial se divide en muchos eventos. Este estilo de proceso viola los beneficios de este enfoque. A toda costa, trate de encapsular el código de acción en el evento correspondiente y no genere eventos dentro de los eventos. Eso se vuelve mucho peor que los Spaghetti resultantes del GoTo.

A veces, los desarrolladores están ansiosos por proporcionar una funcionalidad GUI que requiera tal dependencia de eventos, pero en realidad no existe una alternativa real que sea significativamente más simple.

La línea de fondo aquí es que la técnica no es mala si se usa sabiamente.

    
respondido por el NoChance 16.07.2012 - 17:43
3

Quería actualizar esta respuesta ya que obtuve algunos momentos de Eureka desde que "aplané" y "aplasté" los flujos de control y formulé algunas ideas nuevas sobre el tema.

Efectos secundarios complejos frente a flujos de control complejos

Lo que he descubierto es que mi cerebro puede tolerar efectos secundarios complejos o flujos de control complejos, como los que se encuentran normalmente en el manejo de eventos, pero no el combo de ambos.

Puedo razonar fácilmente sobre el código que causa 4 efectos secundarios diferentes si se aplican con un flujo de control muy simple, como el de un bucle secuencial for . Mi cerebro puede tolerar un bucle secuencial que redimensiona y reposiciona los elementos, los anima, los vuelve a dibujar y actualiza algún tipo de estado auxiliar. Eso es bastante fácil de comprender.

Del mismo modo, puedo comprender un flujo de control complejo como podría suceder con eventos en cascada o atravesar una estructura de datos compleja similar a un gráfico si hay un efecto secundario muy simple en el proceso en el que el orden no importa en absoluto. como elementos de marcado que se procesarán de manera diferida en un simple bucle secuencial.

Donde me pierdo, me siento confundido y abrumado es cuando tienes flujos de control complejos que causan efectos secundarios complejos. En ese caso, el flujo de control complejo hace que sea difícil predecir de antemano dónde va a terminar, mientras que los efectos secundarios complejos hacen que sea difícil predecir qué sucederá como resultado y en qué orden. Así que es la combinación de estas dos cosas lo que lo hace tan incómodo donde, incluso si el código funciona perfectamente bien en este momento, es tan aterrador cambiarlo sin temor a causar efectos secundarios no deseados.

Los flujos de control complejos tienden a hacer que sea difícil razonar cuándo y dónde sucederán las cosas. Eso solo se vuelve realmente inductivo si estos flujos de control complejos desencadenan una combinación compleja de efectos secundarios en los que es importante entender cuándo y dónde suceden las cosas, como los efectos secundarios que tienen algún tipo de dependencia de orden en la que una cosa debería suceder antes de la siguiente.

Simplifique el flujo de control o los efectos secundarios

Entonces, ¿qué haces cuando te encuentras con el escenario anterior que es tan difícil de comprender? La estrategia es simplificar el flujo de control o los efectos secundarios.

Una estrategia ampliamente aplicable para simplificar los efectos secundarios es favorecer el procesamiento diferido. Usando un evento de cambio de tamaño de GUI como ejemplo, la tentación normal podría ser volver a aplicar el diseño de la GUI, reposicionar y cambiar el tamaño de los widgets secundarios, desencadenar otra cascada de aplicaciones de diseño y redimensionar y reposicionar la jerarquía, junto con volver a pintar los controles, posiblemente activando algunos eventos únicos para widgets que tienen un comportamiento de cambio de tamaño personalizado que desencadena más eventos que llevan a saber quién sabe dónde, etc. En lugar de intentar hacer todo esto de una sola vez o enviando spam a la cola de eventos, una posible solución es descender por la jerarquía de widgets y marca qué widgets necesitan que se actualicen sus diseños. Luego, en un paso posterior diferido que tiene un flujo de control secuencial directo, vuelva a aplicar todos los diseños para los widgets que lo necesiten. A continuación, puede marcar lo que los widgets deben ser repintados. De nuevo, en un paso diferido secuencial con un flujo de control directo, vuelva a pintar los widgets marcados como que necesitan ser redibujados.

Esto tiene el efecto de simplificar el flujo de control y los efectos secundarios, ya que el flujo de control se simplifica, ya que no es un evento recursivo en cascada durante el recorrido del gráfico. En su lugar, las cascadas se producen en el bucle secuencial diferido que luego podría manejarse en otro bucle secuencial diferido. Los efectos secundarios se vuelven simples cuando cuenta, ya que, durante los flujos de control más complejos, como los de los gráficos, todo lo que estamos haciendo es simplemente marcar lo que necesita ser procesado por los bucles secuenciales diferidos que activan los efectos secundarios más complejos.

Esto conlleva cierta sobrecarga de procesamiento, pero podría abrir las puertas para, por ejemplo, realizar estos pases diferidos en paralelo, lo que posiblemente le permita obtener una solución aún más eficiente que la que comenzó si el rendimiento es una preocupación. En general, el rendimiento no debería ser una gran preocupación en la mayoría de los casos. Lo más importante es que, si bien esto puede parecer una diferencia discutible, me parece mucho más fácil razonar. Hace que sea mucho más fácil predecir qué está sucediendo y cuándo, y no puedo sobreestimar el valor que puede tener el poder comprender más fácilmente lo que está pasando.

    
respondido por el user204677 15.01.2016 - 04:16
2

Lo que me ha funcionado es hacer que cada evento se mantenga por sí solo, sin hacer referencia a otros eventos. Si llegan de forma asíncrona, no tienes una secuencia, por lo que tratas de averiguar qué sucede en qué orden no tiene sentido, además de que es imposible.

Lo que sí haces es un conjunto de estructuras de datos que se leen, modifican, crean y eliminan mediante una docena de subprocesos sin ningún orden en particular. Tienes que hacer una programación multihilo extremadamente adecuada, lo cual no es fácil. También tiene que pensar en varios subprocesos, como en "Con este evento, voy a ver los datos que tengo en un instante en particular, sin importar lo que fue un microsegundo antes, sin tener en cuenta lo que acaba de cambiar. , y sin tener en cuenta lo que van a hacer los 100 hilos que me esperan para liberar el bloqueo. Luego, haré mis cambios en función de esto y de lo que vea. Luego habrá terminado. "

Una cosa que estoy haciendo es buscar una colección en particular y asegurarme de que tanto la referencia como la colección en sí (si no es segura para subprocesos) estén correctamente bloqueadas y sincronizadas con otros datos. A medida que se agregan más eventos, esta tarea crece. Pero si estuviera rastreando las relaciones entre los eventos, esa tarea crecería mucho más rápido. Además, a veces gran parte del bloqueo se puede aislar en su propio método, lo que hace que el código sea más sencillo.

Tratar cada subproceso como una entidad completamente independiente es difícil (debido al multiproceso de núcleo duro) pero es factible. "Escalable" puede ser la palabra que estoy buscando. El doble de eventos requiere solo el doble de trabajo y tal vez solo 1.5 veces más. Tratar de coordinar eventos más asíncronos lo enterrará rápidamente.

    
respondido por el RalphChapin 16.07.2012 - 18:15
2

Parece que está buscando State Máquinas y amp; Actividades dirigidas por eventos .

Sin embargo, es posible que también desee consultar Flujo de trabajo de marcado de la máquina de estado Muestra .

Aquí tiene una breve descripción de la implementación de la máquina de estado. Un flujo de trabajo de máquina de estados consiste en estados. Cada estado se compone de uno o más controladores de eventos. Cada controlador de eventos debe contener un retraso o una actividad de IEvent como la primera actividad. Cada controlador de eventos también puede contener una actividad SetStateActivity que se utiliza para la transición de un estado a otro.

Cada flujo de trabajo de la máquina de estado tiene dos propiedades: InitialStateName y CompletedStateName. Cuando se crea una instancia del flujo de trabajo de la máquina de estado, se coloca en la propiedad InitialStateName. Cuando la máquina de estado llega a la propiedad CompletedStateName, finaliza la ejecución.

    
respondido por el EL Yusubov 16.07.2012 - 15:55
1

El código controlado por evento no es el problema real. De hecho, no tengo ningún problema en seguir la lógica incluso en el código controlado, donde las devoluciones de llamada se definen explícitamente o se utilizan devoluciones de llamada en línea. Por ejemplo, devoluciones de llamada de estilo de generador en Tornado son muy fáciles de seguir.

Lo que es realmente difícil de depurar son las llamadas a funciones generadas dinámicamente. El patrón (anti?) Que llamaría la fábrica de devolución de llamada del infierno. Sin embargo, este tipo de fábricas de funciones son igualmente difíciles de depurar en el flujo tradicional.

    
respondido por el vartec 16.07.2012 - 16:08

Lea otras preguntas en las etiquetas