为什么在访客模式中需要accept(),为什么不能直接调用visitor.visit()?

And*_*iov 4 c++ design-patterns double-dispatch visitor-pattern

我正在修改我前段时间使用的访客模式。我们有基类 Element,它具有虚方法accept(Visitor),并且该方法在继承自 Element 的所有类中被重写。在任何派生类中accept()所做的就是调用visitor->visit(*this)。现在,当客户端运行代码时,他/她会执行以下操作:

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

为什么客户端不能像这样调用visitor->visit(element):

Visitor& theVisitor = *new ConcreteVisitor();    
for_each(elements.begin(), elements.end(), [&theVisitor](Element& e) { theVisitor.visit(e); });
Run Code Online (Sandbox Code Playgroud)

调用 element.accept(visitor) 进而调用 guest.visit(element) 时有哪些有用的信息?这使得访问者模式的使用变得很麻烦,并且需要在 Element 类的所有层次结构中添加额外的代码。

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

And*_*iov 6

我长期以来对访客模式感到困惑,并且一直试图在互联网上寻找解释,这些解释让我更加困惑。现在我明白了为什么需要访问者模式以及它的实现方式,所以这里是:

需要访问者模式来解决双重调度问题。

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

双重调度是指当​​您有两个类层次结构,并且您有一个层次结构中的一个具体类实例另一个层次结构中的一个具体类实例,并且您想要调用适当的方法来为这两个特定实例完成工作时。

让我们看一个例子。

第一等级:动物。基数:Animal,派生:Fish, Mammal, Bird。第二类层次结构:调用者。基础: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
}
Run Code Online (Sandbox Code Playgroud)

或者:

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.
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在:如何在运行时调用 9 个(或其他)特定方法之一来处理这个特定的Animal和这个特定的Invoker

双重派遣来了。您绝对需要从第一类层次结构中调用一个虚函数,并从第二类层次结构中调用一个虚函数。

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

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

这是实现(你可以复制并运行,我用g++编译器测试过):

访客.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
Run Code Online (Sandbox Code Playgroud)

访客.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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

上述代码的输出:

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
Run Code Online (Sandbox Code Playgroud)

那么,当客户端获得 的实例并调用Animal的实例时会发生什么?Invokeranimal.accept(invoker)

Animal假设is的实例和isBird的实例。InvokerFeedingInvoker

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

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

为什么访客模式好?

  1. 客户端不需要同时依赖 Animals 和 Invokers 的复杂类层次结构。

  2. Insect如果需要添加新的具体动物(例如),则Animal无需更改现有的层次结构。我们只需要添加:doTheJob(Insect& insect)Invoker所有派生调用者。

访问者模式优雅地实现了面向对象设计的开放/封闭原则:系统应该对扩展开放,对修改封闭。

(在经典的访问者模式中Invoker,将被替换为VisitordoTheJob()by visit(),但对我来说,这些名称实际上并不反映某些工作是在元素上完成的事实)。