Alexander
Alexander

Reputation: 758

design of event dispatcher

there is a large c++ project that receives data from outside and after small processing, new data are sent to client code (within the app). now all the client code implements lots of interfaces (e.g. IOnConcreteDataReceived) and pointers to these interfaces are set to the part that receives data from outside.

Event dispatcher looks like a good choice for code decoupling. I would like to get the following code:

// dispatcher global access
IEventDispatcher & getEventDispatcher();
//producer:
ConcreteDataType data{values};
getEventDispatcher().fireEvent<ConcreteDataTypeEvent>(data);

//consumer:
getEventDispatcher().subscribe<ConcreteDataTypeEvent>([](const ConcreteDataType & data){ processData(data);});

As far as in most cases the data types are different the simplest solution that comes to mind is to use data types as event types:

//producer:
ConcreteDataType data{values};
getEventDispatcher().fireEvent<ConcreteDataType>(data);

//consumer:
getEventDispatcher().subscribe<ConcreteDataType>([](const ConcreteDataType & data){ processData(data);});

the implementation is simple

The only thing that i do not like about this variant is the usage of data types as event types. It does not look intuitive

Separation of event types from data types leads to more complicated code. there should be an association between event types and data types, possibly in a central place.

The question: Is the purity of design (separated event types and data types) worth additional efforts or usage data types as event types is not as bad as i think of it? How to implement compile-time checks for event type - data type association?

Upvotes: 1

Views: 608

Answers (1)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275600

namespace notstd {
  template<class T> struct tag_t{constexpr tag_t(){} using type=T;};
  template<class T> constexpr tag_t<T> tag{};
  template<class Tag> using type=typename Tag::type;
}

this lets us manipulate types as values. This is useful, because functions operate on values, and functions have ADL.

namespace dispatch_events {
  template<class T>
  constexpr notstd::tag_t<T> data_type_mapping( notstd::tag_t<T> ) { return {}; }

  template<class E>
  using data_type = notstd::type<decltype(data_type_mapping(notstd::tag<E>))>;
}

now, dispatch_events::data_type<E> is the type of the data used by event E. By default, it is just E.

If you want to create an event with a different data type, create an event type like this:

namespace somewhere {
  struct some_data { int x = 0; };
}

namespace anywhere {
  struct event_bob {};

  constexpr notstd::tag_t<somewhere::some_data>
  data_type_mapping( notstd::tag_t<event_bob> ) { return {}; }
}

now, dispatch_events::data_type< anywhere::event_bob > is somewhere::some_data.

You get easy event<->data correspondence for the common case, and the ability to have the same data on multiple events without having a central registry anywhere. Anyone mapping the event type to data type must have the event type in view; for this to work, they must also have the data_type_mapping function (and hence, the data type) as well.

Simply place the data_type_mapping function in the namespace of the event type, and all is good.

This also permits an event type to map to a primitive type, like int, or a type you do not own, like std::shared_ptr<bob>.

In case you are wondering how this works, it works because template evaluation looks up in the template declaration context and using ADL (argument dependent lookup). ADL of data_type_mapping finds the non-template overload, and that is then preferred over the template overload in dispatch_events namespace. ADL examines the arguments, and the template arguments of the arguments, to find namespaces to search for possible candidate overloads.

Upvotes: 1

Related Questions