双重调度只是使用这种模式的一个原因。
但请注意,这是在使用单一调度范式的语言中实现双重或多重调度的单一方式。
以下是使用该模式的原因:
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 接口中提供它。
– 在对 bot 开发者具有挑战性的国际象棋游戏中,通常应用程序提供标准 API(Piece 接口、子类、Board、常用行为等),并让开发者丰富他们的 bot 策略。
为了能够做到这一点,我们必须提出一个模型,其中数据和行为在 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);
}
片段现在定义了:
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 坐标更改的责任现在对Piece 子类以外的其他类开放。
在Piece 子类中移动访问者执行的处理也不是一种选择。
它确实会产生另一个问题,因为Piece.accept() 接受任何访问者实现。它不知道访问者执行了什么操作,因此不知道是否以及如何更改 Piece 状态。
一种识别访问者的方法是根据访问者实现在Piece.accept() 中执行后处理。这将是一个非常糟糕的主意,因为它会在访问者实现和 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)