什么时候应该使用访问者设计模式?

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

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

119703 次浏览

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


我最喜欢的例子是Scott Meyers,《Effective c++》一书的作者,他称这是他的最重要的c++啊哈!永远时刻< em > < / em >

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

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

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

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

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

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

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

最后,我们实现实际操作没有修改猫和狗:

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

游客设计模式非常适用于目录树、XML结构或文档概要等“递归”结构。

Visitor对象访问递归结构中的每个节点:每个目录、每个XML标记等等。Visitor对象不遍历结构。相反,Visitor方法应用于结构的每个节点。

这是一个典型的递归节点结构。可以是目录或XML标记。 [如果你是一个Java人,想象一下有很多额外的方法来构建和维护子列表。]

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方法调用updown,因此访问者可以跟踪深度。

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

子类可以做一些事情,比如在每个级别上计算节点并积累一个节点列表,生成一个良好的路径分层节号。

这是申请表。它构建了一个树结构someTree。它创建了VisitordumpNodes

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

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

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

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

访问者允许您定义一个新的 操作而不改变类

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

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

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

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

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

访问者模式作为方面对象编程的地下实现。

例如,如果您定义一个新操作,而不改变其操作的元素的类

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

  1. 减少代码的增殖,当数据结构发生变化时,代码只会略有不同。

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

  3. 在不更改遗留代码的情况下向遗留库添加信息。

请看我写过一篇关于这个的文章

虽然我知道如何做,何时做,但我一直不明白为什么。为了帮助任何有c++等语言背景的人,你需要非常小心地读到这

对于懒人,我们使用visitor模式,因为在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;
}
}

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

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

正如你在上面看到的,you BilsterPack包含对药丸,所以你需要将对的数量乘以2。此外,你可能会注意到Bottle使用了unit,这是不同的数据类型,需要进行类型转换。

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

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。这意味着如果添加新类型的容器,则必须更改主方法代码。同时,延长开关时间也是不好的做法。

通过引入以下代码:

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
}

你将计数__abc0个数的责任转移到名为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访问者查看他们的药片计数。他知道怎么数你的药。

visitor.Count有药丸的值。

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

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

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

Cay Horstmann有一个很好的例子来说明在哪里应用访问者在他的面向对象设计和模式书中。他总结了这个问题:

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

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

FileSystem class diagram

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

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

您可以向FileSystem中的每个类添加函数来实现操作(过去已经有人这样做了,因为如何做是非常明显的)。问题是,每当您添加一个新功能(上面的“etc.”行)时,您可能需要向结构类添加越来越多的方法。在某种程度上,在您向软件中添加了一些操作之后,就类的功能内聚而言,这些类中的方法就没有意义了。例如,你有一个FileNode,它有一个方法calculateFileColorForFunctionABC(),以便在文件系统上实现最新的可视化功能。

访问者模式(像许多设计模式一样)诞生于开发人员的痛苦和折磨,他们知道有更好的方法来允许他们的代码更改,而不需要到处进行大量更改,同时也尊重良好的设计原则(高内聚,低耦合)。我的观点是,在你感受过这种痛苦之前,很难理解许多模式的有用性。解释痛苦(就像我们上面尝试添加的“等”功能一样)占用了解释的空间,并且会分散注意力。因此,理解模式很难。

Visitor允许我们将数据结构(例如,FileSystemNodes)上的功能与数据结构本身解耦。该模式允许设计尊重内聚性——数据结构类更简单(它们的方法更少),而且功能被封装到Visitor实现中。这是通过double-dispatching(这是模式的复杂部分)完成的:在结构类中使用accept()方法,在Visitor(功能)类中使用visitX()方法:

应用了访问者的文件系统类图

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

应用了访问者的文件系统类图

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

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

Visitor . enter image description here

在以下情况下使用访问者模式:

  1. 必须执行类似的操作用于在一个结构中分组的不同类型的对象
  2. 您需要执行许多不同且不相关的操作。它将操作从对象结构中分离出来
  3. 必须在不改变对象结构的情况下添加新操作
  4. 将相关操作收集到单个类中而不是强迫你改变或派生类
  5. 将函数添加到要么没有源,要么不能改变源 .

尽管游客模式提供了在不改变Object中现有代码的情况下添加新操作的灵活性,但这种灵活性也有一个缺点。

如果添加了一个新的Visitable对象,则需要在Visitor &ConcreteVisitor类。有一种变通方法可以解决这个问题:使用反射,这将对性能产生影响。

代码片段:

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

在上面的例子中,Chess, Checkers and Ludo是三个不同的游戏(和Visitable类)。在一个晴朗的日子里,我遇到了一个记录每款游戏统计数据的场景。因此,无需修改单个类来实现统计功能,您可以将该职责集中在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 article

sourcemaking文章

欲知详情

装饰

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

相关文章:

IO的装饰器模式

何时使用装饰器模式?< / >

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

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

下面是一个例子,展示了我们需要双重调度的情况。访问者如何帮助我们这样做。

