1

Scenario: say I have a base class SketchbookEventObserver and derived classes MouseClickObserver, TouchObserver and TextChangedObserver.

All of these SketchbookEventObservers have to make a network request with data about the event that happened:

MouseClickObserver - coordinates of the mouse click.

TouchObserver - coordinates and duration of touch.

TextChangedObserver - the old and new text with a text box identifier.

All of these observers are registered on a UIEventRegistry class. When an event happens, it woud call OnEvent on each observer, and pass as params:

  1. The type of event - mouse click / touch / text changed. This is represented by an ID.
  2. The data about the event - as described above. Each type of event will have different types of data.

However, I cannot override OnEvent with different input parameters in each of the derived classes. If I have the input parameter generic and polymorphic, say EventData with a GetData() function, I would still need to override GetData() in derived classes of EventData, which will have different return values. This is also impossible.

The other option is to not have any inheritance between these observers, and treat them as separate entities. EventRegistry will have an array / list of observers of each type, where their types are known, then call mMouseClickObservers[i].OnEvent() for a mouse click event, mTouchObservers[i].OnEvent() for a touch event, and so on.

But this means that EventRegistry will need to have knowledge about the concrete classes, which need to be exposed publicly if EventRegistry is part of a different library / package.

Is there a better way to design this?

3 Answers3

1

One way is you can derive MouseClickEventData, TouchEventData, TextChanged classes from EventData class. And cast your EventData so you will have specific classes for each data.

OnEvent(EventData *data) {
    if (data->type == MOUSE_CLICK) {
        MouseClickEventData *mData = dynamic_cast<MouseClickEventData*>(data); 
        // use mData->getCoordinates();
    }
    if (data->type == TEXT_CHANGED) {
        TextChangedEventData *tData = dynamic_cast<TextChangedEventData*>(data); 
        // use tData->getNewText();
    }
    ....
}
idris
  • 488
  • 3
  • 6
  • This is definitely something I thought of, too. I'm contemplating on whether dynamic_cast is a good way to solve this, or a more elegant design solution exists. – Guru Prasanna Feb 10 '20 at 13:14
  • At some point you have to use cast. Or you can use generic data representantion like xml, json. – idris Feb 10 '20 at 13:23
  • 2
    Wouldn't this mean that EventData solely exists for the dynamic_cast and has no actual methods that are overridden? It doesn't offer any polymorphism on its own. Is this good design? – Guru Prasanna Feb 10 '20 at 13:25
  • Well it doesnt need to has something override. It allows to you have only one OnEvent function. Still it may have something override depends on your requirements. For eg. EventType EventData::GetType() can be overridable – idris Feb 10 '20 at 13:31
1

In order to vary the behavior of the observers based on both the type of the observer and the thing being observed, we need to use double-dispatch. The Visitor Pattern uses double-dispatch to allow a set of visitors to observe a set of events. I won't provide an implementation here, but the psuedocode would resemble:

interface SketchbookEventObserver:
    void handle(MouseClickEvent event);
    void handle(TouchEvent event);

interface SketchbookEvent:
    void accept(SketchbookEventObserver observer);

class MouseClickObserver implements SketchbookEventObserver:

    void handle(MouseClickEvent event):
        ...

    void handle(TouchEvent event):
        ...

interface MouseClickEvent implements SketchbookEvent:

    void accept(SketchbookEventObserver observer):
        observer.handle(this);

An alternative is to register a listener for each type of event. This is the approach taken by Java with its UI frameworks. For example:

class Component:

    void registerMouseClickListener(MouseClickListener listener):
        ...

    void registerTouchListener(TouchListener listener):
        ...

interface MouseClickListener:
    void handle(MouseClickEvent event);

interface TouchListener:
    void handle(TouchEvent event);
Justin Albano
  • 3,809
  • 2
  • 24
  • 51
  • Visitor is a good solution for a small, fixed set of events; so it could work here. If there are likely to be new event types in the future, Visitor becomes a maintenance problem. – jaco0646 Feb 10 '20 at 14:53
  • @jaco0646 That is very true. If there are numerous events to watch, an event handler interface for each event is a better option. – Justin Albano Feb 10 '20 at 15:07
1

The other option is to not have any inheritance between these observers, and treat them as separate entities.

This is my favorite option, and one that I've advocated many times because it maintains type safety of the events. If each event is different, don't handle them all through the same API.

But this means that EventRegistry will need to have knowledge about the concrete classes...

MouseClickObserver, TouchObserver, and TextChangedObserver should all be abstractions. You may only have one implementation of each abstraction now; but the design should not enforce that.

jaco0646
  • 15,303
  • 7
  • 59
  • 83