0%

3.1 Vistor与双分派

Visitor 表示作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
在我理解,Visitor 就是把离散分布在 子类中的某种操作 提取出来放到某一个 Visitor对象中去。虽然有时候这些操作直接放到 子类中也还好,但是会让各个子类的方法膨胀(想象每多一种操作就要多一个基类方法,所有子类就要跟着添加实现)

相关类基本接口如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct Visitor;// forward declaration
struct Obj{
virtual void Accept(Visitor *v);
};

struct OA;// forward declaration also
struct OB;
struct OB;

struct Visitor{
virtual void AcceptOA(OA*o);
virtual void AcceptOB(OB*o);
virtual void AcceptOC(OC*o);
// may OD OE
};

struct OA:public Obj{
void Accept(Visitor *v)override{
v->AcceptOA(this);
}
};
struct OB:public Obj{
void Accept(Visitor *v)override{
v->AcceptOB(this);
}
};
struct OC:public Obj{
void Accept(Visitor *v)override{
v->AcceptOC(this);
}
};

这里有一种很简单的 Visitor

1
2
3
4
5
struct PrintNameVisitor{
virtual void AcceptOA(OA*o)override{ printf("PrintNameVisitor::AcceptOA(%x))\n",o); }
virtual void AcceptOB(OB*o)override{ printf("PrintNameVisitor::AcceptOB(%x))\n",o); }
virtual void AcceptOC(OC*o)override{ printf("PrintNameVisitor::AcceptOC(%x))\n",o); }
}

其实无论用与不用visitor收集操作到具体的visitor 类,实际上需要添加的方法都是一样多的,区别只在于 位置。用了visitor之后就把这种行为统一放到了 Visitor子类中。另外要注意,Visitor作为外部类,访问具体元素时只能访问公共接口,有时候会添加一些复杂度。
归根到底Visitor是一种方法实现位置的选择,

关于避免元素类的 方法数目膨胀(以及由此带来的 同类visitor行为的代码分散)不再赘述。

双分派

多分派的意义见 wiki 多分派
x分派的意思是 运行时方法调用依赖于x个参数动态路由到某个具体实现
C++的虚函数就是一种单分派,因为一次虚函数的调用,依赖于this指针的vfptr,然后查一次表,最后找到子类实现。虚函数调用过程依赖于一个参数 (this指针隐式传入), 即是单分派。注意虚函数是一种单分派的实现方式,但不是所有。

Visitor的双分派过程

此处有用户代码

1
2
3
4
struct Visitor* vis = new PrintNameVisitor;
struct Obj o =new ObjA;
// 注意此处 vis 和 o 都是接口
o->Accept(&vis);

语句 o->Accept(&vis); 有两个参数: oa 和 vis.
第一次分派 发生在 o->Accept 的虚函数查表执行,由此进入 OA::Accept(Visitor * vis)

1
2
3
void OA::Accept(Visitor *vis)override{
vis->AcceptOA(this);
}

第二次分派 发生在 vis->AcceptOA(this); 最终进入 PrintNameVisitor::AcceptOA
这就是Visitor的双分派过程。
从此意义上讲,建议Accept改名叫Dispatch

可能的多分派实现

将元素类和Vistor都看作 多分派的参数,由此有

1
2
3
4
5
6
7
8
9
10
11
12
13
// user code
arg0->Dispatch(I0 arg0, I1 arg1, In argn...);
// 第一次分派之后进入 , 得知 arg0 具体类型,调用 I1::DispatchC0
ConcreteArg0::Dispath(I0 arg0,I1 arg1,In argn...){
arg1->DispatchC0(arg0,arg1,argn...);
}
// 第二次分派之后进入,此时再次得知 arg1 具体类型, 调用I1::DispatchC0C1
ConcreteArg1::DispatchC0(arg0,arg1,argn...){
arg2->DispatchC0C1(arg0,arg1,argn...);
}
// 最终进入,这里的所有参数都是具体类型的
ConcreteArgn::DispatchC0C1Cn(arg0,arg1,argn...){
}

Visitor与双分派分析

Obj::Accept(Visitor*) 是一个双分派的入口,相关分派参数是 Obj 和 Visitor。最终分派的目标实现在 Vistor实例中例如 PrintNameVisitor::AcceptOA.

  1. 双分派不影响分派目标的实现。就是说,不管用不用双分派,针对 Obj集合Visitor集合(或者操作集合)的每一种组合都要求有实现,在这里双分派只是给了另一种放置代码的选择(以前针对同Obj的操作都放到Obj中,现在针对同种操作的都放到Visitor中)
  2. 双分派抽象了行为到Visitor对象,由此增加操作行为的调度的代码的复用。比如
    1
    2
    3
    4
    Obj * obj = createObj();
    Visitor * viss[]={new PrintNameVisitor,new EatLaunchVisitor,new BalahVisitor};
    for(auto vis:viss)
    obj.Accept(vis);
  3. 破坏封装。因为visitor作为外部类操作元素类,所以可能会要求元素类公开某些属性