c++ - 为什么我们需要访问者模式中的accept()以及为什么我们不能直接调用visitor.visit()?

标签 c++ design-patterns visitor double-dispatch

我正在修改我以前使用的Visitor模式。我们有基类Element,它具有虚拟方法accept(Visitor),并且此方法在从Element继承的所有类中均被覆盖。在任何派生类中,accept()所做的全部工作就是调用visitor-> visit(* this)。现在,当客户端运行代码时,他/她将执行以下操作:

Visitor& theVisitor = *new ConcreteVisitor();    
for_each(elements.begin(), elements.end(), [](Element& e) { e.accept(theVisitor));})

为什么客户不能像这样调用visitor-> visit(element):
Visitor& theVisitor = *new ConcreteVisitor();    
for_each(elements.begin(), elements.end(), [&theVisitor](Element& e) { theVisitor.visit(e); });

调用element.accept(visitor)继而调用visitor.visit(element)有什么有用的信息?这使得 guest 模式的使用变得麻烦,并且在元素类的所有层次结构中都需要额外的代码。

那么有人可以在这里解释accept()的好处吗?

最佳答案

长期以来,我一直对“访客”模式感到困惑,并且我一直试图在Internet上找到各种解释,而这些解释使我更加困惑。现在,我了解了为什么需要访问者模式的原因以及实现方式,所以这里是:

需要访客模式来解决“双重调度”问题。

单一调度-当您具有一个类层次结构并且在该层次结构中有一个具体类的实例时
并且您想为此实例调用适当的方法。这可以通过函数重写(使用C++中的虚拟函数表)解决。

双重调度是指当​​您有两个类层次结构,并且在一个层次结构中有一个具体类实例,而在另一个层次结构中有一个具体类实例时,您想调用将对这两个特定实例起作用的适当方法。

让我们来看一个例子。

头等舱等级:动物。基础:Animal,派生:FishMammalBird
第二类层次结构:调用程序。基础:Invoker,派生:MovementInvoker(移动动物),VoiceInvoker(使动物发声),FeedingInvoker(喂养动物)。

现在,对于每种特定的动物和每种特定的调用程序,我们只希望调用一个特定的函数来完成特定的工作(例如,喂鸟或给鱼听起来)。因此,总共有3x3 = 9个函数可以完成这些工作。

另一个重要的事情是:运行这9个功能中的每一个的客户都不想知道他或她手头有什么具体的Animal和什么具体的Invoker

因此,客户希望执行以下操作:

void act(Animal& animal, Invoker& invoker)
{
  // Do the job for this specific animal using this specific invoker
}

要么:
void act(vector<shared_ptr<Animal>>& animals, vector<shared_ptr<Invoker>>& invokers)
{
    for(auto& animal : animals)
    {
        for(auto& invoker : invokers)
        {
            // Do the job for this specific animal and invoker.
        }
    }
}

现在:如何在运行时调用用于处理此特定Animal和此特定Invoker的9种(或任何其他)特定方法之一?

这是双重 dispatch 。您绝对需要从第一类层次结构中调用一个虚函数,并从第二个层次结构中调用一个虚函数。

因此,您需要调用Animal的虚拟方法(使用虚拟函数表,它将在Animal类层次结构中找到具体实例的具体功能),还需要调用Invoker的虚拟方法(它将找到具体的调用者) 。

您必须调用两种虚拟方法。

因此,这里是实现(您可以复制并运行,我已使用g++编译器对其进行了测试):

visitor.h:
#ifndef __VISITOR__
#define __VISITOR__

struct Invoker; // forward declaration;

// -----------------------------------------//

struct Animal
{
    // The name of the function can be anything of course.
    virtual void accept(Invoker& invoker) = 0;
};

struct Fish : public Animal
{
    void accept(Invoker& invoker) override;
};

struct Mammal : public Animal
{
    void accept(Invoker& invoker) override;
};

struct Bird : public Animal
{
    void accept(Invoker& invoker) override;
};

// -----------------------------------------//

struct Invoker
{
  virtual void doTheJob(Fish&   fish)   = 0;
  virtual void doTheJob(Mammal& Mammal) = 0;
  virtual void doTheJob(Bird&   Bird)   = 0;
};

struct MovementInvoker : public Invoker
{
  void doTheJob(Fish&   fish)   override;
  void doTheJob(Mammal& Mammal) override;
  void doTheJob(Bird&   Bird)   override;
};

struct VoiceInvoker : public Invoker
{
  void doTheJob(Fish&   fish)   override;
  void doTheJob(Mammal& Mammal) override;
  void doTheJob(Bird&   Bird)   override;
};

