什么时候应该使用访客设计模式?[关闭]


315

我一直在博客中看到对访客模式的引用,但我必须承认,我只是不明白。我阅读了有关该模式Wikipedia文章,并且了解了该模式的原理,但是对于何时使用它仍然感到困惑。

作为刚刚真正获得装饰器模式并且现在在任何地方都可以看到其用途的人,我也希望能够真正直观地理解这种看似方便的模式。


7
在Jermey Miller在我的黑莓手机上阅读这篇文章后,终于在大厅里等待了两个小时而终于明白了。篇幅虽然很长,但是却很好地解释了双重派遣,访问者和复合派遣,以及您可以使用这些派遣做什么。
乔治·莫尔


3
访客模式?哪一个?关键是:这种设计模式存在很多误解和纯粹的困惑。我写过一篇文章,希望能为这场混乱带来一些秩序:rgomes-info.blogspot.co.uk/2013/01/…–
理查德·戈麦斯

如果要在联合数据类型上具有功能对象,则将需要访问者模式。您可能想知道什么是函数对象和联合数据类型,那么值得阅读ccs.neu.edu/home/matthias/htdc.html
Wei Qiu

Answers:


315

我对访客模式不是很熟悉。让我们看看我是否正确。假设您有动物的等级制度

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

19
S.Lott,走一棵树实际上不是访客模式。(这是完全不同的“分层访问者模式”。)如果不使用继承或接口实现,就无法显示GoF访问者模式。
优厚的

14
@Knownasilya-那不是真的。&-运算符提供接口所需要的声音对象的地址。letsDo(Operation *v) 需要一个指针。
AquilaRapax

3
只是为了清楚起见,此访客设计模式示例是否正确?
哥斯拉

4
经过大量的思考,我想知道为什么您已经在此方法中将Dog和Cat传递给了方法,但为什么在这里调用了IsADog和hereIsACat这两个方法。我希望使用一个简单的performTask(Object * obj),然后在Operation类中强制转换此对象。(并且使用支持替代的语言,无需强制转换)
Abdalrahman Shatou 2013年

6
在最后的“主要”示例中:theSound.hereIsACat(c)本来可以完成工作,您如何证明该模式所带来的所有开销呢?双重调度是合理的。
franssu 2014年

131

您感到困惑的原因可能是访客是致命的误称。许多(突出显示1!)程序员都偶然发现了这个问题。它实际上所做的是用本机不支持(大多数都不支持)的语言实现双重调度


1)我最喜欢的例子是“有效C ++”的著名作者Scott Meyers,他称这是他最重要的C ++之一!曾经的时刻


3
+1“没有模式”-完美的答案。最受支持的答案证明,许多c ++程序员尚未意识到使用类型枚举和转换大小写(c方式)对虚拟函数的局限性优于“即席”多态性。使用虚拟可能会更整洁和看不见,但仍仅限于单一调度。我个人认为,这是c ++的最大缺陷。
user3125280 2014年

@ user3125280我现在已经阅读了4/5文章和“访问者”模式上的“设计模式”一章,它们都没有解释使用这种晦涩的模式相对于case stmt的优势,或者您可能会在一个案例中使用它。至少要提出来!
spinkus 2014年

4
@sam我敢肯定,他们解释-这是相同的优势,你总是从继承/运行时多态性switchswitch硬编码使得在客户端(代码重复)的决定,并没有提供静态类型检查(检查案件的完整性和独特性等)。访问者模式由类型检查器验证,通常使客户端代码更简单。
康拉德·鲁道夫

@KonradRudolph对此表示感谢。但是请注意,例如在Patterns或Wikipedia文章中未明确解决。我不同意您的看法,但是您可以说使用case stmt也有好处,因此它的奇怪之处通常没有被对比:1.您不需要在集合对象上使用accept()方法。2.〜visitor可以处理未知类型的对象。因此,stmt的案例似乎更适合于对具有可变类型集合的对象结构进行操作。模式确实承认,访客模式不太适合这种情况(p333)。
spinkus 2014年

1
@SamPinkus konrad的观点-这就是为什么virtual类似的功能在现代编程语言中如此有用的原因-它们是可扩展程序的基本构建块-我认为c方式(嵌套开关或模式匹配等,取决于您选择的语言)是不需要扩展的代码中的代码更简洁,而令我惊讶的是,在证明者9之类的复杂软件中看到了这种风格。更重要的是,任何想要提供扩展性的语言都应该比递归单一调度(例如,游客)。
user3125280

