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

Ask*_*aga 11 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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

问题:

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

Ron*_*ers 6

就我目前所见,访问者设计模式有两个用途/好处:

  1. 双重派遣
  2. 将数据结构与其上的操作分开

双重派遣

假设您有一个 Vehicle 类和一个 VehicleWasher 类。VehicleWasher 有一个 Wash(Vehicle) 方法:

VehicleWasher
    Wash(Vehicle)

Vehicle
Run Code Online (Sandbox Code Playgroud)

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

CarWasher : VehicleWasher
    Wash(Car)

Car : Vehicle
Run Code Online (Sandbox Code Playgroud)

然后考虑以下客户端代码来清洗特定的车辆(注意 x 和washer 是使用它们的基类型声明的,因为实例可能是基于用户输入或外部配置值动态创建的;在本例中,它们只是使用新的操作符创建的尽管):

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

washer.Wash(x);
Run Code Online (Sandbox Code Playgroud)

许多语言使用单分派来调用适当的函数。单分派意味着在运行时确定调用哪个方法时只考虑单个值。因此,我们只考虑实际的垫圈类型。不考虑 x 的实际类型。因此,最后一行代码将调用 CarWasher.Wash(Vehicle) 而不是CarWasher.Wash(Car)。

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

Accept(VehicleWasher washer)
    washer.Wash(this);
Run Code Online (Sandbox Code Playgroud)

二是修改调用代码,替换washer.Wash(x); 符合以下条件:

x.Accept(washer);
Run Code Online (Sandbox Code Playgroud)

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

现在详细说明您对“接受”和“访问”和“访问者”等术语非常不具体的评论。这是绝对正确的。但这是有原因的。

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

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

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

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

washer.Wash((dynamic)x);
Run Code Online (Sandbox Code Playgroud)

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

分离数据结构和操作

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

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

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

Visit(Item)
Run Code Online (Sandbox Code Playgroud)

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

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

对于具有附加元素(例如节点)的更具体的数据结构,您可能会考虑从传统访问者继承的特定访问者(NodeVisitor)并让您的新数据结构接受该访问者(Accept(NodeVisitor))。由于继承,新访问者可用于新数据结构,也可用于旧数据结构,因此您无需修改​​现有的“接口”(在本例中为超类)。


Lua*_*aan 5

我个人认为,访问者模式仅在您要实现的接口相当静态且变化不大时才有用,而您想给任何人实现自己的功能的机会。

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

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

有关更多信息,请查看本文-http://rgomes-info.blogspot.co.uk/2013/01/a-better-implementation-of-visitor.html


jpo*_*o38 5

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