我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。

作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。


当前回答

你困惑的原因可能是来客是一个致命的用词不当。许多(杰出的)程序员都曾遇到过这个问题。它实际做的是用本身不支持它的语言(大多数语言不支持)实现双重调度。


1)我最喜欢的例子是《Effective c++》的作者Scott Meyers,他称这是他最重要的c++啊哈之一!的时刻。

其他回答

我不理解这种模式,直到我看到bob叔叔的文章和评论。 考虑下面的代码:

public class Employee
{
}

public class SalariedEmployee : Employee
{
}

public class HourlyEmployee : Employee
{
}

public class QtdHoursAndPayReport
{
    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        foreach (Employee e in employees)
        {
            if (e is HourlyEmployee he)
                PrintReportLine(he);
            if (e is SalariedEmployee se)
                PrintReportLine(se);
        }
    }

    public void PrintReportLine(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hours");
    }
    public void PrintReportLine(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    }
}

class Program
{
    static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }
}

虽然它看起来很好,因为它确认了单一责任,但它违反了开放/封闭原则。每次你有新的员工类型,你将不得不添加如果与类型检查。如果你不知道,在编译时你永远也不会知道。

使用访问者模式,你可以让你的代码更干净,因为它不违反开放/关闭原则,也不违反单一责任。如果你忘记实现visit,它将不会编译:

public abstract class Employee
{
    public abstract void Accept(EmployeeVisitor v);
}

public class SalariedEmployee : Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public class HourlyEmployee:Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public interface EmployeeVisitor
{
    void Visit(HourlyEmployee he);
    void Visit(SalariedEmployee se);
}

public class QtdHoursAndPayReport : EmployeeVisitor
{
    public void Visit(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hourly");
        // generate the line of the report.
    }
    public void Visit(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    } // do nothing

    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        QtdHoursAndPayReport v = new QtdHoursAndPayReport();
        foreach (var emp in employees)
        {
            emp.Accept(v);
        }
    }
}

class Program
{

    public static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }       
}  
}

神奇的是,虽然v.Visit(this)看起来一样,但实际上是不同的,因为它调用不同的访问者重载。

感谢@Federico A. Ramponi的精彩解释,我只是在java版本中做了这个。希望对大家有所帮助。

正如@Konrad Rudolph指出的那样,这实际上是使用两个具体实例一起确定运行时方法的双重分派。

因此,实际上,只要正确定义了操作接口,就不需要为操作执行器创建公共接口。

import static java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showTheHobby(food);
        Katherine katherine = new Katherine();
        katherine.presentHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void embed(Katherine katherine);
}