84

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

通过Visitor,您可以定义新操作,而无需更改其所操作元素的类。

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

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

尽管有时这些类是相对静态的,但是您需要经常添加更多方法-添加列。OO设计中的标准方法是将这样的方法添加到所有类中,这可能会很昂贵。访客模式使此操作变得容易。

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


为什么我只在通用类上使用访客模式。我可以这样调用我的实用程序类:AnalyticsManger.visit(someObjectToVisit)与AnalyticsVisitor.visit(someOjbectToVisit)。有什么不同 ?他们俩都分开关注吗?希望能对您有所帮助。
j2emanue '18年

@ j2emanue因为访客模式在运行时使用正确的访客重载。虽然您的代码需要进行类型转换以调用正确的重载。
拒绝访问

有效率的提高吗?我想它避免了铸造好主意
j2emanue

@ j2emanue的想法是编写符合开放/封闭原则而不是性能原因的代码。见叔叔Bob butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
访问

22

游客设计模式可以很好地表现就像目录树,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方法调用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方法的参数。


8
正如其他人所说的那样,这就是“分层访客模式”。
PPC-Coder

1
@ PPC-Coder“分层访客模式”和访客模式之间有什么区别?
Tim Lovell-Smith

3
分层的访客模式比经典的访客模式更加灵活。例如,使用分层模式,您可以跟踪遍历的深度并确定要遍历或停止遍历的分支。经典访问者没有这个概念,将访问所有节点。
PPC-Coder

18

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

当您拥有一个相当稳定的类层次结构,但是对于该层次结构需要完成的操作的需求不断变化时,这将很有用。

经典示例适用于编译器等。抽象语法树(AST)可以准确地定义编程语言的结构,但是您可能希望对AST进行的操作会随着项目的进展而改变:代码生成器,漂亮打印机,调试器,复杂性指标分析。

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

(我听说它辩称,Visitor模式与良好的OO实践相冲突,因为它使数据的操作远离数据。在正常的OO实践失败的情况下,Visitor模式非常有用。)


我也希望您对以下内容提出意见:为什么我只在通用类上使用访客模式。我可以这样调用我的实用程序类:AnalyticsManger.visit(someObjectToVisit)与AnalyticsVisitor.visit(someOjbectToVisit)。有什么不同 ?他们俩都分开关注吗?希望能对您有所帮助。
j2emanue

@ j2emanue:我不明白这个问题。我建议您将其充实并发布为完整问题,以供任何人回答。
Oddthinking

1
我在这里发布了一个新问题: stackoverflow.com/questions/52068876/…–
j2emanue

14

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

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

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

  3. 将信息添加到旧版库,而无需更改旧版代码。

请看我写的关于这个的文章


1
我评论了您的文章,这是我见过的访客最大的用途。有什么想法吗?
George Mauer 2013年

13

正如Konrad Rudolph指出的那样,它适合需要双重派遣的情况

这是一个示例,显示了我们需要双重调度以及访客如何帮助我们的情况。

范例:

可以说我有3种类型的移动设备-iPhone,Android,Windows Mobile。

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

假设蓝牙无线电可以来自两个独立的OEM:英特尔和Broadcom。

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

这是我班级的样子–

在此处输入图片说明 在此处输入图片说明

现在,我要介绍一个操作–在移动设备上打开蓝牙。

它的功能签名应该像这样–

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

所以,这取决于权类型的设备,并根据不同类型正确蓝牙无线电的,它可以通过接通调用适当的步骤或算法

原则上,它变成一个3 x 2的矩阵,其中,我试图根据所涉及对象的正确类型对正确的操作进行矢量化处理。

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

在此处输入图片说明

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

由于采用3x2矩阵,因此必须进行双重调度

设置如下所示- 在此处输入图片说明

我编写示例以回答另一个问题,此处提到了代码及其解释。


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。另外,您可能会注意到Bottleuse 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;
    }
}

请注意,以上代码违反Single Responsibility Principle。这意味着,如果添加新类型的容器,则必须更改主方法代码。延长开关时间也是一个坏习惯。

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

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
}

您将计数Pills的责任移到了称为的类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
嗨,您好,能否请您编辑答案以添加您认为最有启发性的部分。SO通常不鼓励仅链接的答案,因为目标是成为一个知识数据库并且链接断开。
George Mauer 2014年

8

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

