1

Assume I have a factory which takes in a series of bytes and outputs a pointer to a newly-created abstract message.

Now, I would like to define some extensible, manageable, and clean way to "handle" those various messages without knowing the actual concrete type. To me, this sounded like a great application for the visitor design pattern. My various handler objects (i.e., visitors) would provide implementations of abstract methods for the messages they actually "care about" (i.e., handle).

However, in my application, the majority of messages can be segmented into distinct groups where each visitor only handles one of those groups. There are a few messages handled by all (or more than one) visitor, but this is the exception.

For example, assume I have Visitor1, Visitor2, and Visitor3. Below is a list of the messages "handled" by each visitor:

  • Visitor1
    • MessageA
    • MessageB
    • MessageC
  • Visitor2
    • MessageA
    • MessageD
    • MessageE
  • Visitor3
    • MessageA
    • MessageF
    • MessageG

Of course, there could be multiple instances of the same type of visitor on the system.

Which could be implemented in C++ as follows:

class MessageA;
class MessageB;
class MessageC;
class MessageD;
class MessageE;
class MessageF;
class MessageG;

class AbstractVisitor {
public:
    virtual bool visit(const MessageA& msg) { return false; }
    virtual bool visit(const MessageB& msg) { return false; }
    virtual bool visit(const MessageC& msg) { return false; }
    virtual bool visit(const MessageD& msg) { return false; }
    virtual bool visit(const MessageE& msg) { return false; }
    virtual bool visit(const MessageF& msg) { return false; }
    virtual bool visit(const MessageG& msg) { return false; }
};

class Visitor1 : public AbstractVisitor {
public:
    bool visit(const MessageA& msg) override { return true; }
    bool visit(const MessageB& msg) override { return true; }
    bool visit(const MessageC& msg) override { return true; }
};

class Visitor2 : public AbstractVisitor {
public:
    bool visit(const MessageA& msg) override { return true; }
    bool visit(const MessageD& msg) override { return true; }
    bool visit(const MessageE& msg) override { return true; }
};

class Visitor3 : public AbstractVisitor {
public:
    bool visit(const MessageA& msg) override { return true; }
    bool visit(const MessageF& msg) override { return true; }
    bool visit(const MessageG& msg) override { return true; }
};


class AbstractMessage {
public:
    virtual bool accept(AbstractVisitor& v) = 0
};

class MessageA : public AbstractMessage {
public:
    bool accept(AbstractVisitor& v) { v.visit(*this); }
};

//...

The maintenance of this code isn't too bad since it only requires the modification of the AbstractVisitor interface each time a new message is added. However, it still seems a bit problematic to have to keep a single file with one big list of all the possible messages.

The second problem I see is that each visitor has a large vtable containing all the possible visit() methods for every type of message even when each particular visitor only uses a few of them.

The third problem is really a result of the other two problems in that, for example, MessageB and MessageF are really not related except for the common interface. They represent two completely different operations ALWAYS handled by two completely different endpoints.

Since I'm sure this type of problem occurs relatively often. My question is, is this (generally) the most optimal design in this case? Or is there a better pattern for something like this? Are large vtables really a concern (as far as I know, that's one pointer per virtual method times the number of concrete implementations)?

2
  • yeah the visitor pattern is a bit rubbish. instead of overriding, visit the abstract message, examine its type and have an action conditional on the type.
    – Ewan
    Commented Nov 12, 2021 at 1:35
  • Keep in mind that the GoF book which popularised this pattern was written in 1995; some of the patterns it describes would have filled in for features that were missing from popular programming languages at the time, but languages and ways of thinking have evolved a great deal in the past 25 years; the visitor pattern is one of many in that book which doesn't stand the test of time in the face of evolved ways of thinking about program structure as well as the evolved programming languages themselves. Commented Nov 12, 2021 at 6:12

2 Answers 2

3

So the visitor pattern is intended to be a way to build an algorithm without modifying the classes you are visiting. This really isn't an example where the visitor pattern works. The fact you have to add more methods when you have new messages makes this potentially a problem to maintain.

Instead you may simply want to look at queueing patterns. In several platforms, there is a concept of Publish/Subscribe for a message queue. The sender of the message only needs to worry about publishing the message. The subscriber would register the types of messages it cares about, and the queue sends that message to the receiver object.

In this case your Pub/Sub queue would have an interface somewhat like this (assuming messages have a shared base class):

public AbstractMessageQueue {
    public:
        void publish(const Message& msg) = 0
        void subscribe(const Handler& hnd) = 0
}

public Handler {
    public:
        bool handles(const Message& msg) { return false; }
        void receive(const Message& msg) = 0
}

I wouldn't be surprised if there wasn't something already implemented in the Boost libraries for "Event Buss", "Message Queue" or something of that nature.

The message queue only needs to iterate through the subscribers and if the handles(msg) returns true, then it will call that subscriber's receive(msg).

If all messages have the same base class, then adding new message types is trivial. Additionally, having handlers only interested in a handful of types would also be trivial.

Of course you could have these all use templates for a strongly typed queue that only handles one message type. In that case the handles(msg) call wouldn't be necessary, and you simply deliver the message to all subscribers.

