据我所知,访问者设计模式有两个用途/好处:
- 双调度
- 将数据结构与其上的操作分开
双调度
假设您有一个 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洗车机.洗(汽车)。
如果您使用的语言不支持多重分派,但您确实需要它(我可以诚实地说我从未遇到过这种情况),那么您可以使用访问者设计模式来启用此功能。为此,需要做两件事。首先,向 Vehicle 类(访问者)添加一个 Accept 方法,该方法接受 VehicleWasher 作为访问者,然后调用其操作 Wash:
Accept(VehicleWasher washer)
washer.Wash(this);
第二件事是修改调用代码,替换washer.Wash(x);符合以下条件:
x.Accept(washer);
现在,对于 Accept 方法的调用,将考虑 x 的实际类型(并且仅考虑 x 的类型,因为我们假设使用单一调度语言)。在 Accept 方法的实现中,对 Washer 对象(访问者)调用 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)
数据结构需要接受访问者,然后为每个项目调用 Visit 方法。
通过这种方式,您可以实现各种访问者,并且仍然可以添加新的数据结构,只要它们包含 Item 类型的元素即可。
对于具有附加元素(例如节点)的更具体的数据结构,您可以考虑从传统访问者继承的特定访问者(NodeVisitor),并让您的新数据结构接受该访问者(Accept(NodeVisitor))。新的访问者可以用于新的数据结构,但由于继承也可以用于旧的数据结构,因此您不需要修改现有的“接口”(在本例中为超类)。