意图
表示一个作用在对象结构中元素的操作。
visitor模式使得可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
适用性
- 一个对象结构(一个代理类)包含很多提供不同接口的类对象,而你想对这些对象实施一些依赖于其具体类的操作。
- 需要对对象结构中的一个对象进行多种不同且互不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitor可以将多个类的相关的操作集中起来(类似代理)定义在一个类中。当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含他们需要用到的操作。
- 组合成对象结构的类很少改变,但经常需要在此结构上定义新的操作。
结构
参与者
- Visitor(访问者,如NodeVisitor)
- 为该对象结构中每一个ConcreteElement声明一个Visit操作。该操作的名字和特征标识了发送visit请求给该访问者的那个类。这使得访问者可以确定正在被访问元素的具体的类。这样访问者就可以通过该元素的特定接口直接访问它。
- ConcreteVisitor(具体访问者,如TypeCheckingVisitor)
- 实现每个由Visitor声明的操作。每个操作实现本算法(该ConcreteVisitor要实现的算法)的一部分,而该算法片段乃是对应与结构中的具体类(ConcreteElement)。ConcreteVisitor为该算法提供了上下文并存储它的局部状态。这一状态常常在遍历该结构的过程中积累结果。
- Element(元素,如Node)
- 定义一个Accept操作方法的接口,它以一个访问者为参数。
- ConcreteElement(具体元素,如AssignmentNode,VariableRefNode)
- 实现Accept操作方法,该操作以一个访问者为参数。
- ObjectStructure(对象结构,如Program)
- 能枚举它的Elements。
- 可以提供一个高层的接口亦允许该访问者访问它的Elements。
- 可以是一个(Composite)或是一个集合(collection),如一个列表(list)或一个集(set)。
协作
- 一个使用visitor模式的客户必须至少创建一个ConcreteVisitor对象,然后遍历该ObjectStructre,并用该访问者访问每一个Element。
- 当一个Element被访问时,它调用对应于它自身的Visitor操作。如果必要,该Element将自身作为这个操作的一个参数以便该访问者访问它的状态。
效果
- 访问者模式使得易于增加新的操作
- 访问者使得增加依赖于复杂对象的组件的操作变得容易了。仅需增加一个新的Visitor即可在一个ObjectStructure上定义一个新的操作。相反,如果每个功能都分散在多个类之上的话,定义新的操作时必须修改每一类。
- 访问者集中相关的操作而分离无关的操作
- 相关的行为不是分布在定义该ObjectStructure的各个类上,而是集中在一个访问者中。无关行为却被分别放在他们各自的Visitor子类中。这就即简化了这些元素的类,也简化了在这些Visitor中定义的算法。所有与它的算法相关的数据结构都可以被隐藏在访问者中。
- 增加新的ConcreteElement类都很困难
- Visitor模式使得难以增加新的Element的子类。每添加一个新的ConcreteElement都要在Visitor中添加一个新的抽象操作,并在每一个ConcreteVisitor类中实现相应的操作。有时可以在Visitor中提供一个缺省的实现,这一实现可以被大多数的ConcreteVisitor继承,但这与其说是一个规律还不如说是一种例外。
- 因此在应用Visitor模式时,考虑关键的问题时系统的哪个部分会经常变化,是作用于ObjectStructure上的算法呢还是构成该结构的各个ConcreteElement。如果常常有新的ConcreteElement类加入进来的话,Vistor类层次将变得难以维护。如果Element类层次是稳定的,而你不断地增加操作或修改算法,访问者模式可以帮助你管理这些改动。
- 通过类层次进行访问
- 一个迭代器可以通过调用对象的方法来遍历对象。但是迭代器不能对具有不同元素类型的对象结构进行操作。
- 累积状态
- 当Visitor访问ObjectStructure中的每一个Element时,它可能会累积状态。如果没有访问者,这一状态将作为额外的参数传递给进行遍历的操作,或者定义为全局变量。
- 破坏封装
- Visitor以ConcreteElement接口的功能足够强为前提,使得让Visitor足以进行它们的工作。结果是,该模式常常迫视你提供访问元素内部状态的公共操作,这可能会破坏它的封装性。
实现
双分派(Double-dispatching)
双分派意味着得到值性的操作决定于请求的种类和两个接收者的类型。Accept是一个double-dispatch操作。它的含义决定两个类型:Visitor的类型和Element的类型。双分派使得访问者可以对每一个类的元素请求不同的操作。
Visitor模式的关键在于:得到执行的操作不仅决定于Visitor的类型还决定于它访问的Element类型。可以不将操作静态地绑定在Element接口中,而将其安放在一个Visitor中,并使用Accept在运行时绑定。拓展Element接口就等于定义一个新的Visitor子类而不是多个新的Element子类。
谁负责遍历对象结构?
一个访问者必须访问这个对象结构的每一个元素。
遍历的责任可以放到下面三个地方中的任意一个:
- 对象结构中
- 通常由对象结构迭代。一个collection(集合)只需对它的元素进行迭代(对每一个元素调用Accept操作)。而一个Composite是通过调用其子元素的Accept方法,然后这些子元素由递归地调用他们的子元素的Accept方法。
- 在对象结构内部使用迭代器,如此,就不是双分派——如此是通过调用Visitor上的操作并传以Element的参数,而不是调用Element上的操作并传以Visitor为参数。
- 访问者中
- 尽管这样将导致对每一个聚合ConcreteElement
- 一个独立的迭代器对象中