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

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


当前回答

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

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

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

其他回答

Quick description of the visitor pattern. The classes that require modification must all implement the 'accept' method. Clients call this accept method to perform some new action on that family of classes thereby extending their functionality. Clients are able to use this one accept method to perform a wide range of new actions by passing in a different visitor class for each specific action. A visitor class contains multiple overridden visit methods defining how to achieve that same specific action for every class within the family. These visit methods get passed an instance on which to work.

当你考虑使用它的时候

When you have a family of classes you know your going to have to add many new actions them all, but for some reason you are not able to alter or recompile the family of classes in the future. When you want to add a new action and have that new action entirely defined within one the visitor class rather than spread out across multiple classes. When your boss says you must produce a range of classes which must do something right now!... but nobody actually knows exactly what that something is yet.

一种看待它的方法是,访问者模式是一种让客户端向特定类层次结构中的所有类添加额外方法的方式。

当您有一个相当稳定的类层次结构,但您对需要对该层次结构做什么有不断变化的需求时,它是有用的。

经典的例子是编译器之类的。抽象语法树(AST)可以准确地定义编程语言的结构,但是您可能希望在AST上执行的操作将随着项目的进展而变化:代码生成器、漂亮的打印机、调试器、复杂性度量分析。

如果没有访问者模式,每次开发人员想要添加一个新特性时,他们都需要将该方法添加到基类中的每个特性中。当基类出现在单独的库中或由单独的团队生成时,这尤其困难。

(我听说访问者模式与良好的OO实践相冲突,因为它将数据的操作从数据中移开了。访问者模式在正常的OO实践失败的情况下非常有用。)

基于@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);
        }
    }
}

在我看来,使用访问者模式或直接修改每个元素结构添加新操作的工作量大致相同。此外,如果我要添加新的元素类,比如Cow,操作接口将受到影响,并且这将传播到所有现有的元素类,因此需要重新编译所有元素类。那么重点是什么呢?

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原则。在研究人员进行的一项调查中,访问者被证明是一个有争议的模式(关于它的有用性没有达成共识)。一些实验甚至表明,它并没有使代码更容易维护。