何时应该使用访问者设计模式?

345
我经常在博客中看到访问者模式的参考,但我必须承认,我不理解它。我阅读了wikipedia文章中的内容,我理解了它的机制,但我仍然不清楚何时使用它。
作为最近真正理解装饰器模式并且现在无处不见其用途的人,我希望能够直观地理解这个看似方便的模式。

8
在大堂等待两个小时后,我通过黑莓手机阅读了Jeremy Miller的这篇文章,并最终理解了其中介绍的双重分派、访问者模式和组合模式,这篇长文精彩地解释了它们的作用及其应用。 - George Mauer
1
这是一篇不错的文章:http://www.codeproject.com/Articles/186185/Visitor-Design-Pattern - Seyed Morteza Mousavi
示例在此处在此处 - jaco0646
当容器或可迭代对象的不同元素类型具有共同联系、即它们是内聚的,且这些不同行为变化有很好的理由同时发生时,可以使用访问者模式来指定不同类型的行为。当您需要通过开闭原则添加功能时,只需添加一个新的访问者类即可。 - ChrisoLosoph
20个回答

327

我对访问者模式不是很熟悉,请看我是否理解正确。假设您有一组动物的层次结构

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

最后,我们实现了实际操作,而不修改猫或狗

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

16
@Knownasilya - 这不是真的。&-运算符会给出Sound对象的地址,这是接口所需的。 letsDo(Operation *v) 需要一个指针。 - AquilaRapax
3
为了更清晰明了,这个访问者设计模式的例子是否正确? - godzilla
5
@Federico A. Ramponi,这听起来就像是把复杂性转移到了操作类型。假设您添加了数十个或数百个其他操作,然后稍后再添加另一种动物类型,我们称之为Sheep。那么这将迫使您修改所有为Sheep创建的这些操作,并为所有操作添加hereIsASheep(Sheep *d);。 - supertonsky
5
经过深思熟虑,我想知道为什么你在这里调用了两个方法HereIsADog和HereIsACat,尽管你已经将Dog和Cat传递到了这些方法中。我更喜欢一个简单的performTask(Object *obj)方法,并在Operation类中对此对象进行强制类型转换(在支持覆盖的语言中,不需要进行强制类型转换)。 - Abdalrahman Shatou
7
在你最后的“主”示例中:theSound.hereIsACat(c)可以完成工作,你如何为引入的所有开销辩护? 双重分派是其理由。 双重分派是这个设计模式的正当理由。 - franssu
显示剩余12条评论

143
你感到困惑的原因可能是"Visitor"这个术语起得不好。许多(著名的!)程序员都在这个问题上跌倒过。它实际上是在那些不支持它本地化的语言中实现双重派发的方法(大多数语言都不支持)。

1) 我最喜欢的例子是Scott Meyers,他是“Effective C ++”的著名作者,他将这个称为他有史以来最重要的C ++ aha!时刻之一。


6
+1 "there is no pattern" - 这是完美的回答。最受欢迎的答案证明了许多C++程序员尚未意识到使用类型枚举和switch case(C语言方式)实现“特定情况”多态性的限制。使用虚函数可能更加整洁和隐形,但仍然受限于单分派。在我个人看来,这是C++最大的缺陷。 - user3125280
1
@user3125280 我已经阅读了4/5篇文章和有关访问者模式的设计模式章节,但是它们都没有解释为什么要使用这种晦涩的模式而不是case语句,或者在何时使用其中之一。感谢您至少提出了这个问题! - spinkus
5
@sam,我非常确定他们解释了 - 这是从子类化/运行时多态性中始终获得的相同优势 ,与switch相比:switch在客户端硬编码决策(代码重复),并且不提供静态类型检查(检查情况的完整性和差异等)。访问者模式由类型检查器验证,并且通常使客户端代码更简单。 - Konrad Rudolph
@KonradRudolph 谢谢你的回复。不过需要注意的是,这并没有在《设计模式》或维基百科文章中得到明确阐述。我不反对你的看法,但是你可以争论使用case语句也有好处,因此奇怪的是它通常没有进行对比:1.集合对象不需要一个accept()方法。2.访问者可以处理未知类型的对象。因此,case语句似乎更适合于操作具有可变类型集合的对象结构。《设计模式》承认访问者模式不适用于这种情况(p333)。 - spinkus
1
@SamPinkus konrad的观点很准确 - 这就是为什么像virtual这样的特性在现代编程语言中非常有用 - 它们是可扩展程序的基本构建块 - 在我看来,如果代码不需要可扩展性,那么c方式(嵌套开关或模式匹配等,具体取决于您选择的语言)要干净得多,我很高兴地看到像prover 9这样复杂软件中出现了这种风格。更重要的是,任何想要提供可扩展性的语言都应该容纳比递归单分派(即访问者)更好的调度模式。 - user3125280

93

大家的回答都是正确的,但我觉得他们没有解决“何时”的问题。首先,来自《设计模式》的定义:

访问者模式可以在不改变操作元素类的前提下定义新的操作。

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

现在,面向对象设计假设你更可能增加新的类而不是新的方法,所以添加更多行比较容易。你只需要添加一个新类,指定该类中不同的部分,并继承其余部分。

然而,有时候类相对静态,但你需要经常添加更多的方法——添加列。在面向对象设计中,标准的做法是将这些方法添加到所有类中,这可能代价很高。访问者模式使这个过程变得容易。

顺便说一下,这也是Scala模式匹配旨在解决的问题。


1
为什么我要使用访问者模式而不是一个实用类?我可以像这样调用我的实用类:AnalyticsManger.visit(someObjectToVisit),与AnalyticsVisitor.visit(someOjbectToVisit)有什么区别?它们都能实现关注点分离,对吧?希望你能帮忙解答。 - j2emanue
2
@j2emanue 因为访问者模式在运行时使用正确的访问者重载。而你的代码需要类型转换才能调用正确的重载。 - Access Denied
这样做是否可以提高效率?我猜它避免了强制转换,这是一个好主意。 - j2emanue
1
@j2emanue,这个想法是编写符合开闭原则的代码,而不是出于性能方面的考虑。请参阅 Uncle Bob 的开闭原则文章:http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod - Access Denied
@Daniel,你讲解得非常好。你是一个天生的教师。 - RamPrakash

23

访问者设计模式非常适用于像目录树、XML结构或文档大纲这样的“递归”结构。

访问者对象访问递归结构中的每个节点:每个目录、每个XML标记,以此类推。访问者对象不会遍历整个结构。相反,访问者方法应用于结构的每个节点。

下面是一个典型的递归节点结构。它可以是目录或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方法会将一个访问者对象应用于结构中的每个节点。在这种情况下,它是一个自顶向下的访问者。您可以更改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。它创建了一个访问者dumpNodes.

然后将其应用于树上。这个dumpNode对象将“访问”树中的每个节点。

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

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


9
正如其他人所说,这是“层次化访问者模式”。 - PPC-Coder
2
@PPC-Coder,“分层访问者模式”和“访问者模式”有什么区别? - Tim Lovell-Smith
4
分层访问者模式比经典访问者模式更加灵活。例如,使用分层模式可以追踪遍历的深度,并决定要遍历哪个分支或停止完全遍历。经典访问者没有这个概念,会访问所有节点。 - PPC-Coder

22

从某种角度来看,访问者模式是一种让客户端为特定类层次结构中的所有类添加额外方法的方式。

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

经典的例子是编译器及其类似工具。抽象语法树(AST)可以准确地定义编程语言的结构,但您可能希望在项目不断发展时改变对AST所做的操作:代码生成器、美化打印程序、调试器、复杂度分析等。

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

(有人认为访问者模式与良好的OO实践相冲突,因为它将数据的操作移开了数据。访问者模式在普通OO实践失败的情况下非常有用。)


我也想听听您的意见:为什么我要使用访问者模式而不是仅仅使用一个实用类。我可以像这样调用我的实用类:AnalyticsManger.visit(someObjectToVisit),与AnalyticsVisitor.visit(someOjbectToVisit)有什么区别呢?它们都能实现关注点分离,对吧?希望您能帮忙解答。 - j2emanue
@j2emanue:我不明白你的问题。我建议你详细阐述并将其发布为一个完整的问题,以便任何人都可以回答。 - Oddthinking
1
我在这里发布了一个新问题:https://stackoverflow.com/questions/52068876/visitor-pattern-vs-using-using-a-seperate-class-what-is-the-difference - j2emanue

19

双重分派(Double dispatch)只是使用该模式的一个原因,而不是唯一原因。但请注意,在使用单调派范式的语言中,它是实现双或多重分派的唯一方式。

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

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() {
        ...
    }

}

所有棋子的子类也是一样。以下是一个类图,展示了这种设计:

[model class diagram

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

1. performMove()computeIfKingCheck()等行为很可能会使用共同的逻辑,例如,不论具体的棋子是什么,performMove()最终都会把当前棋子移动到特定位置,并有可能拿下对手的棋子。将相关的行为拆分成多个类而不是集中在一起,不完全符合单一职责模式,使得可维护性变差。

2. 棋子子类不能观察、更改程序运行时的checkMoveValidity()检验结果。这个检测超越了人类或计算机可以决策的范围,在玩家每次请求移动操作时执行,以确保被请求的棋子移动是有效的。因此,我们甚至不想在Piece接口中提供它。
3. 在对 bot 开发者挑战的国际象棋游戏中,通常应用程序会提供一个标准 API(Piece接口、子类、Board、共同的行为等),并让开发者丰富其 bot 策略。为了能够达到这个目标,我们必须提出一个数据和行为不紧密耦合在Piece实现中的模型。
因此,让我们使用访问者模式吧!
我们有两种结构:
- 接受访问的模型类(棋子)。 - 访问它们的访问者(移动操作)。
以下是展示该模式的类图:

enter image description here

上半部分是访问者,下半部分是模型类。
以下是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.accept()中执行后处理。这将是一个非常糟糕的想法,因为它会在Visitor实现和Piece子类之间创建高耦合,并且可能需要使用getClass()instanceof或标记来识别Visitor实现等技巧。

2) 更改模型的要求

与其他一些行为设计模式(如Decorator)相反,访问者模式是有侵入性的。
我们确实需要修改最初的接收方类以提供accept()方法以接受访问。
对于内置或第三方类,情况并不那么容易。
我们需要包装或继承(如果可以的话)它们以添加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)

14

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

  1. 减少当数据结构变化时,代码仅略微不同的扩散。

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

  3. 向遗留库添加信息而不更改遗留代码。

请查看我写的一篇文章了解更多信息。


1
我在你的文章中评论了我看到过的访客最大的用途。你有什么想法? - George Mauer

13
如Konrad Rudolph所指出的那样,它适用于需要双重分派的情况。
以下是一个示例,展示了我们需要双重分派的情况以及访问者如何帮助我们做到这一点。
示例:
假设我有3种类型的移动设备 - iPhone、Android、Windows Mobile。
所有这三个设备都安装了蓝牙无线电。
假设蓝牙无线电可以来自2个不同的OEM - Intel和Broadcom。
为了使示例与我们的讨论相关,请还假设Intel无线电公开的API与Broadcom无线电公开的API不同。
这是我的类的外观 -

enter image description here enter image description here

现在,我想介绍一个操作 - 打开移动设备上的蓝牙。
它的函数签名应该像这样 -
 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

所以,根据正确的设备类型正确的蓝牙无线电类型,可以通过调用适当的步骤或算法打开它。
原则上,它变成了一个3 x 2矩阵,在其中我试图根据涉及到的正确对象类型进行向量操作。
一种多态行为,取决于两个参数的类型。

enter image description here

现在,访问者模式可以应用于这个问题。灵感来自维基百科页面所述-“本质上,访问者允许向类族添加新的虚拟函数而无需修改类族本身;相反,创建一个实现虚拟函数所有适当特化的访问者类。访问者将实例引用作为输入,并通过双重分派实现目标。”
由于3x2矩阵,在这里需要双重分派。
设置如下- enter image description here 我写了这个例子来回答另一个问题,代码及其说明在此处提到 here

9
我在以下链接中发现更容易的内容:
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,需要进行类型转换。
因此,在主方法中,您可以使用以下代码计算药丸数量:
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 访问者查看它们的药丸数量。他知道如何计算您的药丸数量。
visitor.Count 中有药丸数量的值。
http://butunclebob.com/ArticleS.UncleBob.IuseVisitor 中,您可以看到真实的场景,其中您无法使用多态性(答案)遵循单一职责原则。事实上,在:
public class HourlyEmployee extends Employee {
  public String reportQtdHoursAndPay() {
    //generate the line for this hourly employee
  }
}

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


2
嗨,Sayed,你能否编辑你的答案并添加你认为最有启发性的部分。因为 Stack Overflow 通常不鼓励仅包含链接的答案,因为其目标是成为一个知识数据库,而链接可能会失效。 - George Mauer

7

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

何时考虑使用它

  1. 当你有一个类族,你知道你将不得不为它们添加许多新操作,但出于某种原因,你不能在未来更改或重新编译类族时。
  2. 当你想添加一个新操作,并且希望将该新操作完全定义在一个访问者类中,而不是分散在多个类中。
  3. 当你的老板说你必须立即创建一系列必须做某些事情的类,但实际上没有人知道那件事情到底是什么。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接