我一直认为一个对象需要数据和消息才能对其进行操作。你什么时候想要一个对象外在的方法?您遵循什么经验法则来接待访客?这是假设您完全控制了对象图。
9 回答
当对一个相当复杂的数据结构的所有元素应用一个操作时,访问者模式特别有用,对于这些元素的遍历是非平凡的(例如并行遍历元素,或遍历高度互连的数据结构)或实现双分派。如果要按顺序处理元素并且不需要双重分派,那么实现自定义 Iterable 和 Iterator 通常是更好的选择,特别是因为它更适合其他 API。
我一直认为一个对象需要数据和消息才能对其进行操作。你什么时候想要一个对象外在的方法?您遵循什么经验法则来接待访客?这是假设您完全控制了对象图。
将特定对象的所有行为都定义在一个类中有时并不方便。例如在Java中,如果您的模块需要toXml
在最初在另一个模块中定义的一堆类中实现一个方法,这很复杂,因为您不能toXml
在原始类文件之外编写其他地方,这意味着您无法在不更改的情况下扩展系统现有源(在 Smalltalk 或其他语言中,您可以将扩展名中的方法分组,这些方法与特定文件无关)。
更一般地说,静态类型语言在(1)向现有数据类型添加新函数和(2)添加支持相同函数的新数据类型实现之间存在张力——这被称为表达式问题(维基百科页面)。
面向对象的语言在第 2 点上表现出色。如果您有接口,则可以安全轻松地添加新实现。函数式语言在第 1 点上表现出色。它们依赖于模式匹配/临时多态性/重载,因此您可以轻松地将新函数添加到现有类型。
访问者模式是在面向对象设计中支持第 1 点的一种方式:您可以轻松地以类型安全的方式使用新行为扩展if-else-instanceof
系统(如果您使用以下方式进行手动模式匹配则不会出现这种情况)如果未涵盖案例,该语言将永远不会警告您)。
当有一组固定的已知类型时,通常会使用访问者,我认为这就是“完全控制对象图”的意思。示例包括解析器中的令牌、具有各种类型节点的树以及类似情况。
所以总结一下,我想说你的分析是对的:)
PS:访问者模式与复合模式配合得很好,但它们也可以单独使用
有时这只是组织问题。如果您有具有 m 种操作(即:方法)的 n 种对象(即:类),您是否希望将 n * m 个类/方法对按类或按方法分组?大多数 OO 语言强烈倾向于按类对事物进行分组,但在某些情况下,按操作组织更有意义。例如,在对象图的多阶段处理中,例如在编译器中,将每个阶段(即:操作)视为一个单元而不是考虑可能发生在特定排序中的所有操作通常更有用的节点。
访问者模式的一个常见用例不仅仅是严格的组织,它是打破不需要的依赖关系。例如,通常不希望您的“数据”对象依赖于您的表示层,特别是如果您想象您可能有多个表示层。通过使用访问者模式,表示层的细节存在于访问者对象中,而不是数据对象的方法中。数据对象本身只知道抽象的访问者界面。
当我发现我想将一个有状态的方法放在 Entity/DataObject/BusinessObject 上时,我经常使用它,但我真的不想将这种状态引入我的对象。有状态的访问者可以完成这项工作,或者从我的无状态数据对象生成有状态执行器对象的集合。当工作的处理将被外包给执行线程时特别有用,许多有状态的访问者/工作人员可以引用同一组无状态对象。
对我来说,使用访问者模式的唯一一个原因是当我需要在树/树这样的类图数据结构上执行双重调度时。
当您需要根据对象类型(在类层次结构中)改变行为时,访问者模式最有用,并且可以根据对象提供的公共接口定义该行为。该行为不是该对象固有的,也不需要对象封装。
我发现访问者通常自然而然地出现在对象的图形/树中,其中每个节点都是类层次结构的一部分。为了允许客户端以统一的方式遍历图/树并处理任何每种类型的节点,访问者模式确实是最简单的替代方案。
例如,考虑一个 XML DOM - 一个 Node 是基类,由 Element、Attribute 和其他类型的 Node 定义类层次结构。
想象一下,要求是将 DOM 输出为 JSON。这种行为不是 Node 固有的——如果是这样,我们将不得不向 Node 添加方法来处理客户端可能需要的所有格式(toJSON()
、、toASN1()
等toFastInfoSet()
)。我们甚至可以争辩说它toXML()
不属于那里,尽管这可能为方便起见,因为它将被大多数客户端使用,并且在概念上“更接近” DOM,因此为了方便,可以将 toXML 设置为 Node 固有的 - 尽管它不是必须的,并且可以像处理所有其他格式。
由于 Node 及其子类使它们的状态作为方法完全可用,我们拥有能够将 DOM 转换为某种输出格式所需的所有外部信息。accept()
我们可以使用 Visitor 接口,在 Node 上使用抽象方法,并在每个子类中实现,而不是将输出方法放在 Node 对象上。
每个访问者方法的实现处理每个节点类型的格式。它可以做到这一点,因为所需的所有状态都可以从每个节点类型的方法中获得。
通过使用访问者,我们打开了实现所需输出格式的大门,而无需为每个 Node 类增加该功能的负担。
当您遇到以下问题时:
需要对异构聚合结构中的节点对象执行许多不同且不相关的操作。您希望避免使用这些操作“污染”节点类。而且,您不希望在执行所需操作之前查询每个节点的类型并将指针转换为正确的类型。
然后,您可以使用具有以下意图之一的访问者模式:
- 表示要对对象结构的元素执行的操作。
- 定义一个新的操作而不改变它所操作的元素的类。
- 恢复丢失类型信息的经典技术。
- 根据两个对象的类型做正确的事。
- 双重派送
当您完全了解实现接口的类时,我总是建议使用访问者。通过这种方式,您将不会进行任何不那么漂亮的instanceof
调用,并且代码变得更具可读性。此外,一旦实施了访问者,就可以在现在和将来的许多地方重用。
访问者模式是双重调度问题的一种非常自然的解决方案。双分派问题是动态分派问题的一个子集,它源于方法重载是在编译时静态确定的,这与在运行时确定的虚拟(覆盖)方法不同。
考虑这种情况:
public class CarOperations {
void doCollision(Car car){}
void doCollision(Bmw car){}
}
public class Car {
public void doVroom(){}
}
public class Bmw extends Car {
public void doVroom(){}
}
public static void Main() {
Car bmw = new Bmw();
bmw.doVroom(); //calls Bmw.doVroom() - single dispatch, works out that car is actually Bmw at runtime.
CarOperations carops = new CarOperations();
carops.doCollision(bmw); //calls CarOperations.doCollision(Car car) because compiler chose doCollision overload based on the declared type of bmw variable
}
下面的这段代码是从我之前的回答中采用的,并翻译成 Java。问题与上面的示例有些不同,但演示了访问者模式的本质。
//This is the car operations interface. It knows about all the different kinds of cars it supports
//and is statically typed to accept only certain ICar subclasses as parameters
public interface CarVisitor {
void StickAccelerator(Toyota car);
void ChargeCreditCardEveryTimeCigaretteLighterIsUsed(Bmw car);
}
//Car interface, a car specific operation is invoked by calling PerformOperation
public interface Car {
public string getMake();
public void setMake(string make);
public void performOperation(CarVisitor visitor);
}
public class Toyota implements Car {
private string make;
public string getMake() {return this.make;}
public void setMake(string make) {this.make = make;}
public void performOperation(CarVisitor visitor) {
visitor.StickAccelerator(this);
}
}
public class Bmw implements Car{
private string make;
public string getMake() {return this.make;}
public void setMake(string make) {this.make = make;}
public void performOperation(ICarVisitor visitor) {
visitor.ChargeCreditCardEveryTimeCigaretteLighterIsUsed(this);
}
}
public class Program {
public static void Main() {
Car car = carDealer.getCarByPlateNumber("4SHIZL");
CarVisitor visitor = new SomeCarVisitor();
car.performOperation(visitor);
}
}