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

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


当前回答

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

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

其他回答

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

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

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

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

1)我们希望定义新的操作,而不需要每次都改变模型,因为模型不会经常变化,而操作则经常变化。

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

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

4)我们正在使用领域模型设计,相同层次结构的模型类执行太多不同的事情,而这些事情可能会聚集在其他地方。

5) We need a double dispatch. We have variables declared with interface types and we want to be able to process them according their runtime type … of course without using if (myObj instanceof Foo) {} or any trick. The idea is for example to pass these variables to methods that declares a concrete type of the interface as parameter to apply a specific processing. This way of doing is not possible out of the box with languages relies on a single-dispatch because the chosen invoked at runtime depends only on the runtime type of the receiver. Note that in Java, the method (signature) to call is chosen at compile time and it depends on the declared type of the parameters, not their runtime type.

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

请注意,遍历元素(迭代)以在每个元素上应用访问者并不是使用该模式的原因。 您使用模式是因为您分离了模型和处理。 通过使用该模式,您还可以从迭代器功能中获益。 这种能力非常强大,并且超越了使用特定方法对普通类型进行迭代的能力,因为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子类也是一样的。 下面是一个说明这种设计的图表类:

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

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

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

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

让我们使用访问者模式!

我们有两种结构:

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

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

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

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

下面是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接收器的调用。 在编译时,该方法被绑定到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的坐标,piece接口必须提供一种方法来做到这一点:

void setCoordinates(Coordinates coordinates);

The responsibility of Piece coordinates changes is now open to other classes than Piece subclasses. Moving the processing performed by the visitor in the Piece subclasses is not an option either. It will indeed create another issue as the Piece.accept() accepts any visitor implementation. It doesn't know what the visitor performs and so no idea about whether and how to change the Piece state. A way to identify the visitor would be to perform a post processing in Piece.accept() according to the visitor implementation. It would be a very bad idea as it would create a high coupling between Visitor implementations and Piece subclasses and besides it would probably require to use trick as getClass(), instanceof or any marker identifying the Visitor implementation.

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)

Visitor设计模式非常适用于目录树、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方法调用了up和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

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

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

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

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

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

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

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