Ok, I’ve got time to explain now.
You probably won’t have chance to read this until morning (unless you want nightmares about event handlers coming to eat you).
I’ll start with the simple stuff and move on to the more complicated issues.
Overview
So, the ButtonDispatcher
(a special kind of event dispatcher) maintains a std::vector
of pointers to ButtonHandler
s (a special kind of event handler).
Each handler has to be individually registered with the dispatcher.
(And in a more robust system, they also have to be unregistered when no longer needed.)
When the event is handed to the dispatcher, it goes through every registered handler, in order of registration, until it finds one that can handle the event.
A handler signals that it managed to handle the event by returning true
, and signals that it couldn’t handle it or doesn’t want to handle it by returning false
.
So if a handler returns true
, the dispatcher stops looking.
A more robust system would allow the order of handlers to be manipulated.
Also, it’s worth noting that none of the pointers in the dispatcher’s std::vector
can be nullptr
because they have to be registered by reference, not by pointer, and references cannot be nullptr
.
This makes the process safer and faster (you don’t need an if(handler == nullptr)
in the for loop).
Dynamic Array vs Linked List
In most examples of the chain of command pattern, each handler has a pointer to another handler.
In doing this they essentially form a unidirectional linked list (with each handler being a node in the list), and this is where the ‘chain’ part of the name comes from.
This can seemingly make the handlers a bit easier to manipulate because you only have to consider two or three handlers at a time,
but as with anything in programming the complexity has to end up somewhere.
The complexity from this approach is two-fold.
Firstly you have the same old ownership problem:
- Does a parent handler own its child handler?
- When does deletion occur?
And secondly, if you try to break the chain, you’re not just splitting two nodes apart, you’re splitting the whole chain, so you have to think about what that means for the whole chain.
This is why my approach uses a dynamic array (in the form of a std::vector
).
It has the same overal effect: you get a list of handlers with a well-defined order, but it’s more efficient and more familiar.
To traverse a std::vector
, the code boils down to copying a pointer and then incrementing it.
To traverse a linked list, you must continually copy pointers from different areas of memory, effectively jumping all over the place.
The Pokitto may not have a cache to worry about,
but that’s still length - 1
extra pointer copies.
Typically with a chain-of-responsibility you rarely alter the chain, but you iterate it often,
so you want fast iteration, not fast alteration.
(This is true in life as well.)
A final note on this:
There is precisely one advantage to the linked list approach, and that is if you want the handlers themselves to decide whether the next node should be called or not.
The Complex Parts
It’s worth noting that the dispatcher can actually be thought of as a special kind of event handler - it handles the events by asking other event handlers to handle them.
This is why I have commented out : public ButtonHandler
on ButtonDispatcher
- to signal that it can be thought of this way, but you should question if it’s a good idea before doing so.
As cool as a tree of handlers sounds, it’s not especially useful unless you want to be able to build a decision tree of handlers.
In a more robust system, the pointers in the std::vector
would be std::shared_ptr
, which prevents the case where a pointer in the dispatcher becomes invalid without the pointer being deregistered.
However, this actually only offsets the problem.
Instead of ending up with crashing or garbled nonsense, you’re left with a handler in the dispatcher that you can’t get rid of.
So, you have several choices to fix this, but only two good ones:
- Have the dispatcher maintain a list of
std::weak_ptr
, and check that a pointer hasn’t deallocated before trying to use it, whilst removing any that have been deallocated.
- Periodically check the
use_count()
of the std::shared_ptr
s and discard any that have a use_count()
of 1
, because that indicates that the dispatcher is the only one keeping the pointer alive.
The first option is more difficult, but it’s the better option.
If you were using C++03, you wouldn’t have either of these options, so praise be C++11. :P
The not so good options that I’ve seen people attempt:
- Pray that everything that registers a handler remembers to deregister it afterwards (which isn’t easy, because then those objects need to somehow reference the dispatcher, or at the very least the code that destroys them does).
- Have a method that manually marks the handler as destroyed and have the dispatcher check that to know when to remove a handler (horrible unnecessary complexity).