访问者模式(Visitor Pattern),据说是一种相对复杂,而且使用条件苛刻的模式。但是Gof既然把它归到23种设计模式里面,那总有它存在的道理。
在职责链模式里面,有个领导审批的例子,http://blog.csdn.net/zj510/article/details/8156852。CLeader类有审批接口,如:
class CLeader
{
public:
CLeader(CLeader* leader = 0, float budget = 0):_successor(leader), _budget(budget){}
virtual void ApproveBudget(float budget) = 0;
protected:
CLeader* _successor;
float _budget;
};
通常领导都是很牛的,绝对不止审批预算这么简单。还有很多其他的功能。比如安排个活动啥的。那么我们现在假如要给领导加个安排活动功能,该怎么办呢?通常有如下办法:
1. 给CLeader类加个接口,比如void ArrangeAction()。这样可以达到目的,但是缺点也很明显,这个改动可能会影响到所有CLeader的子类。同时它也违背了设计的基本原则,开闭原则。
2. 继承CLeader类,创建一个新的类。考虑这么一种情况,假设系统中很多地方已经使用了CLeader类作为参数。那么怎么调用CLeader类的子类里面的ArrangeAction函数呢?像c++这种静态语言怕是做不到的。
OK, 轮到本文的主角出场了:访问者模式。Gof是这么定义的:
意图
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
结构
我们尝试将职责链里面的例子的领导类使用访问者模式来扩展操作。
首先定义领导类,也就是访问者模式里面的元素角色。
class CLeader
{
public:
CLeader(CLeader* leader = 0, float budget = 0):_successor(leader), _budget(budget){}
//访问者模式
virtual void accept(CVisitor* v, void* reserved) = 0;
CLeader* GetSuccessor(){return _successor;}
protected:
CLeader* _successor;
public:
float _budget;
};
注意,里面有一个函数:accept()。通常accept函数一定会有一个访问者参数,这样caller可以把访问者传递给元素类。我个人喜欢给accept函数增加一个参数:void* reserved。有必要的时候可以传递一些额外的信息。
看看两个子类的实现:
class CTeamLeader: public CLeader
{
public:
CTeamLeader(CLeader* leader = 0, float budget = 0):CLeader(leader, budget){}
//不可以放到基类,因为访问者类的函数声明里面用的是子类
virtual void accept(CVisitor* v, void* reserved)
{
v->Visit(this, reserved);
};
};
class CDM: public CLeader
{
public:
CDM(CLeader* leader = 0, float budget = 0): CLeader(leader, budget){}
//不可以放到基类,因为访问者类的函数声明里面用的是子类
virtual void accept(CVisitor* v, void* reserved)
{
v->Visit(this, reserved);
};
};
相当的简单,在accept里面通过访问者对象参数调用访问者的Visit函数,同时将reserved参数传递给访问者。
接下来看看访问者类
class CVisitor
{
public:
//为每一个元素类声明一个访问函数。访问函数会访问元素子类的特性,而某些特性并不一定会在基类里面存在,所以,参数不可以使用基类。
virtual void Visit(CTeamLeader* element, void* reserved) = 0;
virtual void Visit(CDM* element, void* reserved) = 0;
};
里面有两个函数Visit(),参数分别是CTeamLeader和CDM。为什么不使用它们的基类CLeader呢?因为访问者会访问元素类的某些属性,而这些属性并不一定存在于基类CLeader中。其实这也是访问者模式的一个缺点,有时元素类需要公开一些属性让访问者访问,这就破坏了封装性。
OK,现在可以通过创建一个CVisitor的子类来实现职责链模式例子里面的预算审批功能。
class CApprovalVisitor: public CVisitor
{
public:
virtual void Visit(CTeamLeader* element, void* reserved)
{
float* budget = (float*)reserved;
if (*budget <= element->_budget)
{
std::cout << "Team leader approve process budget: " <<*budget << "\n";
}
else
{//超出能力范围,找后继者审批
CLeader* successor = element->GetSuccessor();
if (successor)
{
successor->accept(this, reserved);
}
else
{
std::cout << "approval failed budget: " <<*budget <<"\n";
}
}
}
virtual void Visit(CDM* element, void* reserved)
{
float* budget = (float*)reserved;
if (*budget <= element->_budget)
{
std::cout << "Department manager approve process budget: " << *budget << "\n";
}
else
{
CLeader* successor = element->GetSuccessor();
if (successor)
{
successor->accept(this, reserved);
}
else
{
std::cout << "approval failed budget: " <<*budget <<"\n";
}
}
}
};
我们在Visit函数里面分别实现Team leader和DM的审批功能。如果当前元素对象没有权力,那么就传递给后继者(职责链模式)。
看看怎么调用:
//先创建一个职责链,team leader的后继者是部门经理dm
CLeader* dm = new CDM(0, 1000);
CLeader* tl = new CTeamLeader(dm, 100);
//创建一个审批访问者
CApprovalVisitor* approval = new CApprovalVisitor();
float budget1 = 50;
float budget2 = 200;
float budget3 = 3000;
tl->accept(approval, (void*)&budget1);//发起50块的审批请求
tl->accept(approval, (void*)&budget2);//发起200块的审批请求
tl->accept(approval, (void*)&budget3);//3000的请求,返回失败,因为DM也审批不了
输出:
呵呵,成功地将领导类改成了访问者模式。其实也就是将领导类的行为给分离了出来。通过创建访问者类来实现领导类的行为。一个访问者类可以实现一个操作。比如上面的例子里面,通过类CApprovalVisitor实现了原来职责链例子里面的审批功能。
那么访问者到底有什么好处呢?就想文章前面说的,领导的功能不止预算审批这么简单。比如领导还会给员工安排活动。那么如何通过访问者模式扩展接口呢?可以这么做,新创建一个访问者子类。比如:
class CActionVisitor: public CVisitor
{
public:
virtual void Visit(CTeamLeader* element, void* reserved)
{
//这里可以使用元素的属性。其实这个地方有个潜在的问题,
//访问者模式有可能会破坏类的封装性,如果访问者想多获取一些元素的信息,
//那么元素类就需要多公开一些属性。
cout << "team leader arrange action < 500 yuan\n";
}
virtual void Visit(CDM* element, void* reserved)
{
cout << "department manager arrange action < 1000 yuan\n";
}
};
在visit函数里面,实现安排活动的功能。team leader可以安排500以下的活动,部分经理就牛一些,可以安排1000块的。使用:
//创建一个活动访问者
CActionVisitor* action = new CActionVisitor();
tl->accept(action, 0);
dm->accept(action, 0);
这就是扩展了领导类的功能,通过改变访问者对象,实现了不同的功能,前面是审批功能,现在是活动功能。这个过程无需修改领导类,还是使用领导类的accept接口。这就是访问者模式的好处。在不需要修改元素类的前提下,给元素类增加功能。完整测试代码:
void Pattern_Visitor()
{
//先创建一个职责链,team leader的后继者是部门经理dm
CLeader* dm = new CDM(0, 1000);
CLeader* tl = new CTeamLeader(dm, 100);
//创建一个审批访问者
CApprovalVisitor* approval = new CApprovalVisitor();
float budget1 = 50;
float budget2 = 200;
float budget3 = 3000;
tl->accept(approval, (void*)&budget1);//发起50块的审批请求
tl->accept(approval, (void*)&budget2);//发起200块的审批请求
tl->accept(approval, (void*)&budget3);//3000的请求,返回失败,因为DM也审批不了
//创建一个活动访问者
CActionVisitor* action = new CActionVisitor();
tl->accept(action, 0);
dm->accept(action, 0);
delete tl;
delete dm;
delete approval;
delete action;
}
注意:
1. 访问者模式里面有个参与者叫做ObjectStructure。也就是存放元素对象的结构。这个例子里面这个结构可以想象为职责链里面的链。也就是说访问者模式给这个链里面的所有元素增加新的接口。当然还可以有其他的结构,比如存放元素对象的一个list或者array啥的。
2. 双分派的概念。双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型。在访问者模式里面,通常client会创建一个访问者对象,并且传递给元素对象,元素对象再调用传进来的访问者的对象的接口。就好比例子里面,client创建CApprovalVisitor对象,传给CTeamLeader,CTeamLeader的accept函数里面再调用CApprovalVisitor的接口如Visit()。看这个调用tl->accept(approval, (void*)&budget1);这行代码的执行由tl和approval两个对象决定。第一次分派指tl->accept(),然后在accept里面进行第二次分派:v->Visit(this, reserved);
优点:
不需要修改元素类就可以增加新的接口,所需要做的就是创建一个新的访问者。
缺点:
1. 可能需要元素类公开一些属性,比如CLeader类的budget属性,也就是每个领导所能审批的上限。如果不公开,那么访问者将无法进行审批判断。这有可能破坏元素类的封装性。
2. 假如新增加一个领导,总经理。那么需要在访问者基类里面增加新的接口,这就会影响所有的访问者子类。这是个很大的代码。所以使用访问者模式的前提就是:在对象结构里面的元素比较稳定的情况下,并且元素的操作需要变动,这个时候我们才考虑访问者模式。如果元素种类会增加,那么访问者模式就不太适合了。