16

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 ?

  • You mean, besides refactoring out logic from event handlers? – Telastyn Jul 16 '12 at 13:18
  • Document what goes on. – user1249 Jul 16 '12 at 13:29
  • @Telastyn, I'm not sure to fully understand what do you mean by 'besides refactoring out logic from event handlers'. – Guillaume Jul 16 '12 at 13:33
  • @Thorbjoern: see my update. – Guillaume Jul 16 '12 at 13:41
  • 3
    It sounds like you are not using the correct tool for the job? I mean, if the order matters, then you should not be using plain events for those parts of the application in the first place. Basically if there are limitations on the order of events in a typical bus system, it's not a typical bus system anymore, but you are still using it as such. Which means you need to address that issue by adding some extra logic to handle the order. At least, if I understand your problem correctly.. – stijn Jul 16 '12 at 14:11
7

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.

7

I think proper logging can help quite a big. Make sure that every event thrown/handled is logged somewhere (you can used logging frameworks for this). When you're debugging, you can consult the logs to see the exact order of execution of your code when the bug occurred. Often this will really help narrow down cause of the problem.

  • Yes, that's an helpful advise. The amount of produced logs can then be frightening (I can remember having some hard time to find the cause of a cycle in the event processing of a very complex form.) – Guillaume Jul 16 '12 at 14:35
  • Maybe one solution is to log the interesting information to a separate log file. Also, you can assign a UUID to each event, so you can track each event more easily. You can also write some reporting tool that allows you to extract specific information from the log files. (Or alternatively, use switches in the code to log different information to different log files). – Giorgio Jul 29 '12 at 8:07
3

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

