访客模式的实际优势是什么?有哪些替代方案?

时间:2013-11-26 13:59:45

标签: language-agnostic visitor-pattern

我读了很多关于访客模式及其所谓的优点。然而,对我而言,在实践中应用它们似乎并没有太大的优势:

  • “方便”和“优雅”似乎意味着大量的样板代码
  • 因此,代码很难遵循。 “接受”/“访问”也不是很具描述性
  • 如果您的编程语言没有方法重载(即Vala),那么甚至更糟糕的样板代码
  • 一般情况下,您无法在不修改所有类的情况下向现有类型层次结构添加新操作,因为只要您需要不同的操作,就需要新的“接受”/“访问”方法到处参数和/或返回值(对整个地方的类的更改是这个设计模式应该避免的一件事!?)
  • 向类型层次结构添加新类型需要更改所有访问者。此外,您的访问者不能简单地忽略类型 - 您需要创建一个空的访问方法(样板表再次)

当你想要做的事实上是:

时,这似乎只是一项非常多的工作
// Pseudocode
int SomeOperation(ISomeAbstractThing obj) {
    switch (type of obj) {
        case Foo: // do Foo-specific stuff here
        case Bar: // do Bar-specific stuff here
        case Baz: // do Baz-specific stuff here
        default: return 0; // do some sensible default if type unknown or if we don't care
    }
}

我看到的唯一真正的优势(我在任何地方都没有提到过):访问者模式可能是在cpu时间方面实现上述代码片段的最快方法(如果你没有语言)以上述伪代码的方式进行双重调度或有效类型比较。

问题:

  • 那么,我错过了访客模式的哪些优点?
  • 可以使用哪些替代概念/数据结构来快速运行上述虚构代码示例?

4 个答案:

答案 0 :(得分:5)

在我个人看来,访问者模式只有在你想要实现的界面相当静态并且不会发生很大变化的情况下才有用,而你想让任何人都有机会实现自己的功能。

请注意,通过创建新接口而不是修改旧接口,每次添加新方法时都可以避免更改所有内容 - 那么当访问者未实现所有接口时,您只需要处理一些逻辑

基本上,好处是它允许您选择在运行时调用的正确方法,而不是在编译时调用 - 并且可用的方法实际上是可扩展的。

有关详细信息,请查看此文章 - http://rgomes-info.blogspot.co.uk/2013/01/a-better-implementation-of-visitor.html

答案 1 :(得分:5)

根据经验,我会说"在类型层次结构中添加新类型需要更改所有访问者"是一个优势。因为它肯定会迫使你考虑在你做过某些类型特定事情的所有地方添加的新类型。它可以防止你忘记一个......

答案 2 :(得分:4)

