Event Dispatching: One Size Doesn’t Fit All

My next two articles describe possible implementations of a type safe event dispatching mechanism, based on the Multicast pattern [1], in the context of single-layered and multilayered receptors. The events are propagated via a modified Chain of Responsibility [2] to handlers located at the same or different level than the event source.

The first installment introduces the reader to the basic mechanisms, refined after that throughout two different versions.

Short History

Event dispatching is central to event-based operating systems as well as messaging systems. Usually based on a combination of patterns including Observer, State, and/or variants, an Event Dispatcher (ED) has a couple of distinct components: senders (event sources), events, and receptors (event handlers). This article mainly concentrates on events and handlers, introducing relays as an additional concept that will be discussed later.

EDs can be divided into two main categories based on the nature of the dispatching mechanism: type safe or polymorphic [1]. A type-safe DM preserves the real type of the event; a polymorphic DM manipulates an Event base class, additional dynamic casting being typically needed to recover the original type, for example:

void handle (const BaseEvt* e)
{
   if (dynamic_cast<DerivedEvt*>(e) )    //do Derived
   else if    .
}

The type-safe approach was introduced in [1] as the Multicast pattern:

There are obvious reasons to use a type-safe implementation when the system exposes different Event types. This is the approach used exclusively in this article.

A Layered Approach

Events help in decoupling different parts of a system, parts that can be:

  1. In the same module/layer or
  2. Distributed in different modules/layers.

In A, the Abstract Receivers can become independent concrete Dispatchers linked by a special recursive mechanism based on a type-safe Chain of Responsibility—implementation encapsulated in what I will call a Local Relay. Basically, this approach avoids any run-time polymorphism (as opposed to [1], for example).

But, one can imagine A extended on multiple layers, each layer having unique Dispatchers linked in different Chains. Additional Global Relay components are required, responsible for passing the information between layers, so that if an event cannot be solved in the current layer, it will be relayed to the next one. Please note that the Sender doesn’t know the final Receiver but might have the compile-time certitude that the message will be handled (is implementation specific).

I will call an ED single-layered if between the Source of the Event and the final Receiver there is no Global Relay (in other words, if an event can be solved in the chains directly linked to the source). Otherwise, the ED is multilayered.

A Single Layered Approach: Implementation

I will start with a Multicast implementation demonstrating the basic concepts of Dispatchers and message passing in a single-layered environment [3].

template <class D>
struct Relay
{
   template<class E>
   static void relay(const E& e)
   {
      D::relay(e);
   }
};

template <>
struct Relay <void>
{
   static void relay()
   { }
};

struct EvtA
{
   EvtA(char c) : e(c)
   {}
   char e;
};

template <class R>
struct TopLayerDispatcher : public Relay<R>
{
   using Relay<R>::relay;
   static void relay(const EvtA& e)
   {
      std::cout << "TopLayer: " << e.e <<std::endl;
   }

};

template <class R>
struct BRLayerDispatcher : public Relay<R>
{

   using Relay<R>::relay;
   static void relay(const EvtB& e)
   {
      std::cout << "BRLayer: " << e.e <<std::endl;
   }

};

template <class R>
struct BOLayerDispatcher : public Relay<R>
{

   using Relay<R>::relay;
   static void relay(const EvtC& e)
   {
      std::cout << "BOLayer: " << e.e <<std::endl;
   }

};

template <class W, class E>
void relay(const E& e)
{
   Relay< W >::relay(e);
}

void action()
{

   EvtA e('w');

   typedef TopLayerDispatcher <
           BRLayerDispatcher  <
              BOLayerDispatcher  <
                 void >
                 >
           >
        W;

   relay<W>(e);
}

Now, analyze the above code.

The Dispatchers play the role of the Abstract Receivers from the Multicast pattern. They are assembled at compile time in the complex type W (that actually represents your Chain of Responsibility) that’s processed step-by-step using CRTP (Curiously Recurring Template Pattern [4]), the entire stepping mechanism being encapsulated in the class Relay (the Local Relay). The chain uses void as an end-of-chain tag.

The entire process is type-safe: A compile error is triggered if the event(s) to be processed have no corresponding handlers. This behavior can be easily inhibited in the Relay <void> specialization, as will be further discussed. Nothing special so far…

But, let me complicate things a notch. What if you want to give the chance of handling a particular event to more than one handler? This is obviously not possible with the above code that’s based on a first come, first served paradigm. Some adjustments are necessary [5]:

template <class W>
struct Rel
{
   template < E>
   void relay(const E& e)
   {
      Relay< W >::relay(e);
   }
};

template <class E>
class RelayInterface
{
private:
   typedef void (*FP)( void* a, const E& );
public:
   template <class T>
   RelayInterface(T& x)
   : p_(&x),
   pf_(&functions<T>::relay)
   {}
   void relay(const E& x)
   {
      pf_(p_, x);
   }
private:
   template <class T>
   struct functions
   {
      static void relay(void* a, const E& x)
      {
         static_cast<T*>(a )->relay(x);
      }
   };
   void* p_;
   FP pf_;
};

template <class E>
struct RelayInterfaceContainer
{
   typedef std::vector<RelayInterface<E> > VF;

   template <class W>
      static void push_back(Rel<W>& rel)
      {
         RelayInterface<E> r(rel);
         vf_.push_back(r);
      }

      static void dispatch(const E& e)
      {
         for (typename VF::iterator i = vf_.begin();
              i != vf_.end(); ++i)
            {
               i->relay(e);
            }
         }
private:
   static VF vf_;

};

template <class E> std::vector<RelayInterface<E> >
         RelayInterfaceContainer<E>::vf_;



template <class E>
void dispatch(const E& e)
{
   RelayInterfaceContainer<E>::dispatch(e);
}

void action()
{

   EvtB eb('b');
   EvtA ea('a');
   EvtC ec('c');

   typedef TopLayerDispatcher <
      BRLayerDispatcher <
         BOLayerDispatcher <
            void >
         >
      >
      W;

   typedef TopLayerDispatcher <
      BRLayerDispatcher <
      void >
   >
   WW;

   Rel<W> w;
   Rel<WW> ww;

      RelayInterfaceContainer<EvtB>::push_back(w);
      RelayInterfaceContainer<EvtB>::push_back(ww);

      RelayInterfaceContainer<EvtA>::push_back(w);
      RelayInterfaceContainer<EvtA>::push_back(ww);

      RelayInterfaceContainer<EvtC>::push_back(w);
      //RelayInterfaceContainer<EvtC>::push_back(ww);

      dispatch(eb);
      dispatch(ea);
      dispatch(ec);
}

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read