一、概述
前面已经讨论过的Adapter模式告诉我们如何应对接口不一致对我们的设计造成的影响,但是,这并不能在如下的Context下发挥多大的作用:
一个类系中的多个类要求支持相同的操作,但是这些类提供的接口并不一致。
看到这里,你可能会说,我干嘛要用什么Adapter?我才没那么笨呢,我直接修改整个类系的接口方法,添加新的统一的接口方法不就OK了?
确实如此,但是,如果你凑巧使用的是另一个不方便修改的模块的代码呢?
在这种情况下,试试Visitor(访问者)模式吧。
Visitor模式用于表示一个作用于某对象结构中的各元素的操作,使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
Visitor模式借助所谓的“Double-Dispatch(双分派)”来达到以上目的。在C++、Java这样的典型的强类型、单分派语言中,我们通常使用的都是单分派,单分派的意思就是执行的方法是由调用者而不是调用参数决定的,譬如
a.add(b)
那么,这时执行的方法就是由a来决定的。如果直接支持双分派的语言,那么执行这个方法就可以根据a和b两个的类型来决定。
前面说过,C++、Java等并不能直接支持双分派,因此,要在C++、Java中支持所谓的双分派,必须通过增加额外的附加层和方法来实现。
有两种主要的方式来实现Double-Dispatch,一种是type-switch(以下代码取自JDK):
protected void processEvent(AWTEvent e) {
if (e instanceof FocusEvent) {
processFocusEvent((FocusEvent)e);
} else if (e instanceof MouseEvent) {
switch(e.getID()) {
case MouseEvent.MOUSE_PRESSED:
case MouseEvent.MOUSE_RELEASED:
case MouseEvent.MOUSE_CLICKED:
case MouseEvent.MOUSE_ENTERED:
case MouseEvent.MOUSE_EXITED:
processMouseEvent((MouseEvent)e);
break;
case MouseEvent.MOUSE_MOVED:
case MouseEvent.MOUSE_DRAGGED:
processMouseMotionEvent((MouseEvent)e);
break;
case MouseEvent.MOUSE_WHEEL:
processMouseWheelEvent((MouseWheelEvent)e);
break;
}
} else if (e instanceof KeyEvent) {
processKeyEvent((KeyEvent)e);
} else if (e instanceof ComponentEvent) {
processComponentEvent((ComponentEvent)e);
} else if (e instanceof InputMethodEvent) {
processInputMethodEvent((InputMethodEvent)e);
} else if (e instanceof HierarchyEvent) {
switch (e.getID()) {
case HierarchyEvent.HIERARCHY_CHANGED:
processHierarchyEvent((HierarchyEvent)e);
break;
case HierarchyEvent.ANCESTOR_MOVED:
case HierarchyEvent.ANCESTOR_RESIZED:
processHierarchyBoundsEvent((HierarchyEvent)e);
break;
}
}
}
这种方式通过检查b的类型信息进行Re-Dispatch,虽然type-switch在设计上比较简单,但type-switch是OOD中应当尽量避免使用的技术,因为它可能给我们的代码引入一些难以察觉的Bug,以下面的代码为例(Java Code):
class A {
}
class B extends A {
}
public class Test {
static public void main(String[] args){
B b = new B();
if (b instanceof A) {
System.out.println("b is an instanceof A");
} else if (b instanceof B) {
System.out.println("b is an instanceof B");
}
}
}
程序运行的结果是:
b is an instanceof A
虽然从逻辑上讲,这个结论是正确的,但这显然不是我们期望的答案。要让上面的程序输出“b is an instanceof B”,需要调整上面的if判断的顺序,使子类判断出现在基类判断之前,由特殊到普通,否则,父类判断将屏蔽掉子类判断,对于简单的类型判断,使用type-switch是个不错的选择,但是当继承体系变得十分复杂时,判断顺序上的问题可能给你带来意想不到的麻烦(当然,还有别的办法,如通过getClass来检查类名信息,但这种方式比上面的方式也好不到哪里去)。
还有一种Double-Dispatch实现方式在使用上相对较为安全,但实现较为复杂,而且需要更多的设计技巧,Visitor模式采用的是这一种形式。以下面的函数调用为例:
a.add(Number b)
我们可以在add函数体内采用type-switch方法对b进行判断,完成add操作,也可以像下面这样:
对于整数类型,定义:
public class Integer {
Number add(Number b) {
return b.add(a);
}
...
}
则不管b是什么类型,只要它实现了add(Integer a)这个方法,就可以准确完成add操作。
对于浮点类型,定义:
public class Float {
Number add(Number b) {
return b.add(a);
}
...
}
则不管b是什么类型,只要它实现了add(Float a)这个方法,就可以准确完成add操作。
可以看到,这时候把到底执行哪一个方法转交给了b,而且,在b执行该方法时,已经有了a的类型信息,因此,无需再进行type-switch。
那么这种复杂的Double-Dispatch技术有什么好处呢?它的好处之一在于可以使我们在不改变a的同时,通过对b进行扩充,达到为a提供新的功能的目的。以上面的add为例,我们可以从Number派生出一种新的数值类型,在其中实现各种add操作,则可以在不改变已有数值类型的基础上与之协同工作。当然,由于在使用上存在一些限制,限制了Double-Dispatch的应用,后面将对此进一步进行说明。
二、结构
Visitor模式的结构如下图所示:
图1、Visitor模式的示意类图
其中包括以下组成部分:
Visitor(访问者):为该对象结构中ConcreteElement的每一个类声明一个Visit操作。该操作的名字和特征标识了发送Visit请求给该访问者的那个类。这使得访问者可以确定正被访问元素
的具体的类。这样访问者就可以通过该元素的特定接口直接访问它。
ConcreteVisitor(具体访问者):实现每个由Visitor声明的操作。每个操作实现本算法的一部分,而该算法片断乃是对应于结构中对象的类。ConcreteVisitor为该算法提供了上下文并存储它的局部状态,这一状态常常在遍历该结构的过程中累积结果。
Element(元素):定义一个Accept操作,它以一个访问者为参数。
ConcreteElement(具体元素):实现Accept操作,该操作以一个访问者为参数。
ObjectStructure(对象结构,如Program):能枚举它的元素;可以提供一个高层的接口以允许该访问者访问它的元素;可以是一个复合或是一个集合,如一个列表或一个无序集合。
三、应用
在下列情况下可考虑使用Visitor模式:
1、一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitor使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作。
3、定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
四、优缺点
Visitor模式通过扩充新的继承体系来为已有的继承体系提供新的针对特殊类型的功能(达到与添加虚成员函数相同的效果),适用于十分稳定,并执行繁重处理的继承体系。下面是访问者模式的一些优缺点:
1、Visitor模式使得易于增加新的操作访问者使得增加依赖于复杂对象结构的构件的操作变得容易了。仅需增加一个新的访问者即可在一个对象结构上定义一个新的操作。相反,如果每个功能都分散在多个类之上的话,定义新的操作时必须修改每一类。
2、访问者集中相关的操作而分离无关的操作相关的行为不是分布在定义该对象结构的各个类上,而是集中在一个访问者中。无关行为却被分别放在它们各自的访问者子类中。这就既简化了这些元素的类,也简化了在这些访问者中定义的算法。所有与它的算法相关的数据结构都可以被隐藏在访问者中。
3、增加新的ConcreteElement类很困难Visitor模式使得难以增加新的Element的子类。每添加一个新的ConcreteElement都要在Visitor中添加一个新的抽象操作,并在每一个ConcreteVisitor类中实现相应的操作。有时可以在Visitor中提供一个缺省的实现,这一实现可
以被大多数的ConcreteVisitor继承,但这与其说是一个规律还不如说是一种例外。
所以在应用访问者模式时考虑关键的问题是系统的哪个部分会经常变化,是作用于对象结构上的算法呢,还是构成该结构的各个对象的类。如果老是有新的ConcreteElement类加入进来的话,Visitor类层次将变得难以维护。在这种情况下,直接在构成该结构的类中定义这些操作可能更容易一些。如果Element类层次是稳定的,而你不断地增加操作获修改算法,Visitor模式可以帮助你管理这些改动。
4、通过类层次进行访问一个迭代器可以通过调用节点对象的特定操作来遍历整个对象结构,同时访问这些对象。但是迭代器不能对具有不同元素类型的对象结构进行操作。
5、累积状态当访问者访问对象结构中的每一个元素时,它可能会累积状态。如果没有访问者,这一状态将作为额外的参数传递给进行遍历的操作,或者定义为全局变量。
6、破坏封装访问者方法假定ConcreteElement接口的功能足够强,足以让访问者进行它们的工作。结果是,该模式常常迫使你提供访问元素内部状态的公共操作,这可能会破坏它的封装性。
五、举例
个人认为,Visitor模式是GoF所列举的23种模式中最复杂的,同时由于其使用上的约束较多,日常的应用并不太多,在此不准备对其进行进一步的讨论。以下示例简单演示了Visitor模式的实现方法(Java Code):
abstract class Parts {
abstract void accept(Visitor visitor);
}
// component class: Wheel
class Wheel extends Parts{
private String name;
Wheel(String name){
this.name = name;
}
String getName(){
return this.name;
}
void accept(Visitor visitor){ // function to support double-dispatch
visitor.visit(this);
}
}
// component class: Engine
class Engine extends Parts{
void accept(Visitor visitor){
visitor.visit(this);
}
}
// component class: Body
class Body extends Parts{
void accept(Visitor visitor){
visitor.visit(this);
}
}
// class to demonstrate visitor pattern and double-dispatch. If we don't use double-dispatch, we will lost all class info when we put all components into an array.
class Car{
private Parts[] parts
= { new Engine(), new Body(), new Wheel("front left"), new Wheel("front right"),
new Wheel("back left") , new Wheel("back right") };
void accept(Visitor visitor){
visitor.visit(this);
for(int i=0; i<parts.length; ++i)
parts[i].accept( visitor );
}
}
// visitor interface, all concrete visitor class must implement it.
interface Visitor{ // need a access-function for each element class in the class-hierachy
void visit(Wheel wheel);
void visit(Engine engine);
void visit(Body body);
void visit(Car car);
}
// concrete visitor: PrintVisitor
class PrintVisitor implements Visitor{
public void visit(Wheel wheel){
System.out.println("Visiting "+wheel.getName()
+" wheel");
}
public void visit(Engine engine){
System.out.println("Visiting engine");
}
public void visit(Body body){
System.out.println("Visiting body");
}
public void visit(Car car){
System.out.println("Visiting car");
}
}
// more concrete visitor class, omitted...
// entry class
public class VisitorDemo{
static public void main(String[] args){
Car car = new Car();
Visitor visitor = new PrintVisitor();
car.accept(visitor);
}
}
以下是上述示例对应的C++实现:
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
class Wheel;
class Engine;
class Body;
class Car;
// visitor interface, all concrete visitor class must implement it.
// need a access-function for each element class in the class-hierachy
struct Visitor
{
virtual void visit(Wheel& wheel) = 0;
virtual void visit(Engine& engine) = 0;
virtual void visit(Body& body) = 0;
virtual void visit(Car& car) = 0;
};
struct Parts
{
virtual void accept(Visitor& visitor) = 0;
};
// component class: Wheel
class Wheel : public Parts{
private:
string name;
public:
Wheel(string name)
{
this->name = name;
}
string getName()
{
return this->name;
}
void accept(Visitor& visitor) // function to support double-dispatch
{
visitor.visit(*this);
}
};
// component class: Engine
class Engine : public Parts
{
void accept(Visitor& visitor)
{
visitor.visit(*this);
}
};
// component class: Body
class Body : public Parts
{
void accept(Visitor& visitor)
{
visitor.visit(*this);
}
};
// class to demonstrate visitor pattern and double-dispatch.
// If we don't use double-dispatch, we will lost all class info
// when we put all components into an array.
class Car
{
struct DeleteFunctor
{
void operator () (Parts* p)
{
delete p;
}
};
class AcceptFunctor
{
Visitor& visitor;
public:
AcceptFunctor(Visitor& visitor) : visitor(visitor) {}
void operator () (Parts* p)
{
p->accept(visitor);
}
};
private:
vector<Parts*> vParts;
public:
Car()
{
Parts* pParts[] =
{ new Engine(), new Body(), new Wheel("front left"), new Wheel("front right"),
new Wheel("back left") , new Wheel("back right") };
vector<Parts*> v(pParts, pParts + sizeof(pParts) / sizeof(Parts*));
swap(vParts, v);
}
virtual ~Car()
{
//for_each(vParts.begin(), vParts.end(), DeleteFunctor());
for(vector<Parts*>::iterator it = vParts.begin(); it != vParts.end(); ++it)
delete *it;
}
void accept(Visitor& visitor)
{
visitor.visit(*this);
//for_each(vParts.begin(), vParts.end(), AcceptFunctor(visitor));
for(vector<Parts*>::iterator it = vParts.begin(); it != vParts.end(); ++it)
(*it)->accept( visitor );
}
};
// concrete visitor: PrintVisitor
class PrintVisitor : public Visitor{
public:
void visit(Wheel& wheel)
{
cout << "Visiting " + wheel.getName() + " wheel" << endl;
}
void visit(Engine& engine)
{
cout << "Visiting engine" << endl;
}
void visit(Body& body)
{
cout << "Visiting body" << endl;
}
void visit(Car& car)
{
cout << "Visiting car" << endl;
}
};
// more concrete visitor class, omitted...
// entry function
int main()
{
Car car;
Visitor* pVisitor = new PrintVisitor;
car.accept(*pVisitor);
return 0;
}
在上述实现中,我们可以发现,Visitor模式虽然使得为已有的类型添加新的虚函数的需求变得容易实现,但是,Element类型与Visitor类型之间的耦合十分严重,出现了循环依赖,Visitor需要有所有Element子类的声明,而所有Element子类也需要包含Visitor类的头文件,当需要增加新的Element类型时,由于Visitor类的改动,将造成Element继承体系和Visitor继承体系全部需要重新编译。
那么有什么办法来减轻耦合呢?Martin Robert在其http://www.objectmentor.com/resources/articles/acv.pdf
一文中提出了为每一个Element类型实现一个ConcreteVisitor,并最终通过多继承来实现IntegratedConcreteVisitor以解除这种耦合关系的实现方法,但这种实现方法使得继承体系变得更加复杂,同时还存在一些其它的开销,感兴趣的朋友可以参考<Modern C++ Design>或Robert的文章。
附:
1、关于如何在Java中用Rflection实现Visitor模式,见http://www.javaworld.com/javatips/jw-javatip98_p.html