这是使用此模式的原因:

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()很可能会使用通用逻辑。
例如,无论哪种混凝土PieceperformMove()最终都会将当前棋子设置到特定位置,并有可能拿走对手棋子。
将相关行为拆分为多个类,而不是收集它们,以某种方式击败了单一责任模式。使其难以维护。

– 子类可能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);

}

现在定义了作品:

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

其关键方法是:

void accept(PieceMovingVisitor pieceVisitor);

它提供了第一个调度:基于Piece接收方的调用。
在编译时,该方法绑定到accept()Piece接口的方法,并且在运行时,将在运行时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界面必须提供一种方法:

void setCoordinates(Coordinates coordinates);

Piece坐标更改的责任现在对子类以外的其他类开放Piece
移动访问者在Piece子类中执行的处理也不是一种选择。
的确,由于Piece.accept()接受任何访客实现,这确实会带来另一个问题。它不知道访问者执行什么操作,因此不知道是否以及如何更改Piece状态。
识别访客的一种方法是Piece.accept()根据访客的实现方式执行后处理。这将是一个非常糟糕的主意,因为它会在Visitor实现和Piece子类之间建立高度的耦合,此外,可能还需要使用rick作为getClass()instanceof或使用任何标记来标识Visitor实现。

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)

6

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

复合对象通常具有由单个元素组成的复杂结构。一些元素可能再次具有子元素。...对元素的操作将访问其子元素,将其应用于该元素,然后合并结果。...但是,要在这种设计中添加新的操作并不容易。

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

FileSystem类图

这是我们可能要使用此结构实现的一些操作(功能):

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

您可以向FileSystem中的每个类添加函数以实现操作(并且过去人们很容易做到这一点,因此人们过去已经这样做了)。问题在于,无论何时添加新功能(上面的“ etc.”行),都可能需要向结构类添加越来越多的方法。在某些时候,在您向软件中添加了一些操作之后,就类的功能衔接而言,这些类中的方法不再有意义。例如,您有一个FileNode具有方法的calculateFileColorForFunctionABC(),在文件系统上实现最新的可视化功能。

访客模式(就像许多设计模式一样)源于开发人员的痛苦和苦难。他们知道,有一种更好的方法可以允许对其代码进行更改,而不需要在各处进行大量更改,并且尊重良好的设计原则(高内聚,低耦合)。 )。我认为,直到您感到那种痛苦,才很难理解许多模式的用处。解释痛苦(就像我们上面试图对添加的“ etc.”功能所做的那样)占用了解释空间,这是分心的。因此,很难理解模式。

访问者使我们能够将数据结构(例如FileSystemNodes)上的功能与数据结构本身分离开来。该模式允许设计尊重内聚性-数据结构类更简单(它们的方法较少),并且功能也封装在Visitor实现中。这是通过双重调度(这是模式的复杂部分)完成的:使用accept()结构类中的visitX()方法和Visitor(功能)类中的方法:

应用了Visitor的FileSystem类图

这种结构使我们可以添加新功能,以具体的访客身份在结构上工作(无需更改结构类)。

应用了Visitor的FileSystem类图