4
  • I'll be honest that I haven't worked with C++ in a very long time, but I've worked in other languages where this is a common way to handle message delivery. Commented Nov 12, 2021 at 3:25
  • 1
    I think Boost.Signals2 might be the closest thing that C++ has to a standard way of implementing a pub/sub pattern - stackoverflow.com/a/7523319/1301901 Commented Nov 12, 2021 at 6:45
  • @BerinLoritsch In this type of architecture, how do the handlers take care of the down conversion from the abstract "Message" class to concrete class without explicit casting? (Since I was under the impression its always best to avoid casting in this way) Commented Nov 12, 2021 at 14:29
  • I alluded to it in the latter half of the answer. You can use strongly typed queues with templates/generics, but that does require a separate queue for each message type. Outside of that I don't think you can really get away from downcasting if different messages are sent on the same queue. Commented Nov 12, 2021 at 22:03
0

Mediator Design Pattern

This is a slight variation on Berin's answer. -- This implementation of Mediator ensures that member-functions (methods) which handle messages are accepting the concrete type of the message, and avoids using any RTTI (i.e. dynamic_cast or type-id).

(Based on a .NET library called MediatR: https://github.com/jbogard/MediatR/wiki#notifications)

(This also isn't using Boost.Signals2 that I mentioned in my other comment, although could potentially be improved using that).

Events

Firstly, using C++20 concepts to force all messages to have an associated ::type string so that the Mediator can identify message types using the string instead of typeid or some other RTTI mechanism:

template <typename T>
concept MediatorMessage = requires(T) {
    {T::type} -> std::convertible_to<const std::string>;
};

This forces all classes/structs used as messages/events to define either of these with a suitable value to discriminate against each message type:

  • static const char* type; or
  • static const std::string type;

Handler Interface

The constraint can be used to create a base type that all of the message 'listener' classes would inherit from for each message they want to subscribe to:

template <MediatorMessage M>
class NotificationHandler {
public:
    virtual void handle(const M& notification) = 0;
};

Mediator Class

As per the C# implementation (which benefits from .NET's built-in reference counting and weak references for unsubscribing handlers), the Mediator could track weak_ptr references to its listeners:

class Mediator {
    template <MediatorMessage M>
    class NotificationDespatcher {
        std::list<std::weak_ptr<NotificationHandler<M>>> handlers;
    public:
        NotificationDespatcher() : handlers{} {};
        void despatch(M msg);
        void add(const std::weak_ptr<NotificationHandler<M>>& handler) { handlers.push_back(handler); }
    };
    std::unordered_map<std::string, std::any> despatchers;
    template <MediatorMessage M> NotificationDespatcher<M>& getOrAddDespatcher();
public:
    Mediator() : despatchers{} {};
    template <MediatorMessage M> void publish(const M& msg) { getOrAddDespatcher<M>().despatch(msg); }
    template <MediatorMessage M> void subscribe(const std::weak_ptr<NotificationHandler<M>>& handler) { getOrAddDespatcher<M>().add(handler); }
};

template<MediatorMessage M> Mediator::NotificationDespatcher<M>& Mediator::getOrAddDespatcher() {
    if (!despatchers.contains(M::type)) {
        despatchers[M::type] = NotificationDespatcher<M>{};
    }
    return std::any_cast<NotificationDespatcher<M>&>(despatchers[M::type]);
}

template<MediatorMessage M> void Mediator::NotificationDespatcher<M>::despatch(M msg) {
    auto iter = handlers.begin();
    while (iter != handlers.end()) {
        if (auto handler = iter->lock()) {
            handler->handle(msg);
            iter++;
        }
        else 
            iter = handlers.erase(iter);
    }
}

To allow multiple subscribers, an internal NotificationDespatcher<M> stores a list<weak_ptr<NotificationHandler<M>>> -- the weak_ptr helps with auto-unsubscribe and avoids dangling references/pointers. (But it means the handler must be owned by a shared_ptr)

Mediator stores an unordered_map<string, any> keyed by the ::type used in the concept to segregate these despatchers for each message type without RTTI.

(If multiple messages ended up using the same value for ::type then any_cast would throw an exception once it detected an attempt to publish or subscribe with the wrong type).

Example messages

Messages only need a ::type field which is convertible to std::string:

struct FooEvent {
    static const char* type;
};
const char* FooEvent::type{ "FooEvent" };

struct BarEvent {
    static const std::string type;
};
const std::string BarEvent::type{ "BarEvent" };

Concrete Handler

The handler derives from NotificationHandler<M> for each message/event it supports and implements the void handle(M) member-function:

class FooBarListener 
    : public NotificationHandler<FooEvent>
    , public NotificationHandler<BarEvent> {
public:
    void handle(const FooEvent& notification) {
        std::cout << "FooBarListener received FooEvent notification" << std::endl;
    }

    void handle(const BarEvent& notification) {
        std::cout << "FooBarListener received BarEvent notification" << std::endl;
    }
};

main()

An example program which illustrates publish, subscribe and auto-unsubscribe after the handler is destroyed:

int main() {
    Mediator mediator;

    {
        auto listener = std::make_shared<FooBarListener>();
        mediator.subscribe<FooEvent>(listener);
        mediator.subscribe<BarEvent>(listener);
        mediator.publish(FooEvent{}); // listener is listening
        mediator.publish(BarEvent{});
    } // listener is destroyed.

    mediator.publish(FooEvent{}); // Nothing is listening!
    mediator.publish(BarEvent{});
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.