我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。
作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。
我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。
作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。
当前回答
使用访问者模式至少有三个很好的理由:
减少代码的增殖,当数据结构发生变化时,代码只会略有不同。 将相同的计算应用于多个数据结构,而不改变实现计算的代码。 在不更改遗留代码的情况下向遗留库添加信息。
请看看我写的一篇关于这方面的文章。
其他回答
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方法的参数。
你的问题是什么时候知道:
我不首先编码访问者模式。我编写标准代码,等待需求的出现,然后重构。假设你有多个支付系统,一次安装一个。在签出时,你可以有很多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()
你可以看到如何实现它从这里的例子的数量,我只是向你展示一个用例。
双重分派只是使用此模式的众多原因之一。 但请注意,这是在使用单一分派范式的语言中实现双分派或多分派的唯一方法。
以下是使用该模式的原因:
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)
你困惑的原因可能是来客是一个致命的用词不当。许多(杰出的)程序员都曾遇到过这个问题。它实际做的是用本身不支持它的语言(大多数语言不支持)实现双重调度。
1)我最喜欢的例子是《Effective c++》的作者Scott Meyers,他称这是他最重要的c++啊哈之一!的时刻。
在我看来,使用访问者模式或直接修改每个元素结构添加新操作的工作量大致相同。此外,如果我要添加新的元素类,比如Cow,操作接口将受到影响,并且这将传播到所有现有的元素类,因此需要重新编译所有元素类。那么重点是什么呢?