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

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


当前回答

这里的每个人都是对的,但我认为它没有解决“何时”这个问题。首先,从设计模式:

Visitor允许您定义一个新的 操作而不改变类 它所作用的元素。

现在,让我们考虑一个简单的类层次结构。我有类1、2、3和4,方法A、B、C和d。把它们像电子表格一样放出来:类是行,方法是列。

现在,面向对象设计假设您更有可能增长新类而不是新方法,因此添加更多行,可以说,更容易。您只需添加一个新类,指定该类中的不同之处,并继承其余部分。

但是,有时类是相对静态的,但是您需要频繁地添加更多的方法——添加列。面向对象设计的标准方法是将这样的方法添加到所有类中,这可能成本很高。访问者模式使这变得很容易。

顺便说一下,这就是Scala模式匹配想要解决的问题。

其他回答

使用访问者模式至少有三个很好的理由:

减少代码的增殖,当数据结构发生变化时,代码只会略有不同。 将相同的计算应用于多个数据结构,而不改变实现计算的代码。 在不更改遗留代码的情况下向遗留库添加信息。

请看看我写的一篇关于这方面的文章。

基于@Federico A. Ramponi的精彩回答。

想象一下你有这样的层次结构:

public interface IAnimal
{
    void DoSound();
}

public class Dog : IAnimal
{
    public void DoSound()
    {
        Console.WriteLine("Woof");
    }
}

public class Cat : IAnimal
{
    public void DoSound(IOperation o)
    {
        Console.WriteLine("Meaw");
    }
}

如果你需要在这里添加一个“Walk”方法会发生什么?这对整个设计来说是痛苦的。

同时,添加“Walk”方法会生成新的问题。那"吃"和"睡"呢?我们真的必须为我们想要添加的每个新动作或操作添加一个新方法到Animal层次结构中吗?这很难看,但最重要的是,我们永远无法关闭Animal界面。因此,使用访问者模式,我们可以在不修改层次结构的情况下向层次结构添加新方法!

因此,只需检查并运行这个c#示例:

using System;
using System.Collections.Generic;

namespace VisitorPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var animals = new List<IAnimal>
            {
                new Cat(), new Cat(), new Dog(), new Cat(), 
                new Dog(), new Dog(), new Cat(), new Dog()
            };

            foreach (var animal in animals)
            {
                animal.DoOperation(new Walk());
                animal.DoOperation(new Sound());
            }

            Console.ReadLine();
        }
    }

    public interface IOperation
    {
        void PerformOperation(Dog dog);
        void PerformOperation(Cat cat);
    }

    public class Walk : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Dog walking");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Cat Walking");
        }
    }

    public class Sound : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Woof");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Meaw");
        }
    }

    public interface IAnimal
    {
        void DoOperation(IOperation o);
    }

    public class Dog : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }

    public class Cat : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }
}

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


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

Cay Horstmann在他的OO设计和模式书中有一个很好的例子,说明了在哪里应用Visitor。他总结了这个问题:

复合对象通常具有复杂的结构,由单个元素组成。有些元素可能也有子元素. ...元素上的操作访问它的子元素,对它们应用该操作,并将结果组合在一起. ...然而,向这样的设计中添加新的操作并不容易。

不容易的原因是,操作是在结构类本身中添加的。例如,假设你有一个文件系统:

下面是一些我们可能想用这个结构实现的操作(功能):

显示节点元素的名称(一个文件列表) 显示计算出的节点元素大小(其中目录的大小包括其所有子元素的大小) 等。

You could add functions to each class in the FileSystem to implement the operations (and people have done this in the past as it's very obvious how to do it). The problem is that whenever you add a new functionality (the "etc." line above), you might need to add more and more methods to the structure classes. At some point, after some number of operations you've added to your software, the methods in those classes don't make sense anymore in terms of the classes' functional cohesion. For example, you have a FileNode that has a method calculateFileColorForFunctionABC() in order to implement the latest visualization functionality on the file system.

The Visitor Pattern (like many design patterns) was born from the pain and suffering of developers who knew there was a better way to allow their code to change without requiring a lot of changes everywhere and also respecting good design principles (high cohesion, low coupling). It's my opinion that it's hard to understand the usefulness of a lot of patterns until you've felt that pain. Explaining the pain (like we attempt to do above with the "etc." functionalities that get added) takes up space in the explanation and is a distraction. Understanding patterns is hard for this reason.

Visitor allows us to decouple the functionalities on the data structure (e.g., FileSystemNodes) from the data structures themselves. The pattern allows the design to respect cohesion -- data structure classes are simpler (they have fewer methods) and also the functionalities are encapsulated into Visitor implementations. This is done via double-dispatching (which is the complicated part of the pattern): using accept() methods in the structure classes and visitX() methods in the Visitor (the functionality) classes:

这个结构允许我们添加新的功能,这些功能作为具体的访问者在结构上工作(不需要改变结构类)。

例如,PrintNameVisitor实现目录列表功能,PrintSizeVisitor实现具有大小的版本。我们可以想象有一天有一个以XML生成数据的“ExportXMLVisitor”,或者另一个以JSON生成数据的访问者,等等。我们甚至可以让一个访问者使用图形化语言(如DOT)显示我的目录树,然后用另一个程序进行可视化。

最后要注意的是:Visitor的双重分派的复杂性意味着它更难以理解、编码和调试。简而言之,它有很高的极客因素,违背了KISS原则。在研究人员进行的一项调查中,访问者被证明是一个有争议的模式(关于它的有用性没有达成共识)。一些实验甚至表明,它并没有使代码更容易维护。

感谢@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");
    }
}