您何时决定将访客用于您的对象?

时间:2010-05-12 00:57:04

标签: java design-patterns traversal visitor

我一直认为对象需要数据和消息来对其进行操作。你什么时候想要一个对象的外在方法?有一个经验法则,你有一个访客?这假设您可以完全控制对象图。

9 个答案:

答案 0 :(得分:7)

当将操作应用于相当复杂的数据结构的所有元素时,访问者模式特别有用,其中遍历非平凡(例如,并行遍历元素,或遍历高度互连的数据结构)或实现双重-调度。如果要按顺序处理元素并且不需要双重调度,那么实现自定义Iterable和Iterator通常是更好的选择,特别是因为它更适合其他API。

答案 1 :(得分:6)

  

我一直认为对象需要   数据和要对其采取行动的消息。   你什么时候想要一个方法呢?   对象的外在性?什么规则   你跟着有一个访客吗?   这假设你已经满了   控制对象图。

在一个类中定义特定对象的所有行为有时不方便。例如,在Java中,如果您的模块需要在最初在另一个模块中定义的一堆类中实现方法toXml,那么它很复杂,因为您不能在原始类文件之外的其他地方编写toXml,这意味着您无法在不更改现有源的情况下扩展系统(在Smalltalk或其他语言中,您可以将扩展中的方法分组,这些方法与特定文件无关)。

更一般地说,静态类型语言在(1)向现有数据类型添加新函数,以及(2)添加支持相同函数的新数据类型实现(称为{{3})之间存在紧张关系。 }(expression problem)。

面向对象的语言在第2点表现优异。如果您有一个界面,则可以安全轻松地添加新的实现。功能语言在第1点表现优异。它们依赖于模式匹配/ ad-hoc多态/重载,因此您可以轻松地向现有类型添加新功能。

访问者模式是一种在面向对象的设计中支持第1点的方法:您可以通过类型安全的方式轻松扩展系统的新行为(事实并非如此)如果您使用if-else-instanceof进行手动模式匹配,因为如果没有覆盖案例,语言将永远不会警告您。

当存在一组固定的已知类型时,通常会使用访问者,我认为这就是“完全控制对象图”的含义。示例包括解析器中的标记,具有各种类型节点的树以及类似情况。

总而言之,我会说你的分析是对的:)

PS:访问者模式适用于复合模式,但它们也可以单独使用

答案 2 :(得分:4)

有时它只是一个组织问题。如果你有n种对象(即:类)和m种操作(即:方法),你想让n * m类/方法对按类或方法分组吗?大多数OO语言强烈倾向于按类分组,但有些情况下按操作组织更有意义。例如,在对象图的多阶段处理中,如在编译器中,通常更有用的是将每个阶段(即:操作)视为一个单元,而不是考虑可能发生在特定排序的所有操作节点。

访问者模式的一个常见用例,它不仅仅是严格组织,而是打破不需要的依赖关系。例如,通常不希望您的“数据”对象依赖于您的表示层,特别是如果您想象您可能有多个表示层。通过使用访问者模式,表示层的细节存在于访问者对象中,而不是数据对象的方法中。数据对象本身只知道抽象访问者界面。

答案 3 :(得分:3)

当我发现我想在Entity / DataObject / BusinessObject上放置一个有状态的方法但是我真的不想将这种状态引入我的对象时,我经常使用它。有状态访问者可以完成这项工作,或者从我的非有状态数据对象生成有状态执行程序对象的集合。当工作的处理将被执行到执行程序线程时特别有用,许多有状态访问者/工作者可以引用同一组非有状态对象。

答案 4 :(得分:3)

对我来说,使用访问者模式的唯一原因是我需要在像树/特里这样的图形数据结构上执行双重调度。

答案 5 :(得分:3)

遇到以下问题时:

  
    

需要在异构聚合结构中的节点对象上执行许多不同且不相关的操作。您希望避免使用这些操作“污染”节点类。并且,您不希望在执行所需操作之前查询每个节点的类型并将指针强制转换为正确的类型。

  

然后,您可以使用具有以下意图之一的访客模式:

  
      
  • 表示要对对象结构的元素执行的操作。
  •   
  • 定义新操作,而不更改其操作元素的类。
  •   
  • 恢复丢失类型信息的经典技巧。
  •   
  • 根据两个对象的类型做正确的事。
  •   
  • 双重发送
  •   

(来自http://sourcemaking.com/design_patterns/visitor

答案 6 :(得分:3)

当您需要根据对象类型(在类层次结构中)改变行为时,访问者模式最有用,并且可以根据对象提供的公共接口来定义该行为。该行为不是该对象固有的行为,并且不会受益于对象或需要对象进行封装。

我发现访问者经常会出现对象的图形/树,其中每个节点都是类层次结构的一部分。为了允许客户端以统一的方式遍历图形/树并处理任何类型的节点,访问者模式实际上是最简单的替代方案。

例如,考虑XML DOM - Node是基类,Element,Attribute和其他类型的Node定义类层次结构。

想象一下,要求是将DOM输出为JSON。行为不是Node固有的 - 如果是,我们必须向Node添加方法来处理客户端可能需要的所有格式(toJSON()toASN1()toFastInfoSet()等。我们甚至可以争辩toXML()不属于那里,虽然这可能是为了方便而提供的,因为它将被大多数客户使用,并且在概念上“更接近”DOM,因此可以制作toXML为了方便起见,Node是固有的 - 虽然它不一定是,并且可以像所有其他格式一样处理。

由于Node及其子类使其状态完全可用作方法,因此我们拥有外部所需的所有信息,以便能够将DOM转换为某种输出格式。我们可以使用Visitor接口,在Node上使用抽象accept()方法,并在每个子类中实现,而不是将输出方法放在Node对象上。

每个访问者方法的实现处理每种节点类型的格式。它可以这样做,因为所需的所有状态都可以从每个节点类型的方法中获得。

通过使用访问者,我们打开了实现任何所需输出格式的大门,而无需为每个具有该功能的Node类负担。

答案 7 :(得分:2)

当您完全了解实现接口的类时,我总是建议您使用访问者。通过这种方式,您不会进行任何不那么漂亮的instanceof - 调用,并且代码变得更具可读性。此外,一旦访问者实施,可以在许多地方,现在和将来重复使用。

答案 8 :(得分:1)

访客模式是双重调度问题的一种非常自然的解决方案。双调度问题是动态调度问题的一个子集,它源于在编译时静态确定方法重载的事实,这与在运行时确定的虚拟(覆盖)方法不同。

考虑这种情况:

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);
  }
}