class Hearen {
    String name = "Hearen";
    void showTheHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine {
    String name = "Katherine";
    void presentHobby(Hobby hobby) {
        hobby.embed(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void embed(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}

正如您所期望的那样,公共接口将为我们带来更多的清晰度,尽管它实际上并不是这个模式的基本部分。

import static java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showHobby(food);
        Katherine katherine = new Katherine();
        katherine.showHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void insert(Katherine katherine);
}

abstract class Person {
    String name;
    protected Person(String n) {
        this.name = n;
    }
    abstract void showHobby(Hobby hobby);
}

class Hearen extends  Person {
    public Hearen() {
        super("Hearen");
    }
    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine extends Person {
    public Katherine() {
        super("Katherine");
    }

    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void insert(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}

我不太熟悉来客模式。看看我做得对不对。假设你有一个动物等级

class Animal {  };
class Dog: public Animal {  };
class Cat: public Animal {  };

(假设它是一个具有良好接口的复杂层次结构。)

现在我们想要向层次结构添加一个新操作,即我们想要每个动物发出它的声音。既然层次结构这么简单,你可以直接用多态性来实现:

class Animal
{ public: virtual void makeSound() = 0; };

class Dog : public Animal
{ public: void makeSound(); };

void Dog::makeSound()
{ std::cout << "woof!\n"; }

class Cat : public Animal
{ public: void makeSound(); };

void Cat::makeSound()
{ std::cout << "meow!\n"; }

但是按照这种方式进行,每次想要添加操作时,都必须修改到层次结构中每个类的接口。现在,假设您对原始界面感到满意,并且希望对其进行尽可能少的修改。

访问者模式允许您在合适的类中移动每个新操作,并且您只需要扩展层次结构的接口一次。我们开始吧。首先,我们定义了一个抽象操作(GoF中的“Visitor”类),它对层次结构中的每个类都有一个方法:

class Operation
{
public:
    virtual void hereIsADog(Dog *d) = 0;
    virtual void hereIsACat(Cat *c) = 0;
};

然后,我们修改层次结构以接受新的操作:

class Animal
{ public: virtual void letsDo(Operation *v) = 0; };

class Dog : public Animal
{ public: void letsDo(Operation *v); };

void Dog::letsDo(Operation *v)
{ v->hereIsADog(this); }

class Cat : public Animal
{ public: void letsDo(Operation *v); };

void Cat::letsDo(Operation *v)
{ v->hereIsACat(this); }

最后,我们实现了实际的操作,没有修改Cat和Dog:

class Sound : public Operation
{
public:
    void hereIsADog(Dog *d);
    void hereIsACat(Cat *c);
};

void Sound::hereIsADog(Dog *d)
{ std::cout << "woof!\n"; }

void Sound::hereIsACat(Cat *c)
{ std::cout << "meow!\n"; }

现在,您可以在不修改层次结构的情况下添加操作。 下面是它的工作原理:

int main()
{
    Cat c;
    Sound theSound;
    c.letsDo(&theSound);
}

虽然我知道如何做,何时做,但我一直不明白为什么。为了对有c++等语言背景的人有所帮助,请仔细阅读本文。

对于懒惰的人,我们使用访问者模式,因为“虽然在c++中虚函数是动态分派的,但函数重载是静态完成的”。

或者,换句话说,当你传递一个实际绑定到ApolloSpacecraft对象的飞船引用时,确保CollideWith(ApolloSpacecraft&)被调用。

class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class ExplodingAsteroid : public Asteroid {
public:
  virtual void CollideWith(SpaceShip&) {
    cout << "ExplodingAsteroid hit a SpaceShip" << endl;
  }
  virtual void CollideWith(ApolloSpacecraft&) {
    cout << "ExplodingAsteroid hit an ApolloSpacecraft" << endl;
  }
}

正如Konrad Rudolph已经指出的,它适用于需要双重调度的情况

这里有一个例子,以显示我们需要双重调度&访客如何帮助我们这样做的情况。

例子:

假设我有三种类型的移动设备——iPhone, Android, Windows mobile。

这三种设备都安装了蓝牙收音机。

让我们假设蓝牙收音机可以来自2个独立的原始设备制造商-英特尔和博通。

为了使这个例子与我们的讨论相关,我们还假设Intel电台公开的api与Broadcom电台公开的api是不同的。

这是我的类的样子

现在,我想介绍一个操作——移动设备蓝牙开关。

它的函数特征应该是这样的

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

因此,根据正确的设备类型和蓝牙收音机的正确类型,它可以通过调用适当的步骤或算法打开。

原则上,它变成了一个3 × 2的矩阵,在这里,我试图根据所涉及的对象的正确类型来进行正确的操作。

取决于两个参数类型的多态行为。

现在,访问者模式可以应用于这个问题。灵感来自维基百科页面上的说明——“本质上,访问者允许在不修改类本身的情况下向类族添加新的虚函数;相反,创建一个实现虚函数的所有适当专门化的访问者类。访问者将实例引用作为输入,通过双重调度实现目标。

由于3x2矩阵,双重调度是必要的

这是如何设置看起来像-

我写了一个例子来回答另一个问题,代码和它的解释在这里被提到。