The model of event driven programming simplifies coding to some extent. It has probably evolved as a replacement of big Select (or case ) statements used in older languages and gained popularity in early Visual development environments such as VB 3 (Don't quote me on the history, I did not check it)!

The model becomes a pain if the event sequence matters and when 1 business action is split across many events. This style of process violates the benefits of this approach. At all costs, try to make the action code encapsulated in the corresponding event and don't raise events from within events. That then becomes far worse than the Spaghetti resulting from the GoTo.

Sometimes developers are eager to provide GUI functionality that requires such event dependency, but really there is no real alternative that is significantly simpler.

Bottom line here is that the technique is not bad if used wisely.

  • Is their any alternative design to avoid the 'worse than the Spaghetti' ? – Guillaume Jul 16 '12 at 15:45
  • I am not sure there is an easy way without simplifying the expected GUI behavior, but if you list all your user interactions with a window/page and for each determine exactly what your program will do, you could begin grouping code into one place and direct several events to a group of subs/methods that are responsible for the real processing of the request. Also separating code that only serve the GUI from code that does the back end processing may help. – NoChance Jul 16 '12 at 15:50
  • The need to post this message came from a refactoring when I moved from a inheritance hell to something based on event. On some point it is really better, but on some other it is quite bad... Since I've already had some trouble with 'events gone wild' in some GUI, I'm wondering what can be done to improve maintenance of such event sliced code. – Guillaume Jul 16 '12 at 16:09
  • Event-driven programming is far older than VB; it was present in the SunTools GUI, and before that I seem to remember that it was built into the Simula language. – kevin cline Jul 16 '12 at 17:43
  • @Guillaume, I guess you are over engineering the service. My above description was in fact based on GUI events mostly. Do you (really) need this type of processing? – NoChance Jul 16 '12 at 20:16
3

I wanted to update this answer since I've gotten some eureka moments since after "flattening" and "flattening" control flows and have formulated some new thoughts on the subject.

Complex Side Effects vs. Complex Control Flows

What I've found is that my brain can tolerate complex side effects or complex graph-like control flows as you typically find with event handling, but not the combo of both.

I can easily reason about code that causes 4 different side effects if they're being applied with a very simple control flow, like that of a sequential for loop. My brain can tolerate a sequential loop which resizes and repositions elements, animates them, redraws them, and updates some kind of auxiliary status. That's easy enough to comprehend.

I can likewise comprehend a complex control flow as you might get with cascading events or traversing a complex graph-like data structure if there's just a very simple side effect going on in the process where order doesn't matter the slightest bit, like marking elements to be processed in a deferred fashion in a simple sequential loop.

Where I get lost and confused and overwhelmed is when you have complex control flows causing complex side effects. In that case the complex control flow makes it difficult to predict in advance where you're going to end up while the complex side effects make it difficult to predict exactly what's going to happen as a result and in what order. So it's the combination of these two things that makes it so uncomfortable where, even if the code works perfectly fine right now, it's so scary to change it without fear of causing unwanted side effects.

Complex control flows tend to make it difficult to reason about when/where things are going to happen. That only becomes really headache-inducing if these complex control flows are triggering a complex combination of side effects where it's important to understand when/where things happen, like side effects that have some kind of order dependency where one thing should happen before the next.

Simplify the Control Flow or the Side Effects

So what do you do when you encounter the above scenario which is so difficult to comprehend? The strategy is to either simplify the control flow or the side effects.

A widely-applicable strategy to simplifying side effects is to favor deferred processing. Using a GUI resize event as an example, the normal temptation might be to reapply the GUI layout, reposition and resize the child widgets, triggering another cascade of layout applications and resizing and repositioning down the hierarchy, along with repainting the controls, possibly triggering some unique events for widgets that have custom resizing behavior which trigger more events leading to who-knows-where, etc. Instead of trying to do this all in one pass or by spamming the event queue, one possible solution is to descend down the widget hierarchy and mark what widgets need their layouts to be updated. Then in a later, deferred pass which has a straightforward sequential control flow, reapply all the layouts for widgets that need it. You might then mark what widgets need to be repainted. Again in a sequential deferred pass with straightforward a control flow, repaint the widgets marked as needing to be redrawn.

This has the effect of both simplifying the control flow and side effects since the control flow becomes simplified since it's not cascading recursive events during the graph traversal. Instead the cascades occur in the deferred sequential loop which might then be handled in another deferred sequential loop. The side effects become simple where it counts since, during the more complex graph-like control flows, all we're doing is simply marking what needs to be processed by the deferred sequential loops which trigger the more complex side effects.

This does come with some processing overhead but it might then open up doors to, say, doing these deferred passes in parallel, potentially allowing you to get an even more efficient solution than you started if performance is a concern. Generally performance shouldn't be much of a concern in most cases though. Most importantly, while this might seem like a moot difference, I have found it so much easier to reason about. It makes it much easier to predict what's happening and when, and I can't overestimate the value that can have in being able to more easily comprehend what's going on.

2

What has worked for me is making each event stand on its own, without reference to other events. If they are coming in asynchroniously, you don't have a sequence, so trying to figure out what happens in what order is pointless, besides being impossible.

What you do end up with are a bunch of data structures that are getting read and modified and created and removed by a dozen threads in no particular order. You've got to do extemely proper multi-threaded programming, which is not easy. You've also got to think multi-threaded, as in "With this event, I am going to look at the data I have at a particular instant, without regard to what it was a microsecond earlier, without regard to what just changed it, and without regard to what the 100 threads waiting for me to release the lock are going to do to it. Then I will make my changes based on this even and what I see. Then I am done."

One thing I find myself doing is scanning for a particular Collection and making sure that both the reference and the collection itself (if not threadsafe) are locked correctly and synchronized correctly with other data. As more events are added, this chore grows. But if I was tracking the relationships between events, that chore would grow a lot faster. Plus sometimes a lot of the locking can be isolated in its own method, actually making the code simpler.

Treating each thread as a completely independent entity is difficult (because of the hard-core multi-threading) but doable. "Scalable" may be the word I'm looking for. Twice as many events take only twice as much work, and maybe only 1.5 times as much. Trying to coordinate more asynchronious events will bury you quickly.

  • I have updated my question. You are totally right about sequence and async stuff. How would you handle a case where event X must executed after all the others event are processed ? It is looking like I have to create a more complex Handlers manager, but I also want to avoid to expose too much complexity to the users. – Guillaume Jul 16 '12 at 16:28
  • In your set of data, have a switch and some fields that are set when event X is read. When all others are processed, you check the switch and know you have to handle X and you have the data. The switch and the data should stand on their own, actually. When set, you should think, "I have to do this job," not "I have to hande X." Next problem: how do you know the events are done? and what if you get 2 or more events X? Worst case, you can run a looping maintenance thread that checks the situation and can act on its own initiative. (No input for 3 secs? X switch set? Then run shutdown code. – RalphChapin Jul 16 '12 at 16:43
2

It sounds like you are looking for State Machines & Event Driven Activities.

However, you might also want to look at State Machine Markup Workflow Sample.

Here you are a short overview of state machine implementation. A state machine workflow consists of states. Each state is composed of one or more event handlers. Each event handler must contain a delay or an IEventActivity as the first activity. Each event handler can also contain a SetStateActivity activity that is used to transition from one state to another.

Each state machine workflow has two properties: InitialStateName and CompletedStateName. When an instance of the state machine workflow is created, it is put into the InitialStateName property. When the state machine reaches the CompletedStateName property, it finishes execution.

  • 2
    Whilst this may theoretically answer the question, it would be preferable to include the essential parts of the answer here, and provide the link for reference. – Thomas Owens Jul 16 '12 at 14:00
  • But if you have dozens of handlers attached to each state, you'll be not so fine when trying to understand what is going on... And every event based component may not be described as a state machine. – Guillaume Jul 16 '12 at 15:27
1

Event driven code is not the real problem. I fact I have no problem following logic in even driven code, where call-back are explicitly defined or in-line call-backs are used. For example generator style callbacks in Tornado are very much easy to follow.

What is real hard to debug are dynamically generated function calls. The (anti?)pattern which I would call the Call-back Factory from Hell. However, this kind of function factories are equally hard to debug in traditional flow.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.