我应该何时使用访客devise模式?

我一直在博客中看到访客模式的引用,但我必须承认,我只是不明白。 我阅读维基百科的文章模式 ,我理解它的机制,但我仍然感到困惑,因为我什么时候使用它。

作为一个刚刚获得装饰模式的人,现在已经看到这个模式,所以我希望能够直观地理解这个看起来很方便的模式。

我对访客模式不是很熟悉。 让我们看看我是否正确。 假设你有一个动物层次结构

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"; } 

但是以这种方式继续,每次你想添加一个操作时,你都必须修改接口到每一个层次的类。 现在,假设您对原始界面感到满意,并希望对其进行尽可能less的修改。

访问者模式允许您将每个新操作移动到合适的类中,并且只需要扩展层次结构的接口一次。 我们开始做吧。 首先,我们定义一个抽象操作(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); } 

你的困惑的原因可能是访客是一个致命的错误。 许多(着名的!)程序员已经在这个问题上蹒跚而行了。 它实际上做的是实现双重调度在本地不支持它的语言(大多数不)。


1)我最喜欢的例子是着名的“Effective C ++”作者Scott Meyers,他称这是他最重要的C ++ aha! 时刻 。

这里的每个人都是正确的,但我认为它不能解决“什么时候”。 首先,从devise模式:

访问者可以让你定义一个新的操作,而不用改变它所操作的元素的类。

现在,我们来考虑一个简单的类层次结构。 我有类1,2,3和4以及方法A,B,C和D.将它们排列成一个电子表格:类是行,方法是列。

现在,面向对象的devise假设你比新的方法更有可能增加新的类,所以增加更多的线,可以这么说,更容易。 您只需添加一个新类,指定该类中的不同之处,然后inheritance其他类。

有时候,类是相对静态的,但是你需要经常添加更多的方法 – 添加列。 OOdevise中的标准方法是将这些方法添加到所有类中,这可能是昂贵的。 访问者模式使这很容易。

顺便说一句,这是Scala的模式匹配打算解决的问题。

Visitordevise模式对目录树,XML结构或文档大纲等“recursion”结构非常有效。

Visitor对象以recursion结构访问每个节点:每个目录,每个XML标签,无论如何。 访客对象不循环结构。 相反,Visitor方法应用于结构的每个节点。

这是一个典型的recursion节点结构。 可能是一个目录或一个XML标签。 [如果你是一个Java的人,想象了许多额外的方法来build立和维护子列表。]

 class TreeNode( object ): def __init__( self, name, *children ): self.name= name self.children= children def visit( self, someVisitor ): someVisitor.arrivedAt( self ) someVisitor.down() for c in self.children: c.visit( someVisitor ) someVisitor.up() 

visit方法将Visitor对象应用于结构中的每个节点。 在这种情况下,这是一个自上而下的访问者。 您可以更改visit方法的结构以执行自下而上或其他顺序。

这是一个访问者的超类。 它被visit方法使用。 它“到达”结构中的每个节点。 由于visit方法down调用,访问者可以跟踪深度。

 class Visitor( object ): def __init__( self ): self.depth= 0 def down( self ): self.depth += 1 def up( self ): self.depth -= 1 def arrivedAt( self, aTreeNode ): print self.depth, aTreeNode.name 

子类可以做每个级别的计数节点,并累积节点列表,生成一个很好的path分层节号。

这是一个应用程序。 它构build了一个树结构, someTree树。 它创build一个VisitordumpNodes

然后它将dumpNodes到树上。 dumpNode对象将“访问”树中的每个节点。

 someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") ) dumpNodes= Visitor() someTree.visit( dumpNodes ) 

TreeNode visitalgorithm将确保每个TreeNode都被用作Visitor的arrivedAt方法的参数。

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

当你有一个相当稳定的类层次结构时,这是有用的,但是你需要改变这个层次结构的需求。

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

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

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

使用访客模式至less有三个非常好的理由:

  1. 减less数据结构改变时稍有不同的代码的扩散。

  2. 对几个数据结构应用相同的计算,而不改变实现计算的代码。

  3. 将信息添加到旧版库而不更改旧版代码。

请看看我写的这篇文章 。

正如康拉德·鲁道夫(Konrad Rudolph)已经指出的那样,它适合于需要双重调度的情况

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

例如:

可以说我有3种types的移动设备 – iPhone,Android,Windows Mobile。

所有这三个设备都安装了蓝牙无线电。

让我们假设蓝牙收音机可以来自两个独立的OEM – 英特尔和博通。

只是为了让我们的讨论相关的例子,让我们也假设由英特尔广播暴露的API不同于Broadcom广播暴露的API。

这是我的课程的样子 –

在这里输入图像描述 在这里输入图像描述

现在,我想介绍一个操作 – 在移动设备上打开蓝牙。

它的函数签名应该像这样 –

  void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio) 

所以取决于正确的设备types根据蓝牙无线电的正确types ,可以通过调用适当的步骤或algorithm来开启。

