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

16

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 imaginar cuál será la parte del código que estará involucrada en el tiempo de ejecución.

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

Editar a partir de los comentarios: incluso con algunas buenas prácticas a bordo, como tener un bus de eventos para toda la aplicación y controladores 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 manejadores registrados de muchos lugares diferentes (especialmente cierto cuando hay un autobús).

Luego, el diagrama de secuencia comienza a verse complejo, el tiempo dedicado a darse cuenta de lo que está sucediendo aumenta y la sesión de depuración se vuelve desordenada (punto de interrupción en el administrador de controladores mientras se itera en los controladores, especialmente alegre con el controlador asíncrono y algunos filtros encima).

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

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

Ahora los usuarios del componente (y nosotros también somos usuarios del componente) pueden agregar algunos controladores 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 sofisticada de la interfaz de usuario, la cadena de consultas secuenciales múltiples , lo que sea). Por lo tanto, algunos controladores deben ejecutarse antes / después de otros y deben registrarse desde muchos puntos de entrada diferentes en la aplicación.

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

Este diseño surgió porque el uso de la herencia comenzaba a ser un completo desastre. El sistema de eventos se utiliza en una especie de composición en la que aún no sabes cuáles serán tus compuestos.

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

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

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

Guillaume
fuente
¿Quieres decir, además de refactorizar la lógica de los controladores de eventos?
Telastyn
Documente lo que sucede.
@Telastyn, no estoy seguro de entender completamente a qué te refieres con 'además de refactorizar la lógica de los controladores de eventos'.
Guillaume
@Thorbjoern: mira mi actualización.
Guillaume
3
¿Parece que no estás usando la herramienta correcta para el trabajo? Quiero decir, si el orden importa, entonces no deberías usar eventos simples para esas partes de la aplicación en primer lugar. Básicamente, si hay limitaciones en el orden de los eventos en un sistema de bus típico, ya no es un sistema de bus típico, pero todavía lo está utilizando como tal. Lo que significa que debe abordar ese problema agregando un poco de lógica adicional para manejar el pedido. Al menos, si entiendo correctamente su problema ..
stijn

Respuestas:

7

Descubrí que procesar eventos usando una pila de eventos internos (más específicamente, una cola LIFO con eliminación arbitraria) simplifica enormemente la programación basada en 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, vea 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 devuelve una llamada para informarle cuando haya terminado. Sin embargo, A es tal que después de llamar a su devolución de llamada, es posible que deba hacer un poco más de trabajo. Un peligro surge 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 llama desde A: si A, después de que regresa su devolución de llamada, no puede determinar con seguridad que fue destruido, podría producirse un bloqueo cuando intente realizar el trabajo restante.

NOTA: Es cierto que podría hacer la "destrucción" de alguna otra manera, como decrementar un recuento, pero eso solo conduce a estados intermedios, y a códigos y errores adicionales al manejarlos; es mejor que A simplemente deje de funcionar por completo después de que ya no lo necesite más que continuar en algún estado intermedio.

En mi patrón, A simplemente programaría el trabajo adicional que necesita hacer al insertar un evento interno (trabajo) en la cola LIFO del bucle de eventos, luego procedería a llamar a la devolución de llamada y volvería al bucle de eventos inmediatamente . Este código ya no es un peligro, ya que A simplemente regresa. Ahora, si la devolución de llamada no destruye A, el bucle de eventos eventualmente ejecutará el trabajo empujado para realizar su trabajo adicional (después de que se realice la devolución de llamada y todos sus trabajos empujados, de forma recursiva). Por otro lado, si la devolución de llamada destruye A, el destructor o la función deinit de A puede eliminar el trabajo enviado de la pila de eventos, impidiendo implícitamente la ejecución del trabajo enviado.

Ambroz Bizjak
fuente
7

Creo que un registro adecuado puede ayudar bastante. Asegúrese de que cada evento lanzado / manejado se registre en algún lugar (puede usar marcos de registro para esto). Cuando esté depurando, puede consultar los registros para ver el orden exacto de ejecución de su código cuando ocurrió el error. A menudo, esto realmente ayudará a reducir la causa del problema.

Oleksi
fuente
Sí, ese es un consejo útil. La cantidad de registros producidos puede ser aterradora (recuerdo haber tenido dificultades para encontrar la causa de un ciclo en el caso de que se procese una forma muy compleja).
Guillaume
Quizás una solución es registrar la información interesante en un archivo de registro separado. Además, puede asignar un UUID a cada evento, para que pueda rastrear cada evento más fácilmente. También puede escribir alguna herramienta de informes que le permita extraer información específica de los archivos de registro. (O, alternativamente, use interruptores en el código para registrar información diferente en diferentes archivos de registro).
Giorgio
3

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

El modelo de programación dirigida por eventos simplifica la codificación hasta cierto punto. Probablemente ha evolucionado como un reemplazo de las grandes declaraciones Select (o case) utilizadas en idiomas más antiguos y ganó popularidad en los primeros entornos de desarrollo visual como VB 3 (¡No me cite en el historial, no lo verifiqué!)

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

A veces, los desarrolladores están ansiosos por proporcionar la funcionalidad de GUI que requiere dicha dependencia de eventos, pero realmente no existe una alternativa real que sea significativamente más simple.

La conclusión aquí es que la técnica no es mala si se usa sabiamente.

Ninguna posibilidad
fuente
¿Hay algún diseño alternativo para evitar el 'peor que el Spaghetti'?
Guillaume
No estoy seguro de que haya una manera fácil sin simplificar el comportamiento esperado de la GUI, pero si enumera todas sus interacciones de usuario con una ventana / página y para cada una determina exactamente qué hará su programa, podría comenzar a agrupar el código en un solo lugar y dirigir varios eventos a un grupo de subs / métodos que son responsables del procesamiento real de la solicitud. También puede ayudar separar el código que solo sirve la GUI del código que realiza el procesamiento de back-end.
NoChance
La necesidad de publicar este mensaje vino de una refactorización cuando me mudé de un infierno de herencia a algo basado en un evento. En algún momento es realmente mejor, pero en otro es bastante malo ... Como ya he tenido algunos problemas con 'eventos que se han vuelto locos' en alguna GUI, me pregunto qué se puede hacer para mejorar el mantenimiento de tales código de evento en rodajas
Guillaume
La programación dirigida por eventos es mucho más antigua que VB; estaba presente en la GUI de SunTools y, antes de eso, me parece recordar que estaba integrado en el lenguaje Simula.
Kevin Cline
@Guillaume, supongo que estás sobre ingeniería del servicio. De hecho, mi descripción anterior se basó principalmente en eventos de GUI. ¿Realmente necesita este tipo de procesamiento?
NoChance
3

Quería actualizar esta respuesta ya que obtuve algunos momentos de eureka después de los flujos de control de "aplanamiento" y "aplanamiento" y he formulado algunas ideas nuevas sobre el tema.

Efectos secundarios complejos versus flujos de control complejos

Lo que descubrí es que mi cerebro puede tolerar efectos secundarios complejos o flujos de control complejos tipo gráfico, como suele encontrar con el manejo de eventos, pero no la combinación 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 forbucle secuencial . Mi cerebro puede tolerar un bucle secuencial que cambia el tamaño y la posición de los elementos, los anima, los vuelve a dibujar y actualiza algún tipo de estado auxiliar. Eso es bastante fácil de comprender.

También puedo comprender un flujo de control complejo como el que se obtiene con los eventos en cascada o atravesar una estructura de datos compleja similar a un gráfico si solo hay un efecto secundario muy simple en el proceso donde el orden no importa el más mínimo, como marcar elementos para ser procesado de manera diferida en un simple bucle secuencial.

Cuando me pierdo, me confundo y me abrumo 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 exactamente qué sucederá como resultado y en qué orden. Entonces, 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 dificultar el razonamiento sobre cuándo / dónde sucederán las cosas. Eso solo provoca dolor de cabeza si estos flujos de control complejos desencadenan una combinación compleja de efectos secundarios en los que es importante comprender cuándo / dónde suceden las cosas, como los efectos secundarios que tienen algún tipo de dependencia del orden donde 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 GUI, reposicionar y cambiar el tamaño de los widgets secundarios, desencadenar otra cascada de aplicaciones de diseño y cambiar el tamaño y el reposicionamiento de 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 desencadenan más eventos que conducen a quién sabe dónde, etc. y marque qué widgets necesitan que se actualicen sus diseños. Luego, en un pase diferido posterior que tiene un flujo de control secuencial directo, vuelva a aplicar todos los diseños para los widgets que lo necesiten. Luego, puede marcar qué widgets deben volver a pintarse. Nuevamente en un pase diferido secuencial con un flujo de control directo, repinte 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 está en cascada los eventos recursivos durante el recorrido del gráfico. En cambio, 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 donde cuenta, ya que, durante los flujos de control tipo gráfico más complejos, todo lo que estamos haciendo es simplemente marcar lo que necesita ser procesado por los bucles secuenciales diferidos que desencadenan los efectos secundarios más complejos.