例子:

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

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

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

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

这是我的类的样子

enter image description here enter image description here < / p >

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

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

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

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

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

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

enter image description here

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

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

下面是设置的样子 enter image description here < / p >

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

我真的很喜欢http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html中的描述和例子。

假设你有一个固定的主类层次结构;也许它来自另一个供应商,您无法更改该层次结构。但是,您的意图是希望向该层次结构添加新的多态方法,这意味着通常必须向基类接口添加一些东西。因此,两难的问题是,您需要向基类添加方法,但不能更改基类。你怎么解决这个问题呢?

解决这类问题的设计模式被称为“访问者”(设计模式书中的最后一个),它建立在上一节中展示的双重调度方案的基础上。

访问者模式允许您通过创建访问者类型的单独类层次结构来扩展主类型的接口,以虚拟化在主类型上执行的操作。主类型的对象简单地“接受”访问者,然后调用访问者的动态绑定成员函数。

双重分派只是使用此模式的原因之一 但请注意,这是在使用单一分派范式的语言中实现双分派或多分派的唯一方法

以下是使用该模式的原因:

1) 我们希望在每次都不更改模型的情况下定义新的操作,因为模型不经常变化,而操作经常变化

2) 我们不想把模型和行为结合起来,因为多个应用程序中的我们想要一个可重复使用的模型我们希望有一个可扩展的模型允许客户端类用自己的类定义它们的行为

3)我们有一些常见的操作,这些操作依赖于模型的具体类型,但是我们不想在每个子类中实现逻辑,因为那样会使公共逻辑在多个类中爆炸,从而在多个地方爆炸.

4)我们正在使用一个域模型设计和相同层次结构的模型类执行太多不同的事情,而这些事情可以在其他地方收集.

< p > 5) 我们需要双重调度。< br > 我们声明了带有接口类型的变量,并且希望能够根据它们的运行时类型来处理它们,当然不需要使用if (myObj instanceof Foo) {}或任何技巧 例如,其思想是将这些变量传递给将接口的具体类型声明为参数的方法,以应用特定的处理。 对于依赖于单分派的语言来说,这种方式是不可能开箱即用的,因为在运行时所选择的调用只依赖于接收器的运行时类型 注意,在Java中,要调用的方法(签名)是在编译时选择的,它取决于参数的声明类型,而不是它们的运行时类型

最后一点是使用访问器的一个原因,也是一个后果,因为当你实现访问器时(当然对于不支持多分派的语言),你必须引入双分派实现

注意遍历元素(迭代)并将访问者应用到每个元素上并不是使用该模式的原因 你使用模式是因为你分离了模型和处理 通过使用该模式,您还可以获得迭代器能力 此功能非常强大,并且超出了使用特定方法在普通类型上迭代的范围,因为accept()是一个泛型方法 这是一个特殊的用例。我把它放到一边


Java示例

我将用一个国际象棋的例子来说明该模式的附加价值,在这个例子中,我们想要定义处理,即玩家要求棋子移动

如果不使用访问者模式,我们可以直接在pieces子类中定义块移动行为 例如,我们可以有这样一个Piece接口:

public interface Piece{


boolean checkMoveValidity(Coordinates coord);


void performMove(Coordinates coord);


Piece computeIfKingCheck();


}

每个Piece子类将实现它,如:

public class Pawn implements Piece{


@Override
public boolean checkMoveValidity(Coordinates coord) {
...
}


@Override
public void performMove(Coordinates coord) {
...
}


@Override
public Piece computeIfKingCheck() {
...
}


}
对于所有Piece的子类也是一样的。< br > 下面是一个说明这种设计的图表类:

