Examples in Event Dispatching

Friday Dec 30th 2005 by Radu Braniste
Share:

Learn how dispatchers can be stacked in layers and how events can be selectively sent to certain levels only or broadcasted. Two illustrative implementations are presented: a basic one introducing the main concepts, later refactored in a more refined implementation.

This is the last article [1] in a series describing possible implementations of a type safe event dispatching mechanism in the context of single-layered and multilayered dispatchers/receptors. This time, you'll learn how dispatchers can be stacked in layers and how events can be selectively sent to certain levels only or broadcasted. Two illustrative implementations are presented: a basic one introducing the main concepts, later refactored in a more refined implementation.

The Layered Approach Revisited

In the previous article, I introduced the concept of single-layered ED (Event Dispatcher) as opposed to multilayered ED:

"We 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."

The following diagram exemplifies the multilayered model:

A multilayered system might have event sources possible at every level. Every layer hosts a relay (exceptionally more than one). The relay layout can be composed at runtime so that an event can circulate between relays in a certain flow. Every relay consumes events by further dispatching them to one or more Dispatchers (the Dispatcher/Receptor part was presented in the previous article). If the event is unknown, it is relayed to the next layer, if any, or the compiler detects the missing receptor.

Some scenarios:

  • Event A generated at Level 2->Relay2->Dispatcher2-> No Receptor found->relay to Level3->Relay3->Dispatcher3->No Receptor found->Compiling Error.
  • Event B generated at Level 2->Relay2->Dispatcher2-> Receptor found

The above architectural model serves well in cases when a high level of decoupling is needed. Relays are the single points of contact between modules, relieving modules from sending messages directly from one to another. On top of this, due to the type safe implementation, there is no price to pay for additional indirection/casting and the speed of execution is unaffected.

Notes:

  1. An undeclared event (Event F, for example) generates a compiling error.
  2. Relaying to the last Layer is equivalent to a broadcast (all the relays will be tested until a receptor is found).
  3. "No Receptor Found" triggering a compiler error can be easily changed into a catch-all strategy and possibly a policy can be used, the same way it was described in [1].
  4. A real broadcast can be implemented if more than one receptor is required for the same event in two different ways by using parallel workflows as described in [1].
  5. From the implementation point of view, there is almost no distinction among Relays (Global or Local), Dispatchers, and Receptors. They are different at the architectural level and different in the way they are aggregated.
  6. A Global Relay can be associated directly to a Local Relay and the code described in [1] can be plugged at the Local Relay level (for example, Relay Level 2 is a Global relay, Dispatcher 2 is actually a Local Relay, and Receptors B and C are the Dispatchers as described in [1]). This is useful in cases when a Local Relay "knows" more than one event; then, a single-layered ED offers all the required flexibility in how to relay the events horizontally. A simpler approach is to dispatch the events in a flat mode, as described in the accompanying code.

The Multilayered Approach: Implementation

Due to the similarity of Global and Local Relays, the implementation is almost the same as in [1], with two small cosmetic changes: This time Relay::relay is static no more and one Dispatcher/Relay (Basic) no longer inherits from Relay.

template <class D>
struct Relay
{
   Relay(const D& d) : d_(d)
   {}
   template<class E>
   void relay(const E& e)
   {
      d_.relay(e);
   }
private:
   D d_;
};


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

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

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

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

struct BasicLayerDispatcher
{
   void relay(const EvtA& e)
   {
      std::cout << "BasicLayer: " << e.e <<std::endl;
   }
};

template <class R>
struct SecondLayerDispatcher : public Relay<R>
{
   SecondLayerDispatcher(const R& r) : Relay<R>(r){}

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

template <class R>
struct ThirdLayerDispatcher : public Relay<R>
{
   ThirdLayerDispatcher(const R& r) : Relay<R>(r){}

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

The difference is in how Relays are handled:

namespace RelayGenerator1
{

   template <int I>
   struct RG
   {};