Esto viene con algunos gastos generales de procesamiento, pero luego puede abrir puertas para, por ejemplo, hacer estos pases diferidos en paralelo, lo que le permite obtener una solución aún más eficiente de lo 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, si bien esto puede parecer una gran diferencia, me ha parecido mucho más fácil razonar. Hace que sea mucho más fácil predecir lo que está sucediendo y cuándo, y no puedo sobreestimar el valor que puede tener para poder comprender más fácilmente lo que está sucediendo.


fuente
2

Lo que funcionó para mí es hacer que cada evento sea independiente, sin referencia a otros eventos. Si vienen de forma asincrónica, no tienes una secuencia, por lo que tratar de descubrir qué sucede en qué orden no tiene sentido, además de ser imposible.

Lo que sí termina es un montón de estructuras de datos que se leen y modifican y crean y eliminan por una docena de hilos sin ningún orden en particular. Tienes que hacer una programación multiproceso extremadamente adecuada, lo que no es fácil. También debe pensar en varios subprocesos, como en "Con este evento, voy a ver los datos que tengo en un instante en particular, sin tener en cuenta 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 esperan que libere el candado. Luego haré mis cambios en función de esto y de lo que veo. Luego termino ".

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

Tratar cada subproceso como una entidad completamente independiente es difícil (debido al subprocesamiento múltiple de núcleo duro) pero 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. Intentar coordinar más eventos asincrónicos lo enterrará rápidamente.

RalphChapin
fuente
He actualizado mi pregunta. Tienes toda la razón sobre la secuencia y las cosas asíncronas. ¿Cómo manejaría un caso en el que el evento X debe ejecutarse después de que se hayan procesado todos los demás eventos? Parece que tengo que crear un administrador de controladores más complejo, pero también quiero evitar exponer demasiada complejidad a los usuarios.
Guillaume
En su conjunto de datos, tenga un interruptor y algunos campos que se configuran cuando se lee el evento X. Cuando se procesan todos los demás, verifica el interruptor y sabe que tiene que manejar X y que tiene los datos. El interruptor y los datos deberían estar solos, en realidad. Cuando esté configurado, debe pensar: "Tengo que hacer este trabajo", no "Tengo que entregar X". Siguiente problema: ¿cómo sabes que se hacen los eventos? ¿Y si obtienes 2 o más eventos X? En el peor de los casos, puede ejecutar un hilo de mantenimiento en bucle que verifica la situación y puede actuar por iniciativa propia. (¿No hay entrada durante 3 segundos? ¿Interruptor X configurado? Luego ejecute el código de apagado.
RalphChapin
2

Parece que está buscando máquinas estatales y actividades dirigidas por eventos .

Sin embargo, es posible que también desee ver el ejemplo de flujo de trabajo de marcado de máquina de estado .

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

Cada flujo de trabajo de la máquina de estados 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 alcanza la propiedad CompletedStateName, finaliza la ejecución.

Yusubov
fuente
2
Si bien esto puede responder teóricamente la pregunta, sería preferible incluir aquí las partes esenciales de la respuesta y proporcionar el enlace para referencia.
Thomas Owens
Pero si tiene docenas de controladores conectados a cada estado, no estará tan bien cuando intente comprender lo que está sucediendo ... Y es posible que no todos los componentes basados ​​en eventos se describan como una máquina de estados.
Guillaume
1

El código controlado por eventos no es el verdadero problema. De hecho, no tengo ningún problema para seguir la lógica incluso en el código controlado, donde la devolución de llamada se define explícitamente o se utilizan devoluciones de llamada en línea. Por ejemplo , las devoluciones de llamada de estilo 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?) Al que llamaría la Fábrica de llamadas desde el infierno. Sin embargo, este tipo de fábricas de funciones son igualmente difíciles de depurar en el flujo tradicional.

vartec
fuente