I often find myself trying to decouple objects using the boost/QT signals. The naive way to do this is for each concrete type I want to communicate, I create a new signal and slot signature and wire up all the dependent objects. This leads to the visitor pattern, ideally I'd like to emit a visitor and have all the listening classes receive the visitor and perform an action. The interfaces would look like this:
class IVisitor
{
public:
Visit(IListener* Listener);
Visit(ConcreteListener1* Listener);
Visit(ConcreteListener2* Listener);
//And so on from here
};
Likewise if I want multiple commands I need to make multiple visitors:
class IListener
{
public:
Visit(IVisitor* Listener);
Visit(ConcreteVisitor1* Listener);
Visit(ConcreteVisitor2* Listener);
//And so on from here
};
To me this seems to violate the open/closed principle because I'm always having to go back an update my interfaces each time I want to connect a new listener or implement a new visitor. Ideally, this would use double dispatch and be able to leave the base classes intact with only the derived classes changing based on the visitors they accept using the base class interface if no specialized interface is present. I know this is not possible in C++ because function overloads and argument types are based on compile time information.
In general this is all about re-implementing multiple dispatch in a program that doesn't support it.
I have seen many debates about the visitor pattern and it seems like the pattern that people use and hate. It seems its visitor pattern or dynamic_cast? I have implemented a templated helper class that automates the dreaded if-else logic when using dynamic_cast for better maintenance. So my question is this... Are the pitfalls of using dynamic_cast worse than the pitfalls mentioned for the visitor pattern when maintenance of logic largely automated?
EDIT:
std::visit does indeed seem to be a great way to solve this issue of multiple dispatch. I was able to create a simple messaging system using the following one liner:
std::visit(overloaded{ [&](auto arg) {Listener->Recieve(arg); } }, pCommand->AsVariant());
With visitor pattern,
when a new listener is added to the IVisitor
, you have the guaranty that existing visitors have to handle that new listener.
With simple dynamic_cast
, not handled listeners is more probable.
depending how (each, (so no uniform behavior)) classes implement it, you might throw for unsupported listener, or fallback to "default implementation" (as do nothing).
Alternative to dynamic_cast
is std::variant
usage which, as for visitor, requires to know all listeners types.
std::variant
has a std::visit
which can even do multiple dispatch :-)
so, something like:
using ListenerVariant = std::variant<ConcreteListener1*, ConcreteListener2* /*..*/>;
class IListener
{
public:
virtual ListenerVariant AsVariant() = 0;
// ...
};
and then
std::visit(overloaded{[](ConcreteListener1* l){/*..*/},
[](ConcreteListener2* l){/*..*/}},
listener.AsVariant());
You have the guaranty that all cases are handled, (you can even have fallback).