   template <>
   struct RG<0>
   {
      typedef BasicLayerDispatcher DISPATCHER;
      typedef Relay<DISPATCHER> RELAY;
      static RELAY getRelay()
      {
         return RELAY(DISPATCHER());
      }
   };

   template <>
   struct RG<1>
   {
      typedef SecondLayerDispatcher< RG<0>::RELAY > DISPATCHER;
      typedef Relay<DISPATCHER > RELAY;
      static RELAY getRelay()
      {
         return RELAY(DISPATCHER(RG<0>::getRelay()));
      }
   };

   template <>
   struct RG<2>
   {
      typedef ThirdLayerDispatcher< RG<1>::RELAY > DISPATCHER;
      typedef Relay<DISPATCHER > RELAY;
      static RELAY getRelay()
      {
         return RELAY(DISPATCHER(RG<1>::getRelay()));
      }
   };

   template <int L, class E>
   void relay(const E& e)
   {
      RG<L>::getRelay().relay(e);
   }
}
void action()
{
   EvtD e('w');

   RelayGenerator1::relay<2>(e);
}

Please note how the Relays are indexed and chained. A helper function (RelayGenerator1::relay) allows event dispatching to start from a user-specified level. For example, RelayGenerator1::relay<1> excludes both EvtC and EvtD.

The Refinement

A more sophisticated example is presented in the next code excerpt:

namespace RelayGenerator2
{

   template <class R, template <class T> class D = Relay>
      struct RGBase
      {
         typedef D< typename R::RELAY > DISPATCHER;
         typedef Relay<DISPATCHER > RELAY;

            static RELAY getRelay()
            {
               return RELAY(DISPATCHER(R::getRelay()));
            }

      };
   template <class R >
      struct RGBase<R, Relay>
      {
         typedef R DISPATCHER;
         typedef Relay<DISPATCHER > RELAY;

         static RELAY getRelay()
         {
            return RELAY(R());
         }
      };

   template <int I>
      struct RG
      {
         typedef void DISPATCHER;
      };

   template <>
      struct RG<0> : RGBase<BasicLayerDispatcher>
      { };

   template <>
      struct RG<1> : RGBase<RG<0>, SecondLayerDispatcher >
         { };

   template <>
      struct RG<2> : RGBase< RG<1>, ThirdLayerDispatcher>
         {};

   template <int L, class E>
      void relay(const E& e)
      {
         RG<L>::getRelay().relay(e);
      }

   template <int I>
      struct IsValidRG
      {
         typedef char ONE;
         typedef struct {char a[2];} TWO;
         template <class C>
            static ONE isClass(...);
         template <class C>
            static TWO isClass(int C::*);
         enum E {
            e=sizeof(isClass<typename RG<I>::DISPATCHER>(0) )
               == 2 ? IsValidRG<I+1>::e : I-1
         };

      };

   template <>
      struct IsValidRG<200>
      {
         enum E {
            e
         };
      };

   template <class E>
      void broadcast(const E& e)
      {
         relay<IsValidRG<0>::e>(e);
}

void action()
{
}

The first observation is that RG was factored out using RGBase. The second and more important observation is that a broadcast method was implemented. As mentioned in Note 2, broadcast is automatically achieved by dispatching to the last Relay of the chain. A method of automatically finding the last specialization of a Relay (isValidRG) is based on the SFINAE (Substitution Failure Is Not An Error [2]) concept and the remark that RG::DISPATCHER is a class for all the cases when RG is a specialization and otherwise not.

Where You Are

I presented two multilayered Event Dispatching implementations, together with a design overview. Single- and multilayered designs were compared and their complementary was underlined. With minimal effort, the reader can either combine the two approaches or use one at a time only.

Download the Code

You can download the code that accompanies this article here.

References

  1. Event Dispatching: One Size Doesn't Fit All: Part 1
  2. C++ Templates: The Complete Guide.
    Nicolai M. Josuttis. Addison-Wesley, 2002
Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved