我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。
作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。
我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。
作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。
当前回答
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 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. 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. 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.
其他回答
访问者模式作为方面对象编程的地下实现。
例如,如果您定义一个新操作,而不改变其操作的元素的类
双重分派只是使用此模式的众多原因之一。 但请注意,这是在使用单一分派范式的语言中实现双分派或多分派的唯一方法。
以下是使用该模式的原因:
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允许用户在不修改类本身的情况下向类族中添加新的虚函数;相反,创建一个实现虚函数的所有适当专门化的访问者类
参观者结构:
在以下情况下使用访问者模式:
必须对结构中分组的不同类型的对象执行类似的操作 您需要执行许多不同且不相关的操作。它将操作从对象结构中分离出来 必须在不改变对象结构的情况下添加新操作 将相关操作集合到一个类中,而不是强迫您更改或派生类 向没有源或不能更改源的类库中添加函数
尽管访问者模式提供了在不改变Object中现有代码的情况下添加新操作的灵活性,但这种灵活性也带来了缺点。
如果添加了新的Visitable对象,则需要在Visitor和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);
}
}
}
解释:
Visitable (Element)是一个接口,该接口方法必须添加到一组类中。 Visitor是一个接口,它包含对可访问元素执行操作的方法。 GameVisitor是一个实现Visitor接口(ConcreteVisitor)的类。 每个可访问元素接受Visitor并调用Visitor接口的相关方法。 你可以将游戏视为元素,而将象棋、跳棋和Ludo等具体游戏视为具体元素。
在上述例子中,国际象棋、西洋跳棋和卢多是三种不同的游戏(以及可访问的职业)。在一个晴朗的日子里,我遇到了一个记录每款游戏统计数据的场景。所以无需修改单个类来实现统计功能,你可以将这一职责集中到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文章
sourcemaking文章
欲知详情
装饰
模式允许将行为静态或动态地添加到单个对象,而不影响来自同一类的其他对象的行为
相关文章:
IO的装饰器模式
什么时候使用装饰器模式?
使用访问者模式至少有三个很好的理由:
减少代码的增殖,当数据结构发生变化时,代码只会略有不同。 将相同的计算应用于多个数据结构,而不改变实现计算的代码。 在不更改遗留代码的情况下向遗留库添加信息。
请看看我写的一篇关于这方面的文章。
我发现下面的链接更容易:
在 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对的数量。此外,你可能会注意到瓶使用单位是不同的数据类型,需要强制转换。
所以在主要方法中,您可以使用以下代码计算药丸计数:
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;
}
}
注意,上面的代码违反了单一责任原则。这意味着如果添加新类型的容器,则必须更改主方法代码。同时,延长开关时间也是不好的做法。
通过引入以下代码:
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
}
您将计数药丸数量的责任转移到名为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访问者查看他们的药片计数。他知道怎么数你的药。
看着来访者。伯爵有药丸的价值。
在 http://butunclebob.com/ArticleS.UncleBob.IuseVisitor你看到了真实的场景,你不能使用多态性(答案)来遵循单一责任原则。事实上在:
public class HourlyEmployee extends Employee {
public String reportQtdHoursAndPay() {
//generate the line for this hourly employee
}
}
reportQtdHoursAndPay方法用于报告和表示,这违反了单一责任原则。因此,最好利用访问者模式来解决这一问题。