【发布时间】:2010-05-12 00:57:03
【问题描述】:
我一直认为一个对象需要数据和消息才能对其采取行动。你什么时候想要一个对象外在的方法?您遵循什么经验法则来接待访客?这是假设您完全控制了对象图。
【问题讨论】:
标签: java design-patterns traversal visitor-pattern
我一直认为一个对象需要数据和消息才能对其采取行动。你什么时候想要一个对象外在的方法?您遵循什么经验法则来接待访客?这是假设您完全控制了对象图。
【问题讨论】:
标签: java design-patterns traversal visitor-pattern
访问者模式在对相当复杂的数据结构的所有元素应用操作时特别有用,对于这些元素的遍历非常重要(例如并行遍历元素,或遍历高度互连的数据结构)或实现双-派遣。如果要按顺序处理元素并且不需要双重分派,那么实现自定义 Iterable 和 Iterator 通常是更好的选择,尤其是因为它更适合其他 API。
【讨论】:
我一直认为一个对象需要 数据和对其采取行动的消息。 你什么时候想要一个方法 对象外在的?什么规则 大拇指你跟着有客人吗? 这是假设你有完整的 对象图的控制。
有时将特定对象的所有行为都定义在一个类中并不方便。例如在 Java 中,如果您的模块需要在最初在另一个模块中定义的一堆类中实现方法 toXml,这很复杂,因为您不能在原始类文件之外的其他地方编写 toXml,这意味着您可以不要在不更改现有源的情况下扩展系统(在 Smalltalk 或其他语言中,您可以在扩展中对不绑定到特定文件的方法进行分组)。
更一般地说,静态类型语言在以下两种能力之间存在张力:(1) 向现有数据类型添加新函数,以及 (2) 添加支持相同函数的新数据类型实现——这被称为expression problem ( wikipedia page)。
面向对象的语言在第 2 点上表现出色。如果您有接口,则可以安全轻松地添加新实现。函数式语言在第 1 点上表现出色。它们依赖于模式匹配/临时多态性/重载,因此您可以轻松地将新函数添加到现有类型。
访问者模式是在面向对象设计中支持第 1 点的一种方式:您可以轻松地扩展系统以一种类型安全的方式使用新行为(事实并非如此)如果您使用if-else-instanceof 进行手动模式匹配,因为如果未涵盖案例,该语言将永远不会警告您)。
当存在一组固定的已知类型时,通常会使用访问者,我认为这就是您所说的“完全控制对象图”。示例包括解析器中的令牌、具有各种类型节点的树以及类似情况。
所以总结一下,我想说你的分析是对的 :)
PS:访问者模式与复合模式配合得很好,但它们也可以单独使用
【讨论】:
有时这只是组织问题。如果您有具有 m 种操作(即:方法)的 n 种对象(即:类),您是否希望将 n * m 个类/方法对按类或按方法分组?大多数 OO 语言强烈倾向于按类对事物进行分组,但在某些情况下,按操作组织更有意义。例如,在对象图的多阶段处理中,例如在编译器中,将每个阶段(即:操作)视为一个单元而不是考虑可能发生在特定排序上的所有操作通常更有用节点。
访问者模式的一个常见用例不仅仅是严格的组织,它是打破不需要的依赖关系。例如,通常不希望您的“数据”对象依赖于您的表示层,特别是如果您想象您可能有多个表示层。通过使用访问者模式,表示层的细节存在于访问者对象中,而不是数据对象的方法中。数据对象本身只知道抽象的访问者界面。
【讨论】:
当我发现我想将一个有状态的方法放到 Entity/DataObject/BusinessObject 上时,我经常使用它,但我真的不想将这种状态引入我的对象。有状态的访问者可以完成这项工作,或者从我的无状态数据对象生成有状态执行器对象的集合。当工作的处理将被外包给执行线程时特别有用,许多有状态的访问者/工作人员可以引用同一组无状态对象。
【讨论】:
对我来说,使用访问者模式的唯一原因是当我需要对树/trie 等类似图形的数据结构执行双重调度时。
【讨论】:
当您遇到以下问题时:
需要对异构聚合结构中的节点对象执行许多不同且不相关的操作。您希望避免使用这些操作“污染”节点类。而且,您不希望在执行所需操作之前查询每个节点的类型并将指针转换为正确的类型。
然后您可以使用具有以下意图之一的访问者模式:
- 表示要对对象结构的元素执行的操作。
- 定义一个新的操作而不改变它所操作的元素的类。
- 恢复丢失类型信息的经典技术。
- 根据两个对象的类型做正确的事。
- 双发
【讨论】:
当您需要根据对象类型(在类层次结构中)改变行为时,访问者模式最有用,并且可以根据对象提供的公共接口来定义该行为。该行为不是该对象固有的,也不需要对象封装。
我发现访问者通常自然而然地出现在对象的图形/树中,其中每个节点都是类层次结构的一部分。为了让客户端能够以统一的方式遍历图/树并处理任何类型的节点,访问者模式确实是最简单的替代方案。
例如,考虑一个 XML DOM - 一个 Node 是基类,由 Element、Attribute 和其他类型的 Node 定义类层次结构。
假设要求是将 DOM 输出为 JSON。这种行为不是 Node 固有的——如果是这样,我们将不得不向 Node 添加方法来处理客户端可能需要的所有格式(toJSON()、toASN1()、toFastInfoSet() 等)我们甚至可以争辩说toXML() 不属于那里,尽管这可能是为了方便而提供的,因为它将被大多数客户端使用,并且在概念上“更接近”DOM,因此为了方便,可以将 toXML 设置为 Node 的内在 - 尽管它不必如此,并且可以像所有其他格式一样处理。
由于 Node 及其子类使它们的状态作为方法完全可用,我们拥有能够将 DOM 转换为某种输出格式所需的所有外部信息。我们可以使用访问者接口,而不是将输出方法放在 Node 对象上,在 Node 上使用抽象的accept() 方法,并在每个子类中实现。
每个访问者方法的实现处理每个节点类型的格式。它可以做到这一点,因为所需的所有状态都可以从每个节点类型的方法中获得。
通过使用访问者,我们打开了实现所需输出格式的大门,而无需为每个 Node 类增加该功能的负担。
【讨论】:
当您完全了解实现接口的类时,我始终建议您使用访问者。这样你就不会做任何不那么漂亮的instanceof-calls,并且代码变得更具可读性。此外,一旦实现了访问者,就可以在现在和未来的许多地方重用。
【讨论】:
访问者模式是双重调度问题的一种非常自然的解决方案。双分派问题是动态分派问题的一个子集,它源于方法重载是在编译时静态确定的,这与在运行时确定的虚拟(覆盖)方法不同。
考虑这种情况:
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
}
下面的这段代码取自my previous answer并翻译成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);
}
}
【讨论】: