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

发布于 2024-07-07 21:33:13 字数 1453 浏览 10 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(20

笑梦风尘 2024-07-14 21:33:13

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

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

I'm not very familiar with the Visitor pattern. Let's see if I got it right. Suppose you have a hierarchy of animals

class Animal {  };
class Dog: public Animal {  };
class Cat: public Animal {  };

(Suppose it is a complex hierarchy with a well-established interface.)

Now we want to add a new operation to the hierarchy, namely we want each animal to make its sound. As far as the hierarchy is this simple, you can do it with straight polymorphism:

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

But proceeding in this way, each time you want to add an operation you must modify the interface to every single class of the hierarchy. Now, suppose instead that you are satisfied with the original interface, and that you want to make the fewest possible modifications to it.

The Visitor pattern allows you to move each new operation in a suitable class, and you need to extend the hierarchy's interface only once. Let's do it. First, we define an abstract operation (the "Visitor" class in GoF) which has a method for every class in the hierarchy:

class Operation
{
public:
    virtual void hereIsADog(Dog *d) = 0;
    virtual void hereIsACat(Cat *c) = 0;
};

Then, we modify the hierarchy in order to accept new operations:

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

Finally, we implement the actual operation, without modifying neither Cat nor 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"; }

Now you have a way to add operations without modifying the hierarchy anymore.
Here is how it works:

int main()
{
    Cat c;
    Sound theSound;
    c.letsDo(&theSound);
}
乖乖兔^ω^ 2024-07-14 21:33:13

您感到困惑的原因可能是“访客”是一个致命的用词不当。 许多(著名1!)程序员都被这个问题绊倒了。 它实际上所做的是在本身不支持它的语言中实现双重调度(大多数语言)不)。


1) 我最喜欢的例子是 Scott Meyers,他是《Effective C++》的著名作者,他将此称为他的最重要的 C++ 啊哈! 永远的时刻。

The reason for your confusion is probably that the Visitor is a fatal misnomer. Many (prominent1!) programmers have stumbled over this problem. What it actually does is implement double dispatching in languages that don't support it natively (most of them don't).


1) My favourite example is Scott Meyers, acclaimed author of “Effective C++”, who called this one of his most important C++ aha! moments ever.

二货你真萌 2024-07-14 21:33:13

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

Visitor 允许您定义一个新的
无需更改类即可操作
它所操作的元素。

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

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

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

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

Everyone here is correct, but I think it fails to address the "when". First, from Design Patterns:

Visitor lets you define a new
operation without changing the classes
of the elements on which it operates.

Now, let's think of a simple class hierarchy. I have classes 1, 2, 3 and 4 and methods A, B, C and D. Lay them out like in a spreadsheet: the classes are lines and the methods are columns.

Now, Object Oriented design presumes you are more likely to grow new classes than new methods, so adding more lines, so to speak, is easier. You just add a new class, specify what's different in that class, and inherits the rest.

Sometimes, though, the classes are relatively static, but you need to add more methods frequently -- adding columns. The standard way in an OO design would be to add such methods to all classes, which can be costly. The Visitor pattern makes this easy.

By the way, this is the problem that Scala's pattern matches intends to solve.

寻找我们的幸福 2024-07-14 21:33:13

Visitor 设计模式非常适合目录树、XML 结构或文档大纲等“递归”结构。

Visitor 对象访问递归结构中的每个节点:每个目录、每个 XML 标记等等。 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 方法的参数。

The Visitor design pattern works really well for "recursive" structures like directory trees, XML structures, or document outlines.

A Visitor object visits each node in the recursive structure: each directory, each XML tag, whatever. The Visitor object doesn't loop through the structure. Instead Visitor methods are applied to each node of the structure.

Here's a typical recursive node structure. Could be a directory or an XML tag.
[If your a Java person, imagine of a lot of extra methods to build and maintain the children list.]

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

The visit method applies a Visitor object to each node in the structure. In this case, it's a top-down visitor. You can change the structure of the visit method to do bottom-up or some other ordering.

Here's a superclass for visitors. It's used by the visit method. It "arrives at" each node in the structure. Since the visit method calls up and down, the visitor can keep track of the depth.

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

A subclass could do things like count nodes at each level and accumulate a list of nodes, generating a nice path hierarchical section numbers.

Here's an application. It builds a tree structure, someTree. It creates a Visitor, dumpNodes.

Then it applies the dumpNodes to the tree. The dumpNode object will "visit" each node in the tree.

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

The TreeNode visit algorithm will assure that every TreeNode is used as an argument to the Visitor's arrivedAt method.

莫言歌 2024-07-14 21:33:13

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

当您拥有相当稳定的类层次结构,但对该层次结构需要执行的操作的要求不断变化时,它非常有用。

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

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

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

One way to look at it is that the visitor pattern is a way of letting your clients add additional methods to all of your classes in a particular class hierarchy.

It is useful when you have a fairly stable class hierarchy, but you have changing requirements of what needs to be done with that hierarchy.

The classic example is for compilers and the like. An Abstract Syntax Tree (AST) can accurately define the structure of the programming language, but the operations you might want to do on the AST will change as your project advances: code-generators, pretty-printers, debuggers, complexity metrics analysis.

Without the Visitor Pattern, every time a developer wanted to add a new feature, they would need to add that method to every feature in the base class. This is particularly hard when the base classes appear in a separate library, or are produced by a separate team.

(I have heard it argued that the Visitor pattern is in conflict with good OO practices, because it moves the operations of the data away from the data. The Visitor pattern is useful in precisely the situation that the normal OO practices fail.)

好久不见√ 2024-07-14 21:33:13

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

使用该模式的原因如下:

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 子类都是同样的事情。
这是一个说明此设计的图表类:

[模型类图

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

– 诸如 performMove()computeIfKingCheck( ) 很可能会使用通用逻辑。
例如,无论具体的Piece是什么,performMove()最终都会将当前棋子设置到特定位置,并可能占据对手棋子。
将相关行为拆分到多个类中而不是聚集它们在某种程度上破坏了单一责任模式。 使它们的可维护性变得更加困难。

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

– 在对机器人开发人员来说具有挑战性的国际象棋游戏中,应用程序通常会提供标准 API(Piece 接口、子类、棋盘、常见行为等...)并让开发人员丰富他们的机器人策略。
为了做到这一点,我们必须提出一个模型,其中数据和行为在 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() 方法实现将执行第二次调度。

事实上,每个想要被 PieceMovingVisitor 对象访问的 Piece 子类都会通过传递参数来调用 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 的职责code> 坐标更改现在对 Piece 子类之外的其他类开放。
将访问者执行的处理移到 Piece 子类中也不是一个选项。
它确实会产生另一个问题,因为 Piece.accept() 接受任何访问者实现。 它不知道访问者执行了什么,因此不知道是否以及如何更改 Piece 状态。
识别访问者的一种方法是根据访问者实现在 Piece.accept() 中执行后处理。 这将是一个非常糟糕的主意,因为它会在 Visitor 实现和 Piece 子类之间创建高度耦合,此外它可能需要使用 getClass()instanceof 或任何标识访问者实现的标记。

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)

Double dispatch is just one reason among others to use this pattern.
But note that it is the single way to implement double or more dispatch in languages that uses a single dispatch paradigm.

Here are reasons to use the pattern :

1) We want to define new operations without changing the model at each time because the model doesn’t change often wile operations change frequently.

2) We don't want to couple model and behavior because we want to have a reusable model in multiple applications or we want to have an extensible model that allow client classes to define their behaviors with their own classes.

3) We have common operations that depend on the concrete type of the model but we don’t want to implement the logic in each subclass as that would explode common logic in multiple classes and so in multiple places.

4) We are using a domain model design and model classes of the same hierarchy perform too many distinct things that could be gathered somewhere else.

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.

The last point that is a reason to use the visitor is also a consequence because as you implement the visitor (of course for languages that doesn’t support multiple dispatch), you necessarily need to introduce a double dispatch implementation.

Note that the traversal of elements (iteration) to apply the visitor on each one is not a reason to use the pattern.
You use the pattern because you split model and processing.
And by using the pattern, you benefit in addition from an iterator ability.
This ability is very powerful and goes beyond iteration on common type with a specific method as accept() is a generic method.
It is a special use case. So I will put that to one side.


Example in Java

I will illustrate the added value of the pattern with a chess example where we would like to define processing as player requests a piece moving.

Without the visitor pattern use, we could define piece moving behaviors directly in the pieces subclasses.
We could have for example a Piece interface such as :

public interface Piece{

    boolean checkMoveValidity(Coordinates coord);

    void performMove(Coordinates coord);

    Piece computeIfKingCheck();

}

Each Piece subclass would implement it such as :

public class Pawn implements Piece{

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

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

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

}

And the same thing for all Piece subclasses.
Here is a diagram class that illustrates this design :

[model class diagram

This approach presents three important drawbacks :

– behaviors such as performMove() or computeIfKingCheck() will very probably use common logic.
For example whatever the concrete Piece, performMove() will finally set the current piece to a specific location and potentially takes the opponent piece.
Splitting related behaviors in multiple classes instead of gathering them defeats in a some way the single responsibility pattern. Making their maintainability harder.

– processing as checkMoveValidity() should not be something that the Piece subclasses may see or change.
It is check that goes beyond human or computer actions. This check is performed at each action requested by a player to ensure that the requested piece move is valid.
So we even don’t want to provide that in the Piece interface.

– In chess games challenging for bot developers, generally the application provides a standard API (Piece interfaces, subclasses, Board, common behaviors, etc…) and let developers enrich their bot strategy.
To be able to do that, we have to propose a model where data and behaviors are not tightly coupled in the Piece implementations.

So let’s go to use the visitor pattern !

We have two kinds of structure :

– the model classes that accept to be visited (the pieces)

– the visitors that visit them (moving operations)

Here is a class diagram that illustrates the pattern :

enter image description here

In the upper part we have the visitors and in the lower part we have the model classes.

Here is the PieceMovingVisitor interface (behavior specified for each kind of 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);

}

The Piece is defined now :

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

Its key method is :

void accept(PieceMovingVisitor pieceVisitor);

It provides the first dispatch : a invocation based on the Piece receiver.
At compile time, the method is bound to the accept() method of the Piece interface and at runtime, the bounded method will be invoked on the runtime Piece class.
And it is the accept() method implementation that will perform a second dispatch.

Indeed, each Piece subclass that wants to be visited by a PieceMovingVisitor object invokes the PieceMovingVisitor.visit() method by passing as argument itself.
In this way, the compiler bounds as soon as the compile time, the type of the declared parameter with the concrete type.
There is the second dispatch.
Here is the Bishop subclass that illustrates that :

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

}

And here an usage example :

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

Visitor drawbacks

The Visitor pattern is a very powerful pattern but it also has some important limitations that you should consider before using it.

1) Risk to reduce/break the encapsulation

In some kinds of operation, the visitor pattern may reduce or break the encapsulation of domain objects.

For example, as the MovePerformingVisitor class needs to set the coordinates of the actual piece, the Piece interface has to provide a way to do that :

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) Requirement to change the model

Contrary to some other behavioral design patterns as Decorator for example, the visitor pattern is intrusive.
We indeed need to modify the initial receiver class to provide an accept() method to accept to be visited.
We didn't have any issue for Piece and its subclasses as these are our classes.
In built-in or third party classes, things are not so easy.
We need to wrap or inherit (if we can) them to add the accept() method.

3) Indirections

The pattern creates multiples indirections.
The double dispatch means two invocations instead of a single one :

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

And we could have additional indirections as the visitor changes the visited object state.
It may look like a cycle :

call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)
固执像三岁 2024-07-14 21:33:13

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

  1. 减少数据结构更改时仅略有不同的代码扩散。

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

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

请查看我写的有关此问题的文章

There are at least three very good reasons for using the Visitor Pattern:

  1. Reduce proliferation of code which is only slightly different when data structures change.

  2. Apply the same computation to several data structures, without changing the code which implements the computation.

  3. Add information to legacy libraries without changing the legacy code.

Please have a look at an article I've written about this.

酷到爆炸 2024-07-14 21:33:13

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

。 访客如何帮助我们做到这一点。

示例:

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

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

假设蓝牙无线电可能来自 2 个独立的 OEM——英特尔和英特尔。 博通。

为了使示例与我们的讨论相关,我们还假设 Intel 无线电公开的 API 与 Broadcom 无线电公开的 API 不同。

这就是我的课程的样子 –

在此处输入图像描述
输入图片描述这里

现在给大家介绍一个操作——打开移动设备的蓝牙。

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

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

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

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

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

输入图像描述这里

现在,访问者模式可以应用于这个问题。 灵感来自维基百科页面,该页面指出 - “本质上,访问者允许向一系列类添加新的虚拟函数,而无需修改类本身; 相反,我们创建一个访问者类来实现虚函数的所有适当的专门化。 访问者将实例引用作为输入,并通过双重调度实现目标。”

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

设置如下 -
输入图像描述这里

我写了这个例子来回答另一个问题,代码& 此处提到了其解释。

As Konrad Rudolph already pointed out, it is suitable for cases where we need double dispatch

Here is an example to show a situation where we need double dispatch & how visitor helps us in doing so.

Example :

Lets say I have 3 types of mobile devices - iPhone, Android, Windows Mobile.

All these three devices have a Bluetooth radio installed in them.

Lets assume that the blue tooth radio can be from 2 separate OEMs – Intel & Broadcom.

Just to make the example relevant for our discussion, lets also assume that the APIs exposes by Intel radio are different from the ones exposed by Broadcom radio.

This is how my classes look –

enter image description here
enter image description here

Now, I would like to introduce an operation – Switching On the Bluetooth on mobile device.

Its function signature should like something like this –

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

So depending upon Right type of device and Depending upon right type of Bluetooth radio, it can be switched on by calling appropriate steps or algorithm.

In principal, it becomes a 3 x 2 matrix, where-in I’m trying to vector the right operation depending upon the right type of objects involved.

A polymorphic behaviour depending upon the type of both the arguments.

enter image description here

Now, Visitor pattern can be applied to this problem. Inspiration comes from the Wikipedia page stating – “In essence, the visitor allows one to add new virtual functions to a family of classes without modifying the classes themselves; instead, one creates a visitor class that implements all of the appropriate specializations of the virtual function. The visitor takes the instance reference as input, and implements the goal through double dispatch.”

Double dispatch is a necessity here due to the 3x2 matrix

Here is how the set up will look like -
enter image description here

I wrote the example to answer another question, the code & its explanation is mentioned here.

一杆小烟枪 2024-07-14 21:33:13

我发现以下链接更容易:


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。您也可以请注意,Bottle 使用unit,它是不同的数据类型,需要进行转换。

因此,在 main 方法中,您可以使用以下代码计算药丸数量:

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

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

因此,通过引入以下代码:

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 数量的责任移至名为 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 您会看到真实的场景,您可以不使用多态性(答案)来遵循单一职责原则。 事实上:

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

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

I found it easier in following links:

In
http://www.remondo.net/visitor-pattern-example-csharp/ I found an example that shows an mock example that shows what is benefit of visitor pattern. Here you have different container classes for 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; }
    }
}

As you see in above, You BilsterPack contain pairs of Pills' so you need to multiply number of pair's by 2. Also you may notice that Bottle use unit which is different datatype and need to be cast.

So in main method you may calculate pill count using following code:

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

Notice that above code violate Single Responsibility Principle. That means you must change main method code if you add new type of container. Also making switch longer is bad practice.

So by introducing following code:

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
}

You moved responsibility of counting number of Pills to class called PillCountVisitor (And we removed switch case statement). That mean's whenever you need to add new type of pill container you should change only PillCountVisitor class. Also notice IVisitor interface is general for using in another scenarios.

By adding Accept method to pill container class:

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

    #region IAcceptor Members

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

    #endregion
}

we allow visitor to visit pill container classes.

At the end we calculate pill count using following code:

var visitor = new PillCountVisitor();

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

That mean's: Every pill container allow the PillCountVisitor visitor to see their pills count. He know how to count your pill's.

At the visitor.Count has the value of pills.

In
http://butunclebob.com/ArticleS.UncleBob.IuseVisitor you see real scenario in which you can not use polymorphism (the answer) to follow Single Responsibility Principle. In fact in:

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

the reportQtdHoursAndPay method is for reporting and representation and this violate the Single Responsibility Principle. So it is better to use visitor pattern to overcome the problem.

椵侞 2024-07-14 21:33:13

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

当您可能考虑使用它时

  1. 当您有一个类族时,您知道您将必须添加许多新操作,但由于某种原因您无法更改或重新编译该类族将来。
  2. 当您想要添加新操作并在一个访问者类中完全定义该新操作而不是分布在多个类中时。
  3. 当你的老板说你必须创建一系列必须现在做某事的类时!...但实际上还没有人确切知道那件事是什么。

Quick description of the visitor pattern. The classes that require modification must all implement the 'accept' method. Clients call this accept method to perform some new action on that family of classes thereby extending their functionality. Clients are able to use this one accept method to perform a wide range of new actions by passing in a different visitor class for each specific action. A visitor class contains multiple overridden visit methods defining how to achieve that same specific action for every class within the family. These visit methods get passed an instance on which to work.

When you might consider using it

  1. When you have a family of classes you know your going to have to add many new actions them all, but for some reason you are not able to alter or recompile the family of classes in the future.
  2. When you want to add a new action and have that new action entirely defined within one the visitor class rather than spread out across multiple classes.
  3. When your boss says you must produce a range of classes which must do something right now!... but nobody actually knows exactly what that something is yet.
Spring初心 2024-07-14 21:33:13

Cay Horstmann 有一个很好的示例,说明了在何处应用 他的 OO 设计和模式书籍中的访问者。 他总结了这个问题:

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

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

FileSystem 类图

这里我们可能希望使用此结构实现一些操作(功能):

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

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

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

Visitor 允许我们将数据结构(例如,FileSystemNodes)上的功能与数据结构本身解耦。 该模式允许设计尊重内聚性——数据结构类更简单(它们具有更少的方法),并且功能也被封装到Visitor 实现中。 这是通过双重调度(这是该模式的复杂部分)完成的:在结构类和visitX()accept()方法访问者(功能)类中的代码>方法:

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

该结构允许我们添加新的功能,这些功能可以作为具体的访问者在该结构上工作(无需更改结构类)。

FileSystem class diagram with Visitor applied

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

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

Cay Horstmann has a great example of where to apply Visitor in his OO Design and patterns book. He summarizes the problem:

Compound objects often have a complex structure, composed of individual elements. Some elements may again have child elements. ... An operation on an element visits its child elements, applies the operation to them, and combines the results. ... However, it is not easy to add new operations to such a design.

The reason it's not easy is because operations are added within the structure classes themselves. For example, imagine you have a File System:

FileSystem class diagram

Here are some operations (functionalities) we might want to implement with this structure:

  • Display the names of the node elements (a file listing)
  • Display the calculated the size of the node elements (where a directory's size includes the size of all its child elements)
  • etc.

You could add functions to each class in the FileSystem to implement the operations (and people have done this in the past as it's very obvious how to do it). The problem is that whenever you add a new functionality (the "etc." line above), you might need to add more and more methods to the structure classes. At some point, after some number of operations you've added to your software, the methods in those classes don't make sense anymore in terms of the classes' functional cohesion. For example, you have a FileNode that has a method calculateFileColorForFunctionABC() in order to implement the latest visualization functionality on the file system.

The Visitor Pattern (like many design patterns) was born from the pain and suffering of developers who knew there was a better way to allow their code to change without requiring a lot of changes everywhere and also respecting good design principles (high cohesion, low coupling). It's my opinion that it's hard to understand the usefulness of a lot of patterns until you've felt that pain. Explaining the pain (like we attempt to do above with the "etc." functionalities that get added) takes up space in the explanation and is a distraction. Understanding patterns is hard for this reason.

Visitor allows us to decouple the functionalities on the data structure (e.g., FileSystemNodes) from the data structures themselves. The pattern allows the design to respect cohesion -- data structure classes are simpler (they have fewer methods) and also the functionalities are encapsulated into Visitor implementations. This is done via double-dispatching (which is the complicated part of the pattern): using accept() methods in the structure classes and visitX() methods in the Visitor (the functionality) classes:

FileSystem class diagram with Visitor applied

This structure allows us to add new functionalities that work on the structure as concrete Visitors (without changing the structure classes).

FileSystem class diagram with Visitor applied

For example, a PrintNameVisitor that implements the directory listing functionality, and a PrintSizeVisitor that implements the version with the size. We could imagine one day having an 'ExportXMLVisitor` that generates the data in XML, or another visitor that generates it in JSON, etc. We could even have a visitor that displays my directory tree using a graphical language such as DOT, to be visualized with another program.

As a final note: The complexity of Visitor with its double-dispatch means it is harder to understand, to code and to debug. In short, it has a high geek factor and goes agains the KISS principle. 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.

幽蝶幻影 2024-07-14 21:33:13

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

In my opinion, the amount of work to add a new operation is more or less the same using Visitor Pattern or direct modification of each element structure. Also, if I were to add new element class, say Cow, the Operation interface will be affected and this propagates to all existing class of elements, therefore requiring recompilation of all element classes. So what is the point?

弱骨蛰伏 2024-07-14 21:33:13

访问者模式与方面对象编程的地下实现相同。

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

Visitor Pattern as the same underground implementation to Aspect Object programming..

For example if you define a new operation without changing the classes of the elements on which it operates

岁月苍老的讽刺 2024-07-14 21:33:13

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

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

虽然它看起来不错,因为它确认了单一职责,但它违反了开放/封闭原则。 每次您有新的 Employee 类型时,您都必须添加 if 并进行类型检查。 如果你不这样做,你将永远不会在编译时知道这一点。

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

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

I didn't understand this pattern until I came across with uncle bob article and read comments.
Consider the following code:

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

While it may look good since it confirms to Single Responsibility it violates Open/Closed principle. Each time you have new Employee type you will have to add if with type check. And if you won't you'll never know that at compile time.

With visitor pattern you can make your code cleaner since it does not violate open/closed principle and does not violate Single responsibility. And if you forget to implement visit it won't compile:

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

The magic is that while v.Visit(this) looks the same it's in fact different since it call different overloads of visitor.

婴鹅 2024-07-14 21:33:13

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

同时,添加“步行”方法会产生新的问题。 “吃”还是“睡”呢? 我们真的必须为我们想要添加的每个新动作或操作添加一个新方法到 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);
        }
    }
}

Based on the excellent answer of @Federico A. Ramponi.

Just imagine you have this hierarchy:

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

What happen if you need to add a "Walk" method here? That will be painful to the whole design.

At the same time, adding the "Walk" method generate new questions. What about "Eat" or "Sleep"? Must we really add a new method to the Animal hierarchy for every new action or operation that we want to add? That's ugly and most important, we will never be able to close the Animal interface. So, with the visitor pattern, we can add new method to the hierarchy without modifying the hierarchy!

So, just check and run this C# example:

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);
        }
    }
}
往事风中埋 2024-07-14 21:33:13

我真的很喜欢 http:// /python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html

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

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

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

I really like the description and the example from http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html.

The assumption is that you have a primary class hierarchy that is fixed; perhaps it’s from another vendor and you can’t make changes to that hierarchy. However, your intent is that you’d like to add new polymorphic methods to that hierarchy, which means that normally you’d have to add something to the base class interface. So the dilemma is that you need to add methods to the base class, but you can’t touch the base class. How do you get around this?

The design pattern that solves this kind of problem is called a “visitor” (the final one in the Design Patterns book), and it builds on the double dispatching scheme shown in the last section.

The visitor pattern allows you to extend the interface of the primary type by creating a separate class hierarchy of type Visitor to virtualize the operations performed upon the primary type. The objects of the primary type simply “accept” the visitor, then call the visitor’s dynamically-bound member function.

