Event Programming – How to Ease the Maintenance of Event-Driven Code

event-programmingmaintenance

When using an event based component I often feel some pain at maintenance phase.

Since the executed code is all split around it can be quite hard to figure what will be all the code part that will be involved at runtime.

This can lead to subtle and hard to debug problems when someone adds some new event handlers.

Edit from comments:
Even with some good practices on-board, like having an application wide event bus and handlers delegating business to other part of the app, there is a moment when the code starts to become hard to read because there is a lot of registered handlers from many different places (especially true when there is a bus).

Then sequence diagram starts to look over complex, time spend to figure out what is happening is increasing and debugging session becomes messy (breakpoint on the handlers manager while iterating on handlers, especially joyful with async handler and some filtering on top of it).

//////////////
Example

I have a service that is retrieving some data on the server. On the client we have a basic component that is calling this service using a callback. To provide extension point to the users of the component and to avoid coupling between different components, we are firing some events: one before the query is sent, one when the answer is coming back and another one in case of a failure.
We have a basic set of handlers that are pre-registered which provide the default behavior of the component.

Now users of the component (and we are user of the component too) can add some handlers to perform some change on the behavior (modify the query, logs, data analysis, data filtering, data massaging, UI fancy animation, chain multiple sequential queries, whatever).
So some handlers must be executed before/after some others and they are registered from a lots of different entry point in the application.

After a while, it can happens that a dozen or more handlers are registered, and working with that can be tedious and hazardous.

This design emerged because using inheritance was starting to be a complete mess. The event system is used at a kind of composition where you don't know yet what will be your composites.

End of example
//////////////

So I'm wondering how other people are tackling this kind of code. Both when writing and reading it.

Do you have any methods or tools that let you write and maintain such code without to much pain ?

Best Answer

I've found that processing events using a stack of internal events (more specifically, a LIFO queue with arbitrary removal) greatly simplifies event-driven programming. It allows you to split the processing of an "external event" into several smaller "internal events", with well-defined state in between. For more information, see my answer to this question.

Here I present a simple example which is solved by this pattern.

Suppose you are using object A to perform some service, and you give it a callback to inform you when it's done. However, A is such that after calling your callback, it may need to do some more work. A hazard arises when, within that callback, you decide that you don't need A any more, and you destroy it some way or another. But you're being called from A - if A, after your callback returns, cannot safely figure out that it was destroyed, a crash could result when it attempts to perform the remaining work.

NOTE: It's true that you could do the "destruction" in some other way, like decrementing a refcount, but that just leads to intermediate states, and extra code and bugs from handling these; better for A to just stop working entirely after you don't need it anymore other than continue in some intermediate state.

In my pattern, A would simply schedule the further work it needs to do by pushing an internal event (job) into the event loop's LIFO queue, then proceed to call the callback, and return to event loop immediately. This piece of code it no longer a hazard, since A just returns. Now, if the callback doesn't destroy A, the pushed job will eventually be executed by the event loop to do its extra work (after the callback is done, and all its pushed jobs, recursively). On the other hand, if the callback does destroy A, A's destructor or deinit function can remove the pushed job from the event stack, implicitly preventing execution of the pushed job.