struct FeedingInvoker : public Invoker
{
  void doTheJob(Fish&   fish)   override;
  void doTheJob(Mammal& Mammal) override;
  void doTheJob(Bird&   Bird)   override;
};

#endif

visitor.cpp:
#include <iostream>
#include <memory>
#include <vector>
#include "visitor.h"
using namespace std;

// -----------------------------------------//

void Fish::accept(Invoker& invoker)
{
    invoker.doTheJob(*this);
}

void Mammal::accept(Invoker& invoker)
{
    invoker.doTheJob(*this);
}

void Bird::accept(Invoker& invoker)
{
    invoker.doTheJob(*this);
}

// -----------------------------------------//

void MovementInvoker::doTheJob(Fish& fish)
{
    cout << "Make the fish swim" << endl;
}

void MovementInvoker::doTheJob(Mammal& Mammal)
{
    cout << "Make the mammal run" << endl;
}

void MovementInvoker::doTheJob(Bird& Bird)
{
    cout << "Make the bird fly" << endl;
}

// -----------------------------------------//

void VoiceInvoker::doTheJob(Fish& fish)
{
    cout << "Make the fish keep silence" << endl;
}

void VoiceInvoker::doTheJob(Mammal& Mammal)
{
    cout << "Make the mammal howl" << endl;
}

void VoiceInvoker::doTheJob(Bird& Bird)
{
    cout << "Make the bird chirp" << endl;
}

// -----------------------------------------//

void FeedingInvoker::doTheJob(Fish& fish)
{
    cout << "Give the fish some worms" << endl;
}

void FeedingInvoker::doTheJob(Mammal& Mammal)
{
    cout << "Give the mammal some milk" << endl;
}

void FeedingInvoker::doTheJob(Bird& Bird)
{
    cout << "Give the bird some seed" << endl;
}

int main()
{
    vector<shared_ptr<Animal>> animals = { make_shared<Fish>   (),
                                           make_shared<Mammal> (),
                                           make_shared<Bird>   () };

    vector<shared_ptr<Invoker>> invokers = { make_shared<MovementInvoker> (),
                                             make_shared<VoiceInvoker>    (),
                                             make_shared<FeedingInvoker>  () };

    for(auto& animal : animals)
    {
        for(auto& invoker : invokers)
        {
            animal->accept(*invoker);
        }
    }
}

上面代码的输出:
Make the fish swim
Make the fish keep silence
Give the fish some worms
Make the mammal run
Make the mammal howl
Give the mammal some milk
Make the bird fly
Make the bird chirp
Give the bird some seed

那么,当客户端获得一个Animal实例和一个Invoker实例并调用animal.accept(invoker)时,会发生什么?

假设Animal的实例为Bird,而Invoker的实例为FeedingInvoker

然后由于虚拟函数表Bird::accept(Invoker&)将被调用,该表将依次运行invoker.doTheJob(Bird&)
由于Invoker实例是FeedingInvoker,因此虚拟函数表将使用FeedingInvoker::accept(Bird&)进行此调用。

因此,我们进行了双重调度,并为BirdFeedingInvoker调用了正确的方法(9种可能的方法之一)。

为什么访客模式好?
  • 客户端不需要同时依赖于Animals和Invokers的复杂类层次结构。
  • 如果需要添加新的具体动物(例如Insect),则无需更改现有的Animal层次结构。
    我们只需要在doTheJob(Insect& insect)和所有派生的调用者中添加:Invoker

  • 访客模式很好地实现了面向对象设计的打开/关闭原则:系统应允许扩展,禁止修改。

    (在经典的访客模式中,Invoker会被Visitor替换,doTheJob()会被visit()替换,但对我来说,这些名称实际上并不反射(reflect)对元素执行某些操作的事实)。

    关于c++ - 为什么我们需要访问者模式中的accept()以及为什么我们不能直接调用visitor.visit()?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50387849/

    相关文章:

    c++ - 具有链接树结构的命令模式(使用指针)

    c++ - 使用非虚拟接口(interface)沿模板类向下转换

    c++ - 何时真正使用访客模式

    c++ - 我如何找到 C++ 编译器认为定义为常量的内容?

    c++ - qt qgraphicsscene线子类

    c++ - 无法管理链接 cpp 文件 - 未解析的外部符号

    java - 命令模式如何将发送者与接收者解耦?

    C++ 什么更快?静态成员函数还是普通成员函数?

    design-patterns - 在遵循“测试驱动开发”范例时,我是否应该从不使用静态方法,类和单例?

    c++ - 在std::variant中保存的类型上调用<<运算符?