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

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


当前回答

基于@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,操作接口将受到影响,并且这将传播到所有现有的元素类,因此需要重新编译所有元素类。那么重点是什么呢?

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

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

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

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

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

我发现下面的链接更容易:

在 http://www.remondo.net/visitor-pattern-example-csharp/我找到了一个例子,展示了一个模拟的例子,展示了什么是访问者模式的好处。这里有不同的Pill容器类:

namespace DesignPatterns
{
    public class BlisterPack
    {
        // Pairs so x2
        public int TabletPairs { get; set; }
    }

    public class Bottle
    {
        // Unsigned
        public uint Items { get; set; }
    }

    public class Jar
    {
        // Signed
        public int Pieces { get; set; }
    }
}

正如你在上面看到的,你BilsterPack包含对药片,所以你需要乘以2对的数量。此外,你可能会注意到瓶使用单位是不同的数据类型,需要强制转换。

所以在主要方法中,您可以使用以下代码计算药丸计数:

foreach (var item in packageList)
{
    if (item.GetType() == typeof (BlisterPack))
    {
        pillCount += ((BlisterPack) item).TabletPairs * 2;
    }
    else if (item.GetType() == typeof (Bottle))
    {
        pillCount += (int) ((Bottle) item).Items;
    }
    else if (item.GetType() == typeof (Jar))
    {
        pillCount += ((Jar) item).Pieces;
    }
}

注意,上面的代码违反了单一责任原则。这意味着如果添加新类型的容器,则必须更改主方法代码。同时,延长开关时间也是不好的做法。

通过引入以下代码:

public class PillCountVisitor : IVisitor
{
    public int Count { get; private set; }

    #region IVisitor Members

    public void Visit(BlisterPack blisterPack)
    {
        Count += blisterPack.TabletPairs * 2;
    }

    public void Visit(Bottle bottle)
    {
        Count += (int)bottle.Items;
    }

    public void Visit(Jar jar)
    {
        Count += jar.Pieces;
    }

    #endregion
}

您将计数药丸数量的责任转移到名为PillCountVisitor的类(并且我们删除了switch case语句)。这意味着每当您需要添加新的药丸容器类型时,您应该只更改PillCountVisitor类。还要注意IVisitor接口一般用于其他场景。

通过在药丸容器类中添加Accept方法:

public class BlisterPack : IAcceptor
{
    public int TabletPairs { get; set; }

    #region IAcceptor Members

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }

    #endregion
}

我们允许访客参观药丸容器课程。

最后,我们计算药丸计数使用以下代码:

var visitor = new PillCountVisitor();

foreach (IAcceptor item in packageList)
{
    item.Accept(visitor);
}

这意味着:每个药片容器允许PillCountVisitor访问者查看他们的药片计数。他知道怎么数你的药。

看着来访者。伯爵有药丸的价值。

在 http://butunclebob.com/ArticleS.UncleBob.IuseVisitor你看到了真实的场景,你不能使用多态性(答案)来遵循单一责任原则。事实上在:

public class HourlyEmployee extends Employee {
  public String reportQtdHoursAndPay() {
    //generate the line for this hourly employee
  }
}

reportQtdHoursAndPay方法用于报告和表示,这违反了单一责任原则。因此,最好利用访问者模式来解决这一问题。

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


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

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

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

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