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);
}

Event Dispatching: One Size Doesn't Fit All

There are two dispatching chains of types W and WW. Registration at run-time (equivalent to storing both in the same container) is impossible because containers should be homogeneous.

But there is hope—the same idiom used in static interfaces [6] can be used to mask a type that can be recovered at a later time. This piece of magic is implemented in RelayInterface that conceals W and WW. It is now possible to use the following construct:

std::vector<RelayInterface<E> >

allowing containers discriminated by event types only. Once the chains are registered, a single "dispatch(evt);" call takes care of dispatching the event through all the registered chains. Because the process never tries to hide the true type of the event, it is fully type safe; the compiler rejects any attempt to use an unhandled event. The obvious downside is the need to register every chain against every event and this can be sometimes a very laborious task.

A third attempt simplifies the previous model even further [7]:

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

template <>
struct Relay <void>
{
   static void relay()
   { }
   template <class E>
   static void relay(const E& e)
   {
      std::cout << "No relay to catch event " << e.e << std::endl;
   }

};

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

template <class W>
struct Rel
{
   typedef Relay< W > WWW;
};

struct EvtTableInit
{
protected:
   template <class T>
   struct functions
   {
      template <class E>
      static void relay(const E& e)
      {
         T::WWW::relay(e);
      }
   };
};

struct EvtTable : private EvtTableInit
{
private:
   typedef void (*FPA)(const EvtA& );
   typedef void (*FPB)(const EvtB& );
   typedef void (*FPC)(const EvtC& );
public:
   template <class T>
   EvtTable(T& )
   :
   pfa_(&functions<T>::relay),
   pfb_(&functions<T>::relay)
   pfc_(&functions<T>::relay)
   {}

   void relay ( const EvtA& e) const
      {
      pfa_( e);
      }
   voidB  relay ( const EvtB& e) const
      {
      pfb_( e);
      }
   void relay (const EvtC& e) const
      {
      pfc_( e);
      }

private:
   FPA pfa_;
   FPB pfb_;
   FPC pfc_;
};

template <class EVT_TABLE>
class RelayInterface
{
public:
   typedef Relay< void > WWW;
   template <class T>
   RelayInterface(T& x)
   : et_(x)
   {}
   template <class E>
   void relay(const E& e) const
   {
      et_.relay( e);
   }

private:
   EVT_TABLE et_;
};

template <class EVT_TABLE>
struct RelayInterfaceContainer
{
   template <class R>
      void addRelay()
      {
         Rel<R> w;
         RELAY r(w);
         vf_.push_back(r);
      }
   template <class E>
   void relay(const E& e)
   {
   for ( typename VF::iterator i = vf_.begin(); i != vf_.end(); ++i)
      {
      i->relay(e);
      }
   }
private:
   typedef RelayInterface<EVT_TABLE> RELAY;
   typedef std::vector <RELAY> VF;
   VF vf_;
};

void action()
{

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

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

   typedef TopLayerDispatcher <
      BOLayerDispatcher <
         void >
      >
      WW;

   RelayInterfaceContainer<EvtTable> rc;

   rc.addRelay<W>();
   rc.addRelay<WW>();

   rc.relay(eb);

}

The events are now kept in the static interface, their use becoming implicit. As a result, RelayInterface can be refactored and simplified.

RelayInterface doesn't use the extra void* instance variable, because the whole approach is now static. Taking advantage of the unique signature of the relay, method EvtTableInit encapsulates the main "hiding" mechanism used by the static interface while EvtTable and EvtTable1 demonstrate how easily you can extend the model to allow new events to be integrated. On the same note, RelayInterfaceContainer takes EvtTable as a policy, further increasing the flexibility.

The previous versions were inflexible: There is a compilation error every time a handler for a certain event is missing. Now, this requirement was relaxed and the user can choose between two different specializations, triggered by selecting the appropriate end-of-chain tag:

  1. Relay<void> has a new catchall method that defers the catch of unsupported events for runtime.
  2. Relay<void*> preserves the previous behavior.

Where We Are

This time, I presented three type-safe variations of a single-layered Event Dispatching mechanism, using a Chain of Responsibility-like relaying mechanism. The basic implementation was refined to accommodate generalizations such as distributing the same event to more than one handler or different end-of-chain policies. The Static Interface idiom was used to make the code more generic.

In the next installment, I'll discuss a multilayered Event Dispatching implementation, together with an overview of potential use cases.

References

[1] Pattern Hatching: Design Patterns Applied by John M. Vlissides. Addison-Wesley, 1998.

[2] Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, and Ralph Johnson. Addison-Wesley, 1994.

[3] The code can be found in Basic1LayeredRelay/relay.h.

[4] C++ Templates: The Complete Guide by David Vandevoorde and Nicolai M. Josuttis. Addison-Wesley, 2002.

[5] The code can be found in 1LayeredRelayVer1/relay.h.

[6] "C++ Idioms in BREW: Better Interfaces."

[7] The code can be found in 1LayeredRelayVer2/relay.h



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: April 22, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Database professionals — whether developers or DBAs — can often save valuable time by learning to get the most from their new or existing productivity tools. Whether you're responsible for managing database projects, performing database health checks and reporting, analyzing code, or measuring software engineering metrics, it's likely you're not taking advantage of some of the lesser-known features of Toad from Dell. Attend this live …

  • Ever-increasing workloads and the challenge of containing costs leave companies conflicted by the need for increased processing capacity while limiting physical expansion. Migration to HP's new generation of increased-density rack-and-blade servers can address growing demands for compute capacity while reducing costly sprawl. Sponsored by: HP and Intel® Xeon® processors Intel, the Intel logo, and Xeon Inside are trademarks of Intel Corporation in the U.S. and/or other countries. HP is the sponsor …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds