我什么时候应该使用访客设计模式?

时间:2008-10-31 23:04:19

标签: design-patterns visitor-pattern

我一直在博客中看到对访客模式的引用,但我必须承认,我只是不明白。我读了wikipedia article for the pattern,我理解它的机制,但我仍然对我何时使用它感到困惑。

最近刚刚得到装饰模式的人现在看到它在任何地方的用途我都希望能够直观地理解这个看似方便的模式。

20 个答案:

答案 0 :(得分:293)

我对访客模式不太熟悉。让我们看看我是否做对了。假设你有动物等级

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

最后,我们实现了实际操作,而不修改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 :(得分:121)

你混淆的原因可能是访客是一个致命的误称。许多(突出的 1 !)程序员偶然发现了这个问题。它实际上做的是在本地不支持它的语言中实现double dispatching(大部分都不支持)。


1)我最喜欢的例子是着名的“有效C ++”作者Scott Meyers,他称这是他的most important C++ aha! moments ever

答案 2 :(得分:77)

这里的每个人都是正确的,但我认为它无法解决“何时”。首先,来自设计模式:

  

访客可让您定义新内容   不改变类的操作   它运作的元素。

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

现在,面向对象的设计假设你比新方法更有可能增长新的类,所以可以说,添加更多的行更容易。您只需添加一个新类,指定该类中的不同内容,并继承其余类。

但有时,这些类是相对静态的,但您需要经常添加更多方法 - 添加列。 OO设计中的标准方法是将这些方法添加到所有类中,这可能是昂贵的。访客模式使这很容易。

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

答案 3 :(得分:21)

访客设计模式非常适用于目标树,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都用作访问者arrivedAt方法的参数。

答案 4 :(得分:18)

查看它的一种方法是访问者模式是一种让客户向特定类层次结构中的所有类添加其他方法的方法。

当您拥有一个相当稳定的类层次结构时,它很有用,但您需要更改对该层次结构需要完成的任务的要求。

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

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

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

答案 5 :(得分:14)

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

  1. 减少代码的扩散,这在数据结构发生变化时略有不同。

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

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

  4. 请查看an article I've written about this

答案 6 :(得分:12)

正如Konrad Rudolph已经指出的那样,它适用于我们需要双重调度

的情况

这是一个示例,显示我们需要双重调度&amp;访客如何帮助我们这样做。

示例:

假设我有3种类型的移动设备 - iPhone,Android,Windows Mobile。

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

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

为了使这个例子与我们的讨论相关,我们还假设英特尔无线电公开的API与Broadcom无线电公开的API不同。

这是我的课程的样子 -

enter image description here enter image description here

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

它的功能签名应该是这样的 -

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

因此,取决于正确类型的设备取决于正确类型的蓝牙无线电,可以通过调用适当的步骤或算法来打开

原则上,它变成一个3 x 2矩阵,其中我试图根据所涉及的对象的正确类型来引导正确的操作。

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

enter image description here

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

由于3x2矩阵

,双重调度是必要的

以下是设置的外观 - enter image description here

我写了一个例子来回答另一个问题,即代码&amp;其解释提到了here

答案 7 :(得分:9)

我发现以下链接更容易:

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包含一对药丸&#39;因此,您需要将对的数量乘以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
}

您将计算Pill s的数量的责任转移到了名为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有丸的价值。

http://butunclebob.com/ArticleS.UncleBob.IuseVisitor您看到真实的情况,您不能使用polymorphism(答案)来遵循单一责任原则。实际上在:

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

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

答案 8 :(得分:5)

Cay Horstmann有一个很好的例子,说明在哪里申请Visitor in his OO Design and patterns book。他总结了这个问题:

  

复合对象通常具有复杂的结构,由单个元素组成。一些元素可能再次具有子元素。 ...对元素的操作访问其子元素,将操作应用于它们,并组合结果。 ...但是,在这样的设计中添加新操作并不容易。

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

FileSystem class diagram

以下是我们可能希望使用此结构实现的一些操作(功能):

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

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

访问者模式(就像许多设计模式一样)诞生于开发人员的痛苦和痛苦,他们知道有更好的方法可以让代码改变而不需要在任何地方进行大量更改。尊重良好的设计原则(高内聚,低耦合)。我认为,在你感受到疼痛之前,很难理解很多模式的用处。解释痛苦(就像我们尝试在上面添加“等”功能一样)会在解释中占用空间并且会分散注意力。因此理解模式很难。

访问者允许我们从数据结构本身中分离数据结构的功能(例如,FileSystemNodes)。该模式允许设计尊重内聚 - 数据结构类更简单(它们具有更少的方法),并且功能被封装到Visitor实现中。这是通过 double-dispatching (这是模式的复杂部分)完成的:在结构类中使用accept()方法,在访问者中使用visitX()方法(功能)课程:

FileSystem class diagram with Visitor applied

这种结构允许我们添加在结构上起作用的新功能作为具体的访问者(不改变结构类)。

FileSystem class diagram with Visitor applied

例如,实现目录列表功能的PrintNameVisitor和实现具有大小的版本的PrintSizeVisitor。我们可以想象有一天会有一个以XML格式生成数据的'ExportXMLVisitor',或者用JSON生成它的其他访问者等等。我们甚至可以让一个访问者使用graphical language such as DOT显示我的目录树,以便可视化与另一个程序。

最后要说明的是:访问者的双重调度的复杂性意味着它更难理解,编码和调试。简而言之,它具有极高的极客因素,并且再次成为KISS原则。 In a survey done by researchers, Visitor was shown to be a controversial pattern (there wasn't a consensus about its usefulness). Some experiments even showed it didn't make code easier to maintain.

答案 9 :(得分:5)

访问者模式的快速描述。需要修改的类必须全部实现'accept'方法。客户端调用此accept方法对该类系列执行一些新操作,从而扩展其功能。通过为每个特定操作传入不同的访问者类,客户端可以使用这一种接受方法来执行各种新操作。访问者类包含多个重写的访问方法,这些方法定义了如何为系列中的每个类实现相同的特定操作。这些访问方法传递给一个可以工作的实例。

您可以考虑使用

  1. 当你有一系列课程时,你知道你将不得不添加许多新的动作,但由于某种原因,你将来无法改变或重新编译这类课程。
  2. 如果您想要添加新操作并将新操作完全定义在访问者类中,而不是分散在多个类中。
  3. 当你的老板说你必须制作一系列必须做一些现在的课程时!......但是没有人真正知道那些东西到底是什么。

答案 10 :(得分:5)

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

答案 11 :(得分:4)

访客模式与Aspect对象编程相同的地下实现..

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

答案 12 :(得分:4)

双重发送只是使用此模式的其中一个原因 但请注意,这是使用单一调度范例的语言实现双重或更多调度的单一方式。

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

1)我们希望在不改变模型的情况下定义新操作,因为模型不会经常改变操作频繁更改。

2)我们不想结合模特和行为因为我们希望在多个应用程序中拥有可重复使用的模型,或者我们希望拥有一个可扩展的模型,允许客户端类使用自己的类定义行为。

3)我们有共同的操作依赖于模型的具体类型,但我们不希望在每个子类中实现逻辑,因为这会在多个类中爆炸共同逻辑,因此在多个地方

4)我们正在使用域模型设计,同一层次结构的模型类执行太多可以在其他地方收集的不同内容

5)我们需要双重发送 我们有使用接口类型声明的变量,我们希望能够根据它们的运行时类型处理它们......当然不使用if (myObj instanceof Foo) {}或任何技巧。
例如,这个想法是将这些变量传递给声明接口的具体类型作为参数的方法,以应用特定的处理。 这种做法不可能开箱即用,因为语言依赖于单一调度,因为在运行时调用的选择仅取决于接收器的运行时类型。
请注意,在Java中,要在编译时选择要调用的方法(签名),它取决于声明的参数类型,而不是它们的运行时类型。

使用访问者的最后一点也是一个结果,因为当您实现访问者时(当然对于不支持多个调度的语言),您必然需要引入双重调度实现。 />

请注意,遍历每个元素的迭代元素(迭代)不是使用该模式的理由。
您使用该模式是因为您拆分模型和处理 通过使用该模式,您还可以从迭代器功能中受益 这种能力非常强大,并且通过特定方法超越了常见类型的迭代,因为accept()是一种通用方法 这是一个特殊用例。所以我会把它放在一边。

Java中的示例

我将通过国际象棋示例说明模式的附加值,我们希望将处理定义为玩家请求移动的棋子。

如果没有访问者模式使用,我们可以直接在片段子类中定义片段移动行为 例如,我们可以使用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子类都是一样的。
这是一个图表类,说明了这个设计:

[model class diagram

这种方法存在三个重要缺点:

- performMove()computeIfKingCheck()等行为很可能会使用通用逻辑 例如,无论具体PieceperformMove()最终将当前棋子设置到特定位置并可能获得对手棋子。
在多个类中拆分相关行为而不是收集它们会以某种方式失败单一责任模式。使他们的可维护性更难。

- 处理checkMoveValidity()不应该是Piece子类可能看到或更改的内容。
检查超出了人或计算机的行为。该检查在玩家请求的每个动作中执行,以确保所请求的棋子移动有效 所以我们甚至不想在Piece界面中提供它。

- 在针对僵尸开发人员挑战的国际象棋游戏中,通常应用程序提供标准API(Piece接口,子类,板,常见行为等)并让开发人员丰富他们的机器人战略。
为了能够做到这一点,我们必须提出一个模型,其中数据和行为在Piece实现中没有紧密耦合。

让我们去使用访客模式!

我们有两种结构:

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

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

这是一个说明模式的类图:

enter image description here

在上半部分我们有访客,在下半部分我们有模型类。

以下是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);

}

现在定义了这件作品:

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

其关键方法是:

void accept(PieceMovingVisitor pieceVisitor);

它提供第一个调度:基于Piece接收器的调用。
在编译时,该方法绑定到Piece接口的accept()方法,并且在运行时,将在运行时Piece类上调用有界方法。
accept()方法实现将执行第二次调度。

实际上,希望被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));
}

访客弊端

访客模式是一种非常强大的模式,但它在使用前还应考虑一些重要的限制。

1)减少/破坏封装的风险

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

例如,由于MovePerformingVisitor类需要设置实际部分的坐标,Piece接口必须提供一种方法:

void setCoordinates(Coordinates coordinates);

Piece坐标更改的责任现在对除Piece子类之外的其他类开放 移动访问者在Piece子类中执行的处理也不是一种选择 由于Piece.accept()接受任何访问者实施,它确实会产生另一个问题。它不知道访问者的表现,因此不知道是否以及如何改变作品状态 识别访问者的方法是根据访问者实施在Piece.accept()中执行后期处理。这将是一个非常糟糕的主意,因为它会在Visitor实现和Piece子类之间创建一个高耦合,此外它可能需要使用技巧getClass()instanceof或任何标识Visitor实现的标记。 / p>

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)

答案 13 :(得分:3)

Visitor

  

访问者允许在不修改类本身的情况下向一个类族添加新的虚函数;相反,我们创建一个访问者类,实现虚拟函数的所有适当的特化

访客结构:

enter image description here

使用访客模式:

  1. 必须对在结构
  2. 中分组的不同类型的对象执行类似的操作
  3. 您需要执行许多不同且不相关的操作。 它将操作与对象分离
  4. 必须在不改变对象结构的情况下添加新操作
  5. 将相关操作集中到单个类中,而不是强迫您更改或派生类
  6. 将函数添加到没有源或无法更改源的类库
  7. 即使 Visitor 模式提供了在不更改Object中现有代码的情况下添加新操作的灵活性,但这种灵活性也存在缺陷。

    如果添加了新的Visitable对象,则需要在Visitor&amp ;;中更改代码。 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. VisitableElement)是一个接口,必须将此接口方法添加到一组类中。
    2. Visitor是一个接口,其中包含对Visitable元素执行操作的方法。
    3. GameVisitor是一个实现Visitor接口(ConcreteVisitor)的类。
    4. 每个Visitable元素接受Visitor并调用Visitor接口的相关方法。
    5. 您可以将Game视为Element,将Chess,Checkers and Ludo等具体游戏视为ConcreteElements
    6. 在上面的例子中,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文章

      了解更多详情

      Decorator

        

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

      相关帖子:

      Decorator Pattern for IO

      When to Use the Decorator Pattern?

答案 14 :(得分:3)

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

如果您需要添加&#34; Walk&#34;会发生什么?方法在这?这对整个设计来说都是痛苦的。

同时,添加&#34; Walk&#34;方法生成新问题。怎么样&#34;吃&#34;或者&#34;睡眠&#34;?对于我们要添加的每个新操作或操作,我们是否必须为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);
        }
    }
}

答案 15 :(得分:1)

虽然我已经理解了如何以及何时,但我从未理解为什么。如果它能帮助任何具有C ++语言背景的人,你需要非常小心地read this

对于懒惰,我们使用访问者模式,因为“而虚拟函数是在C ++中动态调度的,函数重载是静态完成的”

或者换句话说,确保在传入实际绑定到ApolloSpacecraft对象的SpaceShip引用时调用CollideWith(ApolloSpacecraft&amp;)。

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

答案 16 :(得分:1)

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

  

假设您有一个固定的主类层次结构;也许它来自其他供应商,您无法对该层次结构进行更改。但是,您的意图是您希望向该层次结构添加新的多态方法,这意味着通常您必须向基类接口添加一些内容。所以困境是你需要向基类添加方法,但是你不能触及基类。你怎么解决这个问题?

     

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

     

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

答案 17 :(得分:1)

直到碰到uncle bob article并阅读评论时,我才理解这种模式。 考虑以下代码:

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

虽然看起来不错,因为它确认Single Responsibility违反了Open/Closed原则。每次使用新的Employee类型时,如果要进行类型检查,则必须添加。而且,如果您不这样做,那么在编译时就永远不会知道。

使用访问者模式,您可以使代码更整洁,因为它不违反开放/闭合原则,也不违反单一职责。而且,如果您忘记实现访问,它将无法编译:

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

答案 18 :(得分:0)

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

答案 19 :(得分:0)

您的问题是何时知道:

我不首先使用访客模式进行编码。我编码标准,并等待需要发生然后重构。因此,假设您一次安装了多个付款系统。在结帐时,您可能会有许多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()

您可以从此处的许多示例中看​​到如何实现它,只是向您展示了一个用例。