访问者模式是否比 RTTI 的受控使用更好?
Is the visitor pattern a better option than controlled use of RTTI?
我经常发现自己试图使用 boost/QT 信号来解耦对象。执行此操作的天真方法是针对我想要通信的每个具体类型,我创建一个新的信号和插槽签名并连接所有依赖对象。这导致了访问者模式,理想情况下我想发出一个访问者并让所有监听的 classes 接收访问者并执行一个动作。界面将如下所示:
class IVisitor
{
public:
Visit(IListener* Listener);
Visit(ConcreteListener1* Listener);
Visit(ConcreteListener2* Listener);
//And so on from here
};
同样,如果我想要多个命令,我需要吸引多个访问者:
class IListener
{
public:
Visit(IVisitor* Listener);
Visit(ConcreteVisitor1* Listener);
Visit(ConcreteVisitor2* Listener);
//And so on from here
};
对我来说,这似乎违反了 open/closed 原则,因为每次我想连接一个新的侦听器或实现一个新的访问者时,我总是必须返回更新我的接口。理想情况下,这将使用双重调度并且能够保持基本 classes 不变,只有派生的 classes 根据他们使用基本 class 接口接受的访问者进行更改,如果没有专门的界面存在。我知道这在 C++ 中是不可能的,因为函数重载和参数类型基于编译时信息。
一般来说,这都是关于在不支持它的程序中重新实现多重分派。
我看过很多关于访问者模式的争论,它似乎是人们使用和讨厌的模式。看来它的访客模式还是dynamic_cast?我已经实现了一个模板化的助手 class,它在使用 dynamic_cast 时自动执行可怕的 if-else 逻辑以实现更好的维护。所以我的问题是......当逻辑维护基本上自动化时,使用 dynamic_cast 的陷阱是否比针对访问者模式提到的陷阱更糟糕?
编辑:
std::visit 似乎确实是解决这个多重分派问题的好方法。我能够使用以下一个衬里创建一个简单的消息传递系统:
std::visit(overloaded{ [&](auto arg) {Listener->Recieve(arg); } }, pCommand->AsVariant());
使用访客模式,
当一个新的监听器被添加到 IVisitor
时,您可以保证现有的访问者必须处理这个新的监听器。
对于简单的 dynamic_cast
,未处理的侦听器更有可能。
取决于如何(每个,(所以没有统一的行为))类 实现它,你可能会抛出不受支持的侦听器,或者回退到 "default implementation"(什么都不做)。
dynamic_cast
的替代方法是 std::variant
用法,对于访问者,需要了解所有听众类型。
std::variant
有一个 std::visit
甚至可以进行多重调度 :-)
所以,类似于:
using ListenerVariant = std::variant<ConcreteListener1*, ConcreteListener2* /*..*/>;
class IListener
{
public:
virtual ListenerVariant AsVariant() = 0;
// ...
};
然后
std::visit(overloaded{[](ConcreteListener1* l){/*..*/},
[](ConcreteListener2* l){/*..*/}},
listener.AsVariant());
您可以保证所有情况都得到处理,(您甚至可以回退)。
我经常发现自己试图使用 boost/QT 信号来解耦对象。执行此操作的天真方法是针对我想要通信的每个具体类型,我创建一个新的信号和插槽签名并连接所有依赖对象。这导致了访问者模式,理想情况下我想发出一个访问者并让所有监听的 classes 接收访问者并执行一个动作。界面将如下所示:
class IVisitor
{
public:
Visit(IListener* Listener);
Visit(ConcreteListener1* Listener);
Visit(ConcreteListener2* Listener);
//And so on from here
};
同样,如果我想要多个命令,我需要吸引多个访问者:
class IListener
{
public:
Visit(IVisitor* Listener);
Visit(ConcreteVisitor1* Listener);
Visit(ConcreteVisitor2* Listener);
//And so on from here
};
对我来说,这似乎违反了 open/closed 原则,因为每次我想连接一个新的侦听器或实现一个新的访问者时,我总是必须返回更新我的接口。理想情况下,这将使用双重调度并且能够保持基本 classes 不变,只有派生的 classes 根据他们使用基本 class 接口接受的访问者进行更改,如果没有专门的界面存在。我知道这在 C++ 中是不可能的,因为函数重载和参数类型基于编译时信息。
一般来说,这都是关于在不支持它的程序中重新实现多重分派。
我看过很多关于访问者模式的争论,它似乎是人们使用和讨厌的模式。看来它的访客模式还是dynamic_cast?我已经实现了一个模板化的助手 class,它在使用 dynamic_cast 时自动执行可怕的 if-else 逻辑以实现更好的维护。所以我的问题是......当逻辑维护基本上自动化时,使用 dynamic_cast 的陷阱是否比针对访问者模式提到的陷阱更糟糕?
编辑:
std::visit 似乎确实是解决这个多重分派问题的好方法。我能够使用以下一个衬里创建一个简单的消息传递系统:
std::visit(overloaded{ [&](auto arg) {Listener->Recieve(arg); } }, pCommand->AsVariant());
使用访客模式,
当一个新的监听器被添加到 IVisitor
时,您可以保证现有的访问者必须处理这个新的监听器。
对于简单的 dynamic_cast
,未处理的侦听器更有可能。
取决于如何(每个,(所以没有统一的行为))类 实现它,你可能会抛出不受支持的侦听器,或者回退到 "default implementation"(什么都不做)。
dynamic_cast
的替代方法是 std::variant
用法,对于访问者,需要了解所有听众类型。
std::variant
有一个 std::visit
甚至可以进行多重调度 :-)
所以,类似于:
using ListenerVariant = std::variant<ConcreteListener1*, ConcreteListener2* /*..*/>;
class IListener
{
public:
virtual ListenerVariant AsVariant() = 0;
// ...
};
然后
std::visit(overloaded{[](ConcreteListener1* l){/*..*/},
[](ConcreteListener2* l){/*..*/}},
listener.AsVariant());
您可以保证所有情况都得到处理,(您甚至可以回退)。