娇女薄笑 2024-07-14 21:33:13

访客 来自维基百科

Visitor 允许人们向一系列类添加新的虚函数,而无需修改类本身; 相反,我们创建一个访问者类来实现虚拟函数的所有适当的专门化

使用访问者模式如果:

  1. 必须对不同类型的对象执行类似的操作
  2. 执行不同且不相关的操作。
  3. 必须在不更改对象结构的情况下添加新操作

代码片段:

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 和 Ludo 等具体游戏视为 ConcreteElements

在上面的示例中,Chess、Checkers 和 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

来自维基百科的 装饰器

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

相关帖子:

何时使用装饰器模式?

GoF IO 装饰器模式的用例和示例

Visitor from wikipedia

Visitor allows one to add new virtual functions to a family of classes without modifying the classes themselves; instead, one creates a visitor class that implements all of the appropriate specializations of the virtual function

Use Visitor pattern if:

  1. Similar operations have to be performed on objects of different types
  2. Execute distinct and unrelated operations.
  3. New operations have to be added without change in object structure

Code snippet:

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

Explanation:

  1. Visitable (Element) is an interface and this interface method has to be added to a set of classes.
  2. Visitor is an interface, which contains methods to perform an operation on Visitable elements.
  3. GameVisitor is a class, which implements Visitor interface ( ConcreteVisitor).
  4. Each Visitable element accept Visitor and invoke a relevant method of Visitor interface.
  5. You can treat Game as Element and concrete games like Chess,Checkers and Ludo as ConcreteElements.

In above example, Chess, Checkers and Ludo are three different games ( and Visitable classes). On one fine day, I have encountered with a scenario to log statistics of each game. So without modifying individual class to implement statistics functionality, you can centralise that responsibility in GameVisitor class, which does the trick for you without modifying the structure of each game.

output:

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

Decorator from wikipedia

pattern allows behaviour to be added to an individual object, either statically or dynamically, without affecting the behaviour of other objects from the same class

Related posts:

When to Use the Decorator Pattern?

Use Cases and Examples of GoF Decorator Pattern for IO

转角预定愛 2024-07-14 21:33:13

虽然我知道如何以及何时发生,但我从来不明白为什么。 如果它对具有 C++ 等语言背景的任何人有帮助,您需要阅读本文很小心。

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

或者,换句话说,确保在传入实际绑定到 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;
  }
}

While I have understood the how and when, I have never understood the why. In case it helps anyone with a background in a language like C++, you want to read this very carefully.

For the lazy, we use the visitor pattern because "while virtual functions are dispatched dynamically in C++, function overloading is done statically".

Or, put another way, to make sure that CollideWith(ApolloSpacecraft&) is called when you pass in a SpaceShip reference that is actually bound to an ApolloSpacecraft object.

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;
  }
}
苏大泽ㄣ 2024-07-14 21:33:13

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

Thanks for the awesome explanation of @Federico A. Ramponi, I just made this in java version. Hope it might be helpful.

Also just as @Konrad Rudolph pointed out, it's actually a double dispatch using two concrete instances together to determine the run-time methods.

So actually there is no need to create a common interface for the operation executor as long as we have the operation interface properly defined.

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

As you expect, a common interface will bring us more clarity though it's actually not the essential part in this pattern.

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");
    }
}
独行侠 2024-07-14 21:33:13

你的问题是何时知道:

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

您可以从此处的示例数量中了解如何实现它,我只是向您展示一个用例。

your question is when to know:

i do not first code with visitor pattern. i code standard and wait for the need to occur & then refactor. so lets say you have multiple payment systems that you installed one at a time. At checkout time you could have many if conditions (or instanceOf) , for example :

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

now imagine i had 10 payment methods, it gets kind of ugly. So when you see that kind of pattern occuring visitor comes in handly to seperate all that out and you end up calling something like this afterwards:

new PaymentCheckoutVistor(paymentType).visit()

You can see how to implement it from the number of examples here, im just showing you a usecase.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文