【问题标题】:What are the actual advantages of the visitor pattern? What are the alternatives?访问者模式的实际优势是什么?有哪些替代方案?
【发布时间】:2013-12-11 17:12:10
【问题描述】:

我阅读了很多关于访问者模式及其假定优势的文章。然而,对我来说,在实践中应用它们似乎并没有那么大的优势:

  • “方便”和“优雅”似乎意味着大量的样板代码
  • 因此,代码很难遵循。 'accept'/'visit' 也不是很具有描述性
  • 如果您的编程语言没有方法重载(即 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 时间而言,访问者模式可能是实现上述代码 sn-p 的最快方法(如果你没有以上述伪代码的方式具有双重分派或高效类型比较的语言)。

问题:

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

【问题讨论】:

  • IMO 你错过了一个重要的点......你应该永远(我会写得更大)有类似switch (type of obj)的东西。不管是否访客。如果您的代码不是那样,那么您将不会编写任何样板代码。
  • 示例访问者何时可以帮助您?想象一下编写一个实用程序来搜索文件内的文本。搜索引擎将访问每个文件系统项(文件将被访问,目录将访问传播到每个子项)。文件系统项目可以是目录、文件、指向 FTP 站点的链接……搜索引擎永远不会知道它的工作原理。访问者不应该知道确切的类型(不是因为访问者模式,而是因为 OOP 原则......)
  • 访问者级别太低了。当您可以执行以下操作时,使用访问者毫无意义:cs.indiana.edu/~dyb/pubs/nano-jfp.pdf
  • @Adriano:您说访问者不应该知道确切的类型,但是当调用访问者的任何访问方法时,传递的参数就是确切的类型(IMO 这就是重点)。此外,如果您查看有关访问者模式的 Wikipedia 文章,您会发现 Scala 示例的工作方式与我的伪代码中所述完全一样。
  • @BillAskaga 这是一个示例和一个用例(此外,我会重载方法而不是那个,这也是关于 OOP 而不是访问者模式)。模式的重点始终是……您不必总是使用它们。当它们使您的代码变得更复杂时......不要使用它们。但是,再次,IMO 使该代码可读性降低的不是访问者,而是围绕它的基本逻辑。

标签: language-agnostic visitor-pattern


【解决方案1】:

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

  1. 双重调度
  2. 将数据结构与对它们的操作分开

双发

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

VehicleWasher
    Wash(Vehicle)

Vehicle

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

CarWasher : VehicleWasher
    Wash(Car)

Car : Vehicle

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

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

washer.Wash(x);

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

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

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

第二件事是修改调用代码,更换洗衣机。Wash(x);符合以下内容:

x.Accept(washer);

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

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

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

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

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

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

washer.Wash((dynamic)x);

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

独立的数据结构和操作

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

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

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

Visit(Item)

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

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

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

【讨论】:

    【解决方案2】:

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

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

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

    欲了解更多信息,请查看这篇文章 - http://rgomes-info.blogspot.co.uk/2013/01/a-better-implementation-of-visitor.html

    【讨论】:

      【解决方案3】:

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

      【讨论】:

      • 这并不总是必需的,其次这完全违背了基本的开放/封闭基础。
      【解决方案4】:

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

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

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

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

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

      【讨论】:

        猜你喜欢
        • 2010-11-02
        • 2010-10-28
        • 1970-01-01
        • 2011-07-15
        • 1970-01-01
        • 2015-05-19
        • 2021-05-28
        • 1970-01-01
        • 2011-03-09
        相关资源
        最近更新 更多