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

时间:2018-05-17 09:23:14

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

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

onCreate

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

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

调用element.accept(visitor)调用visitor.visit(element)时有什么有用的信息?这使得访问者模式的使用变得繁琐,并且在Element类的所有层次结构中都需要额外的代码。

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

1 个答案:

答案 0 :(得分:0)

我一直对访客模式感到困惑,而且我一直试图在互联网上找到解释,而这些解释让我更加困惑。现在我意识到了为什么需要访问者模式以及实现方式的原因,所以这里是:

解决Double Dispatch问题所需的访客模式。

单个调度 - 当您有一个类层次结构并且在此层次结构中有一个具体类的实例时 并且您想为此实例调用适当的方法。这是通过函数重写来解决的(使用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.
        }
    }
}

现在:如何在RUN-TIME中调用处理此特定Animal和此特定Invoker的9种(或其他)特定方法之一?

Double 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的实例为BirdInvoker的实例为FeedingInvoker

然后,由于虚拟功能表Bird::accept(Invoker&)将被调用,而后者将运行invoker.doTheJob(Bird&)。 由于Invoker实例为FeedingInvoker,虚拟函数表将使用FeedingInvoker::accept(Bird&)进行此调用。

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

为什么访客模式好?

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

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

  3. 访客模式优雅地实现了面向对象设计的开放/封闭原则:系统应该对扩展开放并且不接受修改。

    (在经典访客模式中Invoker将被VisitordoTheJob()替换为visit(),但对我而言,这些名称实际上并未反映出以下事实:一些工作是在元素上完成的。)