[模型类图

这种方法有三个重要的缺点:

-行为,如performMove()computeIfKingCheck()将很可能使用公共逻辑 例如,无论具体的Piece是什么,performMove()最终将当前块设置为特定位置,并可能获取对手块 将相关的行为拆分到多个类中,而不是将它们聚集在一起,在某种程度上挫败了单一责任模式。使它们的可维护性更加困难

-处理为checkMoveValidity()不应该是Piece子类可以看到或改变的东西 它是超越人类或计算机行为的检查。该检查在玩家请求的每个动作时执行,以确保所请求的棋子移动是有效的 所以我们甚至不想在Piece接口中提供它。< / p > 在对机器人开发者具有挑战性的国际象棋游戏中,通常应用程序提供了一个标准的API (Piece接口,子类,棋盘,常见行为等…),并让开发者丰富他们的机器人策略 要做到这一点,我们必须提出一个模型,其中数据和行为在Piece实现中不是紧密耦合的

所以让我们使用访问者模式!

我们有两种结构:

-接受访问的模型类(碎片)

-拜访他们的访客(移动操作)

下面是一个说明该模式的类图:

enter image description here

上面的部分是访问者,下面的部分是模型类。< br >

下面是PieceMovingVisitor接口(为每种Piece指定的行为):

public interface PieceMovingVisitor {


void visitPawn(Pawn pawn);


void visitKing(King king);


void visitQueen(Queen queen);


void visitKnight(Knight knight);


void visitRook(Rook rook);


void visitBishop(Bishop bishop);


}

Piece的定义如下:

public interface Piece {


void accept(PieceMovingVisitor pieceVisitor);


Coordinates getCoordinates();


void setCoordinates(Coordinates coordinates);


}

它的关键方法是:

void accept(PieceMovingVisitor pieceVisitor);
它提供了第一个分派:基于Piece接收器的调用。< br > 在编译时,该方法被绑定到Piece接口的accept()方法,在运行时,该有界方法将在运行时的Piece类上调用。< br > accept()方法实现将执行第二次分派。< br > < / p > 实际上,每个Piece子类都希望被PieceMovingVisitor对象访问,通过传递参数本身来调用PieceMovingVisitor.visit()方法 通过这种方式,编译器在编译时立即将已声明形参的类型与具体类型绑定 这是第二个调度。
下面是Bishop子类,它说明了这一点
public class Bishop implements Piece {


private Coordinates coord;


public Bishop(Coordinates coord) {
super(coord);
}


@Override
public void accept(PieceMovingVisitor pieceVisitor) {
pieceVisitor.visitBishop(this);
}


@Override
public Coordinates getCoordinates() {
return coordinates;
}


@Override
public void setCoordinates(Coordinates coordinates) {
this.coordinates = coordinates;
}


}

下面是一个用法示例:

// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();


// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);


// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
piece.accept(new MovePerformingVisitor(coord));
}

游客的缺点 < br >

访问者模式是一个非常强大的模式,但它也有一些重要的局限性,在使用它之前应该考虑

1)降低/破坏封装的风险

在某些类型的操作中,访问者模式可能会减少或破坏域对象的封装

例如,由于MovePerformingVisitor类需要设置实际块的坐标,Piece接口必须提供一种方法来做到这一点:

void setCoordinates(Coordinates coordinates);
Piece坐标改变的责任现在向Piece子类以外的其他类开放 移动访问者在Piece子类中执行的处理也不是一个选项 它确实会产生另一个问题,因为Piece.accept()接受任何访问者实现。它不知道访问者执行了什么,因此不知道是否以及如何改变Piece状态 一种识别访问者的方法是根据访问者的实现在Piece.accept()中执行后处理。这将是一个非常糟糕的主意,因为它将在Visitor实现和Piece子类之间创建一个高度耦合,而且它可能需要使用trick作为getClass()instanceof或任何标识Visitor实现的标记

2)变更模型的要求

与其他行为设计模式(例如Decorator)相反,访问者模式是侵入式的 我们确实需要修改初始接收器类,以提供一个accept()方法来接受访问 Piece及其子类没有任何问题,因为它们是我们的课程.
在内置类或第三方类中,事情就不那么容易了 我们需要包装或继承(如果可以的话)它们来添加accept()方法

3)间接

模式创建多个指向 双重分派意味着两次调用而不是一次调用:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
当访问者改变被访问对象的状态时,我们可以有额外的间接指示 它可能看起来像一个循环:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)

需要修改的类必须全部实现'accept'方法。客户端调用这个accept方法来对类族执行一些新的操作,从而扩展它们的功能。通过为每个特定的操作传递不同的访问者类,客户端可以使用这个accept方法执行各种各样的新操作。访问者类包含多个覆盖的访问方法,定义如何为家族中的每个类实现相同的特定操作。这些访问方法被传递给一个要在其上工作的实例。

当你考虑使用它的时候

  1. 当你有一个类族时,你知道你将不得不添加许多新的动作,但由于某种原因,你不能在将来改变或重新编译类族。
  2. 当您希望添加一个新操作,并将该新操作完全定义在一个访问者类中,而不是分散在多个类中。
  3. 当你的老板说你必须生成一系列类,这些类必须做一些现在!…但没有人确切知道这是什么东西。

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

同样正如@Konrad鲁道夫指出的那样,它实际上是一个双重分发使用两个具体实例一起确定运行时方法。

所以实际上,只要我们正确定义了操作接口,就不需要为操作执行器创建常见的接口。

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

你的问题是什么时候知道:

我不首先编码访问者模式。我编码标准,等待需要发生&然后重构。假设你有多个支付系统,一次安装一个。在签出时,你可以有很多if条件(或instanceOf),例如:

//psuedo code
if(payPal)
do paypal checkout
if(stripe)
do strip stuff checkout
if(payoneer)
do payoneer checkout

现在假设我有10种支付方式,这有点难看。因此,当你看到这种模式发生时,访问者会很容易地将所有这些分离出来,然后你最终会调用这样的东西:

new PaymentCheckoutVistor(paymentType).visit()

你可以看到如何实现它从这里的例子的数量,我只是向你展示一个用例。

我不理解这个模式,直到我遇到鲍勃叔叔的文章并阅读注释。 考虑以下代码:

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)看起来是一样的,但实际上是不同的,因为它调用不同的重载访问者。