据我所知,到目前为止,访客设计模式有两个用途/好处:

  1. 双重发送
  2. 将数据结构与其上的操作分开
  3. 双重发送

    假设你有一个Vehicle类和一个VehicleWasher类。 VehicleWasher采用洗涤(车辆)方法:

    VehicleWasher
        Wash(Vehicle)
    
    Vehicle
    

    此外,我们还有像汽车这样的特定车辆,并且在未来我们还会有其他特定车辆。为此我们有一个Car类,但也有一个特定的CarWasher类,它具有特定于洗车的操作(伪代码):

    CarWasher : VehicleWasher
        Wash(Car)
    
    Car : Vehicle
    

    然后考虑以下客户端代码来清洗特定的车辆(请注意x和垫圈是使用其基本类型声明的,因为实例可能是根据用户输入或外部配置值动态创建的;在此示例中,它们只是使用虽然是一个新的运营商:

    Vehicle x = new Car();
    VehicleWasher washer = new CarWasher();
    
    washer.Wash(x);
    

    许多语言使用单个调度来调用适当的函数。单个调度意味着在运行时期间,在确定调用哪个方法时,只考虑单个值。因此,我们只考虑实际类型的洗衣机。 x的实际类型不被考虑在内。因此,最后一行代码将调用CarWasher.Wash(Vehicle)和 NOT CarWasher.Wash(Car)。

    如果您使用的语言不支持多次发送并且您确实需要它(我可以说我从未遇到过这种情况),那么您可以使用访问者设计模式来启用此功能。为此,需要做两件事。首先向Vehicle类(被访者)添加一个Accept方法,将VehicleWasher作为访问者接受,然后调用其操作Wash:

    Accept(VehicleWasher washer)
        washer.Wash(this);
    

    第二件事是修改调用代码并更换washer.Wash(x);符合以下内容:

    x.Accept(washer);
    

    现在,对A​​ccept方法的调用考虑了x的实际类型(并且只考虑x的那个,因为我们假设使用单个调度语言)。在Accept方法的实现中,在洗衣机对象(访问者)上调用Wash方法。为此,考虑了洗衣机的实际类型,这将调用CarWasher.Wash(Car)。通过组合两个单一调度,实现双重调度。

    现在,请详细说明您对“接受”和“访问”等术语的评论,并且访客非常不具体。这绝对是真的。但这是有原因的。

    考虑此示例中的要求,以实现能够修复车辆的新类:VehicleRepairer。如果此类只能从VehicleWasher继承并在Wash方法中包含其修复逻辑,则此类只能在此示例中用作访问者。但那当然没有任何意义,会让人感到困惑。所以我完全同意设计模式往往具有非常模糊和非特定的命名,但它确实使它们适用于许多情况。您的命名越具体,它就越具有限制性。

    您的switch语句只考虑一种实际上是单一调度手动方式的类型。以上述方式应用访问者设计模式将提供双重调度。 这样,在向层次结构添加其他类型时,不一定需要其他访问方法。当然,它确实增加了一些复杂性,因为它使代码的可读性降低。但当然所有模式都需要付出代价。

    当然,不能总是使用这种模式。如果您期望使用多个参数进行大量复杂操作,那么这将不是一个好的选择。

    另一种方法是使用支持多个调度的语言。例如.NET直到引入动态关键字的4.0版才支持它。然后在C#中,您可以执行以下操作:

    washer.Wash((dynamic)x);
    

    因为x然后被转换为动态类型,它的实际类型将被考虑用于调度,因此x和washer将用于选择正确的方法,以便调用CarWasher.Wash(Car)(制作代码)正常工作,保持直观)。

    单独的数据结构和操作

    另一个好处和要求是它可以将数据结构与操作分开。这可能是一个优势,因为它允许添加具有自己的操作的新访问者,同时还允许添加“继承”的数据结构。这些行动。然而,只有在可以完成/有意义的情况下才可以应用它。执行操作的类(访问者)不知道数据结构的结构,也不知道哪些使代码更易于维护和重用。在应用此原因时,访问者可以对数据结构中的不同元素进行操作。

    假设您有不同的数据结构,它们都包含类Item的元素。结构可以是列表,堆栈,树,队列等。

    然后,您可以实现访问者,在这种情况下将具有以下方法:

    Visit(Item)
    

    数据结构需要接受访问者,然后为每个项目调用Visit方法。

    通过这种方式,您可以实现各种访问者,只要它们包含Item类型的元素,您仍然可以添加新的数据结构。

    对于具有其他元素(例如节点)的更具体的数据结构,您可能会考虑从传统访客继承的特定访问者(NodeVisitor)并让您的新数据结构接受该访问者(Accept(NodeVisitor))。新访问者可以用于新的数据结构,但也可以用于旧数据结构,因为您不需要修改现有的界面' (在这种情况下超级)。

答案 3 :(得分:3)

这是一个老问题,但我想回答。

访问者模式非常有用,主要是在您构建了一个对象树的复合模式时,这种树排列是不可预测的。

类型检查可能是访问者可以做的一件事,但是说你想根据一个树来构建一个表达式,这个表可以根据用户输入或类似的东西改变它的形式,访问者将是一种有效的方法。您可以验证树,或根据树上找到的项目构建复杂对象。

访问者还可以携带一个对象,该对象可以在该树上找到的每个节点上执行某些操作。这个访问者可能是一个复合本身,它在每个节点上链接了许多操作,或者它可以携带一个中介对象来调解操作或在每个节点上调度事件。

你的想象力是这一切的极限。您可以过滤集合,从完整的树构建抽象语法树,解析字符串,验证事物集合等。