例如,一个PrintNameVisitor实现目录列表功能的,一个PrintSizeVisitor实现具有大小的版本的。我们可以想象1天具有“ExportXMLVisitor`生成XML中的数据,或者产生它JSON一位来访者,等等。我们甚至可以有客人,其中显示使用我的目录树的图形语言,如DOT,以可视化与另一个程序。

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


我认为目录结构是一种很好的组合模式,但与您的最后一段相同。
zar

5

我认为,使用Visitor Pattern或直接修改每个元素结构来添加新操作的工作量几乎相同。另外,如果我要添加新的元素类,例如Cow,Operation接口将受到影响,并且会传播到所有现有的元素类,因此需要重新编译所有元素类。那有什么意义呢?


4
当您遍历对象层次结构时,几乎每次使用访客时都是如此。考虑一个嵌套的树菜单。您要折叠所有节点。如果您没有实现访问者,则必须编写图遍历代码。或与访客:rootElement.visit (node) -> node.collapse()。使用visitor,每个节点都为其所有子节点实现图遍历,因此您已完成。
George Mauer 2013年

@GeorgeMauer,双重派遣的概念为我清除了动机:要么依赖于类型的逻辑伴随着类型,要么痛苦的世界。分布遍历逻辑的想法仍然让我停下来。效率更高吗?它更容易维护吗?如果必须添加“折叠到N级”怎么办?
nik.shornikov 2015年

@ nik.shornikov的效率在这里实际上应该不是问题。在几乎所有语言中,几个函数调用的开销都可以忽略不计。除此之外,还可以进行微优化。它更容易维护吗?这要看情况。我认为大多数时候是这样,有时不是。至于“折至N级”。轻松传递levelsRemaining计数器作为参数。在呼叫下一级孩子之前将其减小。在您的访客内部if(levelsRemaining == 0) return
George Mauer 2015年

1
@GeorgeMauer,完全同意效率是一个小问题。但是我认为应该将决定归结为可维护性,例如覆盖接受签名。
nik.shornikov,2015年

5

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

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


提起Aspect对象编程
里程碑

5

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

当您考虑使用它时

  1. 当您拥有一类班级时,您知道自己将不得不添加许多新动作,但是由于某些原因,您将来将无法更改或重新编译这一系列班级。
  2. 当您想添加一个新动作并在一个访问者类中完全定义该新动作时,而不是分散在多个类中。
  3. 当你的老板说,你必须生产出一系列必须做点什么类现在!...但没有人真正知道的东西到底是什么呢。

4

在碰到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类型时,如果要进行类型检查,则必须添加。如果您不这样做,那么在编译时就永远不会知道这一点。

使用访问者模式,您可以使代码更整洁,因为它不违反打开/关闭原则,也不违反单一职责。而且,如果您忘记实现访问,它将无法编译:

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)外观相同,但实际上却有所不同,因为它调用了不同的访问者重载。


是的,我特别发现它在使用树结构时有用,而不仅仅是平面列表(平面列表是树的特例)。正如您所注意到的,它不仅在列表上非常混乱,而且随着节点之间的导航变得更加复杂,访问者可以成为救星
George Mauer

3

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

如果您需要在此处添加“步行”方法会怎样?这将给整个设计带来痛苦。

同时,添加“步行”方法会产生新的问题。那“吃”或“睡觉”呢?我们是否真的必须为要添加的每个新动作或操作向动物层次结构中添加新方法?这很丑陋,最重要的是,我们将永远无法关闭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);
        }
    }
}

走路,吃饭是不适合的例子,因为他们是常见的两种Dog,以及Cat。您可以在基类中创建它们,以便它们被继承或选择合适的示例。
Abhinav Gauniyal '16

声音不同,采样很好,但不确定是否与访客模式有关
DAG

3

游客

访问者允许在不修改类本身的情况下将新的虚函数添加到类家族中。而是创建一个访问者类,该类实现虚拟函数的所有适当专业化

访客结构:

在此处输入图片说明

在以下情况下使用访问者模式:

  1. 必须执行类似的操作结构中分组的不同类型的对象
  2. 您需要执行许多不同且不相关的操作。它将操作与对象分开
  3. 必须添加新操作而不更改对象结构
  4. 将相关操作汇总到一个类中,而不用强迫您更改或派生类
  5. 将函数添加到您没有源代码或无法更改源代码的类库

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

说明:

  1. VisitableElement)是一个接口,此接口方法必须添加到一组类中。
  2. Visitor 是一个接口,其中包含在其上执行操作的方法 Visitable元素。
  3. GameVisitor 是一个类,它实现 Visitor接口(ConcreteVisitor)的类。
  4. Visitable元素都接受Visitor并调用相关的方法Visitor接口。
  5. 您可以将具体游戏Game视为ElementChess,Checkers and LudoConcreteElements

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

参考

oodesign文章

来源制作文章

更多细节

装饰器

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

相关文章:

IO的装饰器模式

何时使用装饰图案?


2

我真的很喜欢http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html中的描述和示例。

假设您拥有固定的主类层次结构;也许是来自其他供应商的,您无法更改该层次结构。但是,您的意图是要向该层次结构添加新的多态方法,这意味着通常必须向基类接口添加一些内容。因此,难题是您需要向基类中添加方法,但是您不能碰到基类。您如何解决这个问题?

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

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


从技术上讲,Visitor模式实际上只是他们示例中的基本双重调度。我认为,仅从这一点来看并不是特别有用。
乔治·莫尔

1

虽然我了解了方式和时间,但我从未理解为什么。如果它可以帮助具有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;
  }
}

2
在访客模式中使用动态调度使我完全困惑。模式的建议用法描述了可以在编译时完成的分支。使用功能模板似乎可以更好地解决这些问题。
Praxeolitic

0

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

0

您的问题是何时知道:

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

您可以从此处的许多示例中看​​到如何实现它,只是向您展示了一个用例。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.