原则上,它变成了一个3×2的matrix,我试图根据涉及的对象的正确types来引导正确的操作。

多态行为取决于这两个参数的types。

在这里输入图像描述

现在,访问者模式可以应用于这个问题。 灵感来源于维基百科页面,声明: “实质上,访问者允许在不修改类本身的情况下,将新的虚函数添加到类的一个家族; 相反,创build一个访问者类来实现虚函数的所有适当的特化。 访客以实例参考为input,并通过双重调度实现目标。“

由于3x2matrix,双派遣是必要的

这里是如何设置 – 在这里输入图像描述

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

在我看来,使用Visitor Pattern或直接修改每个元素结构,添加新操作的工作量或多或less是相同的。 另外,如果我要添加新的元素类,比如Cow ,Operation界面将会受到影响,并且这会传播到所有现有类的元素,因此需要重新编译所有元素类。 那么有什么意义呢?

我发现以下链接更容易:

http://www.remondo.net/visitor-pattern-example-csharp/我find一个示例,显示一个模拟示例,显示什么是访客模式的好处。; 在这里你有不同的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的数量。你也可能注意到, Bottle使用unit是不同的数据types,需要投。

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

 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; } } 

注意上面的代码违反了Single Responsibility Principle 。 这意味着如果添加新types的容器,则必须更改主方法代码。 也使开关更长是不好的做法。

所以通过引入下面的代码:

 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 } 

您将Pill s计数的职责移到了名为PillCountVisitor类(并删除了switch case语句)。 这意味着只要你需要添加新types的药丸容器,你应该只改变PillCountVisitor类。 另外注意IVisitor接口是在另一种情况下使用的一般。

通过将Accept方法添加到pill容器类:

 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访客看他们的药片计数。 他知道如何计算你的药丸。

visitor.Count有药片的价值。

http://butunclebob.com/ArticleS.UncleBob.IuseVisitor中,您会看到真正的场景,您不能使用多态性; (答案)遵循单一责任原则。 其实在:

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

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

Cay Horstmann有一个很好的例子,可以在他的OOdevise和模式书中应用Visitor 。 他总结了这个问题:

复合对象通常具有由单个元素组成的复杂结构。 一些元素可能会再次有子元素。 …对元素的操作访问其子元素,对其应用操作并合并结果。 …但是,为这样的devise添加新的操作并不容易。

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

FileSystem类图

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

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

您可以在FileSystem的每个类中添加函数来实现这些操作(而且人们已经在过去做了这个,因为很明显它是如何做到的)。 问题是,无论何时添加新的function(上面的“等”行),都可能需要向结构类添加更多的方法。 在某些时候,在你添加到软件中的一些操作之后,这些类中的方法在类的function内聚方面就没有意义了。 例如,您有一个具有方法calculateFileColorForFunctionABC()FileNode ,以便在文件系统上实现最新的可视化function。

访问者模式(像许多devise模式)源于开发人员的痛苦和苦难 ,他们知道有一种更好的方法来允许他们的代码改变,而不需要在任何地方进行大量的改变,并且尊重良好的devise原则(高内聚性,低耦合性)。 这是我的意见,很难理解许多模式的有用性,直到你感到痛苦。 解释痛苦(就像我们试图用“等”function来做上面的事情一样)在解释中占用空间,是一种分心。 理解模式很难这个原因。

访问者允许我们从数据结构本身分离数据结构(例如, FileSystemNodes )的function。 该模式允许devise尊重内聚 – 数据结构类更简单(它们有更less的方法),并且function被封装到Visitor实现中。 这是通过双重调度 (这是模式的复杂部分)完成的:在结构类中使用accept()方法,在Visitor(function)类中使用visitX()方法:

带有Visitor的FileSystem类图

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

带有Visitor的FileSystem类图

例如,实现目录列表function的PrintSizeVisitor和实现具有大小的版本的PrintSizeVisitor 。 我们可以想象有一天会有一个'ExportXMLVisitor`产生XML数据,或者是另外一个使用JSON产生数据的访问者。我们甚至可以有一个访问者使用DOT这样的graphics化语言来显示我的目录树,以便可视化与另一个程序。

最后说明:Visitor的双重调度的复杂性意味着难以理解,编码和debugging。 简而言之,它有一个很高的怪胎因素,并再次遵循KISS原则。 在一项由研究人员进行的调查中,访客被certificate是一个有争议的模式(对于它的用处还没有达成共识)。 有些实验甚至表明它并没有使代码更容易维护。

基于@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”方法会发生什么? 这对整个devise来说是痛苦的。

同时,添加“漫游”方法会产生新的问题。 那么“吃”或“睡”呢? 我们是否真的必须为每一个我们想要添加的新操作或操作添加一个新的方法到Animal层次结构中? 这是丑陋的,最重要的是,我们将永远无法closures动物界面。 因此,通过访问者模式,我们可以添加新的方法而不修改层次结构!

所以,只需检查并运行这个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); } } } 

Visitor Pattern作为Aspect Object编程的同一个地下实现。

例如,如果你定义一个新的操作而不改变它所操作的元素的类

游客

访问者允许将一个新的虚拟函数添加到一个类族中而无需修改这些类本身; 相反,创build一个访问者类来实现虚函数的所有适当的特化

访客结构:

在这里输入图像描述

使用访客模式,如果:

  1. 类似的操作必须对结构中分组的不同types的对象执行
  2. 你需要执行许多不同的和不相关的操作。 它将操作与对象结构分离
  3. 必须在对象结构中添加新的操作而不改变
  4. 将相关操作收集到一个类中,而不是强迫您更改或派生类
  5. 将函数添加到您没有源或不能更改源的类库

即使访问者模式提供了灵活性来添加新的操作,而不改变对象中现有的代码,但这种灵活性带来了缺点。

如果添加了新的Visitable对象,则需要在Visitor&ConcreteVisitor类中更改代码 。 有一个解决方法来解决这个问题:使用reflection,这将影响性能。

代码片段:

 import java.util.HashMap; interface Visitable{ void accept(Visitor visitor); } interface Visitor{ void logGameStatistics(Chess chess); void logGameStatistics(Checkers checkers); void logGameStatistics(Ludo ludo); } class GameVisitor implements Visitor{ public void logGameStatistics(Chess chess){ System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc.."); } public void logGameStatistics(Checkers checkers){ System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser"); } public void logGameStatistics(Ludo ludo){ System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser"); } } abstract class Game{ // Add game related attributes and methods here public Game(){ } public void getNextMove(){}; public void makeNextMove(){} public abstract String getName(); } class Chess extends Game implements Visitable{ public String getName(){ return Chess.class.getName(); } public void accept(Visitor visitor){ visitor.logGameStatistics(this); } } class Checkers extends Game implements Visitable{ public String getName(){ return Checkers.class.getName(); } public void accept(Visitor visitor){ visitor.logGameStatistics(this); } } class Ludo extends Game implements Visitable{ public String getName(){ return Ludo.class.getName(); } public void accept(Visitor visitor){ visitor.logGameStatistics(this); } } public class VisitorPattern{ public static void main(String args[]){ Visitor visitor = new GameVisitor(); Visitable games[] = { new Chess(),new Checkers(), new Ludo()}; for (Visitable v : games){ v.accept(visitor); } } } 

说明:

  1. VisitableElement )是一个接口,这个接口方法必须被添加到一组类中。
  2. Visitor是一个接口,它包含对Visitable元素执行操作的方法。
  3. GameVisitor是一个实现Visitor接口( ConcreteVisitor )的类。
  4. 每个Visitable元素接受Visitor并调用Visitor接口的相关方法。
  5. 你可以把Game作为Element ,像Chess,Checkers and Ludo这样的具体游戏视为ConcreteElements

在上面的例子中, Chess, Checkers and Ludo是三种不同的游戏(和Visitable类)。 在一个晴朗的一天,我遇到了一个场景,logging每场比赛的统计数据。 因此,如果不修改单个类来实现统计function,则可以将这个责任集中在GameVisitor类中,这样做不会改变每个游戏的结构。

输出:

 Logging Chess statistics: Game Completion duration, number of moves etc.. Logging Checkers statistics: Game Completion duration, remaining coins of loser Logging Ludo statistics: Game Completion duration, remaining coins of loser 

参考

oodesign文章

源文章

更多细节

装饰

模式允许将行为静态或dynamic添加到单个对象,而不会影响来自同一类的其他对象的行为

相关文章:

IO的装饰模式

何时使用装饰模式?

虽然我已经了解了如何,何时,我从来没有明白为什么。 如果它可以帮助任何具有C ++语言背景的人,那么您要仔细阅读 。

对于懒惰,我们使用访问者模式,因为“当虚拟函数在C ++中dynamic分派时,函数重载是静态完成的”

换句话说,当你通过一个实际绑定到ApolloSpacecraft对象的SpaceShip参考时,确保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; } } 

我非常喜欢http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html中的描述和示例。;

假定你有一个固定的主类层次结构; 也许它来自另一个供应商,您不能对该层次结构进行更改。 然而,你的意图是,你想添加新的多态方法到该层次结构,这意味着通常你必须添加一些东西到基类接口。 所以困难是你需要添加方法到基类,但是你不能触及基类。 你怎么解决这个问题?

解决这类问题的devise模式被称为“访问者”(devise模式书中的最后一个),它build立在上一节所示的双重调度scheme之上。

访问者模式允许您通过创buildVisitortypes的单独类层次结构来扩展主types的接口,以虚拟化对主types执行的操作。 主types的对象只是“接受”访问者,然后调用访问者的dynamic绑定成员函数。

When you want to have function objects on union data types, you will need visitor pattern.

You might wonder what function objects and union data types are, then it's worth reading http://www.ccs.neu.edu/home/matthias/htdc.html

Interesting Posts