C++设计模式行为模式———访问者模式
一、引言
访问者模式是一种行为设计模式, 它能将算法与其所作用的对象隔离开来。想象我到你家串门去看望你,我就是访问者,你就是被访问者。
假如有这样一位非常希望赢得新客户的资深保险代理人。 他可以拜访街区中的每栋楼, 尝试向每个路人推销保险。 所以, 根据大楼内组织类型的不同, 他可以提供专门的保单:
- 如果建筑是居民楼, 他会推销医疗保险。
- 如果建筑是银行, 他会推销失窃保险。
- 如果建筑是咖啡厅, 他会推销火灾和洪水保险。
二、访问者模式
假设一位老人去医院看病,医生给开了三种药品:
- 阿司匹林肠溶片:改善血液流通,预防血栓形成,血栓形成就产生阻塞,人就会直接面临危险。
- 氟伐他汀钠缓释片:降血脂。因为血脂高意味着血流慢,营养无法运输到身体各部位,还很可能引发心脑血管疾病。
- 黛力新:医生说我的心脏没有器质性病变,是神经出现了紊乱,出现的症状是因为神经的误报,因此开此药治疗自主神经功能紊乱以及身心疾病伴有的焦虑症状。
我拿着这张药品单分别跑到了医院的缴费窗口去缴费,然后又跑到了取药窗口去取药。如果写一段代码来描述药品以及缴费和取药的过程,该怎样做呢?首先可以用几个类来描述药品创建一个药品父类和上述3种药品相关子类,代码如下:
// 药品父类
class Medicine {
public:
virtual std::string getMdcName() = 0; // 纯虚函数,获取药品名称
virtual float getPrice() = 0; // 纯虚函数,获取药品价格
virtual ~Medicine() {} // 虚析构函数,确保子类正确析构
};
// 阿司匹林肠溶片子类
class M_asplcrp : public Medicine {
public:
virtual std::string getMdcName() override {
return "阿司匹林肠溶片";
}
virtual float getPrice() override {
return 46.8f;
}
};
// 氟伐他汀钠缓释片子类
class M_fftdnhsp : public Medicine {
public:
virtual std::string getMdcName() override {
return "氟伐他汀钠缓释片";
}
virtual float getPrice() override {
return 111.3f;
}
};
// 黛力新子类
class M_dlx : public Medicine {
public:
virtual std::string getMdcName() override {
return "黛力新";
}
virtual float getPrice() override {
return 122.0f;
}
};
下面再引入一个用于计算药品的价格(同于缴费)以及描述取药过程。引入一个类MedicineProc来做这样的事:
class MedicineProc {
public:
// 增加药品到药品列表中
void addMedicine(Medicine* p_mdc) {
m_mdclist.push_back(p_mdc);
}
// 针对费用缴纳和取药所做的处理动作
void procAction(std::string strVisitor) {
// strVisitor代表拿到了药品单的人,不同的人拿到药品单所要做的处理也不同
if (strVisitor == "收费人员") { // 收费人员要根据药品单向我(患者)收取费用
float totalCost = 0.0f; // 总费用
for (auto iter = m_mdclist.begin(); iter != m_mdclist.end(); ++iter) {
float tmpPrice = (*iter)->getPrice();
std::cout << "收费人员累计药品\"" << (*iter)->getMdcName() << "\"的价格:" << tmpPrice << std::endl;
totalCost += tmpPrice;
}
std::cout << "所有药品的总价为:" << totalCost << ",收费人员收取了我的费用!" << std::endl;
}
else if (strVisitor == "取药人员") { // 取药人员要根据药品单为我拿药
for (auto iter = m_mdclist.begin(); iter != m_mdclist.end(); ++iter) {
std::cout << "取药人员将药品\"" << (*iter)->getMdcName() << "\"拿给了我!" << std::endl;
}
}
}
private:
std::list<Medicine*>m_mdclist;
};
在MedicineProc类的procAction方法中,通过对拿到了药品单的收费人员或取药人员的判断来进行不同的处理一一收费人员要计算总价格并向我收取费用,取药人员要把指定的药品拿给我。
分析一下MedicineProc类的procAction方法,如果将来我的营养师或健身教练拿到了这个药品单,那么他要根据这个药品单为我配制营养餐或者提供相应的训练建议来改善我的身体状况,那么procAction方法中会继续增加额外的if分支。
显然,这样修改代码是违反开闭原则的。每当增加一个新角色(指上述的营养师或者健身教练)拿到这个药品单,可能对我都会有一些不同的建议或者动作产生。
通过采用访问者模式编写代码,当新角色拿到这个药品单后并针对我提出不同的建议或动作时,不再需要在procAction方法中增加if分支,只需要编写新的类来实现新角色要完成的事情即可。换句话说,就是把procAction方法中各个if分支中的代码拆解到诸多子类中去实现,这样就做到了不修改原有代码也能增加新的角色从而增加新的操作方式的目的。

访问者模式包含访问者和被访问者两个重要角色,被访问者也常常称为“元素”。在上述范例中,收费人员、取药人员、营养师、健身教练这些都属于访问者,而药品单中的3种药品,属于被访问者。
对于“访问”这件事,读者理解的不要过于复杂,看看如下两个情形:
- 想象某人到你家去串门,该人就是访问者,你就是被访问者;
- 某人查看一个东西,某人就是访问者,被查看的东西就是被访问者(所以上述收费人员、取药人员等都是访问者,而各种药品就是被访问者)。
在明确了访问者和被访问者之后,上述范例代码就可以通过访问者模式来改写了。在访问者模式中,药品父类Medicine以及各个药品子类M_asplcrp、M_fftdnhsp、M_dlx依然存在,但其中的代码会增加一些。这里将引入一个访问者父类(抽象访问者类)以及若干访问者子类来取代原有的MedicineProc(药品处理相关)类,每个访问者子类就代表上述的一个访问者(收费人员、取药人员、营养师、健身教练)。访问者父类代码如下:
class Visitor{
public:
virtual ~Visitor(){}
virtual void Visit_elm_asplcrp(M_asplcrp* pelem) = 0;
virtual void Visit_elm_fftdnhsp(M_fftdnhsp* pelem) = 0;
virtual void Visit_elm_dlx(M_dlx* pelem) = 0;
};
在上述访问者父类的代码中,除虚析构函数外,不难看到还有3个虚函数Visit_elm_asplcrp、Visit_elm_fftdnhsp、Visit_elm_dlx,这3个虚函数用于访问各个被访问者(药品/元素)。注意,它们的形参都是指针类型,而且这些指针的类型正好是3种药品(3个被访问者)类型。谈到这里,就需要说一下访问者模式使用时的一个局限性,就是要求被访问者的品种尽可能固定而不发生改变。
在本范例中,被访问者是3种药品,在不增加或者减少药品品种的情况下,使用访问者模式最合适(扩充功能时代码改动量最小),否则如果将来要增加第四种药品(增加第四个被访问者),则必须扩充Visitor类,增加一个新的虚函数接口来访问第四种药品,该虚函数接口的形参也必须是一个指针,并且是指向第四种药品类型的指针。这样代码的改动量就比较大,因为后续Visitor的各个子类也需要增加相应的虚函数。这3个用于访问各个被访问者的接口名称可以各不相同(例如,上述的Visit_elm_asplcrp、Visit_elm_fftdnhsp、Visit_elm_dlx,方法名中就标明了要访问的元素的名字),也可以完全相同(成员函数重载),只是参数类型不同。
接着实现收费人员和取药人员访问者子类(营养师、健身教练访问者子类),每个子类都要实现Visitor父类中声明的3个虚函数:
// 收费人员访问者子类
class Visitor_SFRY : public Visitor {
public:
virtual void Visit_elm_asplcrp(M_asplcrp* pelem) override {
float tmpprice = pelem->getPrice();
cout << "收费人员累计药品《" << pelem->getMdcName() << "》的价格:" << tmpprice << endl;
m_totalcost += tmpprice;
}
virtual void Visit_elm_fftdnhsp(M_fftdnhsp* pelem) override {
float tmpprice = pelem->getPrice();
cout << "收费人员累计药品《" << pelem->getMdcName() << "》的价格:" << tmpprice << endl;
m_totalcost += tmpprice;
}
virtual void Visit_elm_dlx(M_dlx* pelem) override {
float tmpprice = pelem->getPrice();
cout << "收费人员累计药品《" << pelem->getMdcName() << "》的价格:" << tmpprice << endl;
m_totalcost += tmpprice;
}
// 返回总费用
float getTotalCost() {
return m_totalcost;
}
private:
float m_totalcost = 0.0f; // 总费用
};
// 取药人员访问者子类
class Visitor_QYRY : public Visitor {
public:
virtual void Visit_elm_asplcrp(M_asplcrp* pelem) override {
cout << "取药人员将药品《" << pelem->getMdcName() << "》拿给了我!" << endl;
}
virtual void Visit_elm_fftdnhsp(M_fftdnhsp* pelem) override {
cout << "取药人员将药品《" << pelem->getMdcName() << "》拿给了我!" << endl;
}
virtual void Visit_elm_dlx(M_dlx* pelem) override {
cout << "取药人员将药品《" << pelem->getMdcName() << "》拿给了我!" << endl;
}
};
接着说回药品父类Medicine(被访问者父类)。前面说过,在访问者模式中,被访问者通常称为元素,那么在元素父类Medicine中,会声明一个叫作Accept的方法(虚成员函数)用于接受访问者的访问。Accept翻译成中文就是“接受”的意思,作为被访问者,接受访问者的访问是必需的。完整的Medicine类实现代码如下:
class Medicine {
public:
virtual std::string getMdcName() = 0; // 纯虚函数,获取药品名称
virtual float getPrice() = 0; // 纯虚函数,获取药品价格
virtual ~Medicine() {} // 虚析构函数,确保子类正确析构
virtual void Accept(Visitor* pvisitor) = 0; //访问者的父类指针
};
//各个药品子类Accept方法的实现体代码
void M_asplcrp::Accept(Visitor* pvisitor) {
pvisitor->Visit_elm_asplcrp(this);
}
void M_fftdnhsp::Accept(Visitor* pvisitor) {
pvisitor->Visit_elm_fftdnhsp(this);
}
void M_dlx::Accept(Visitor* pvisitor) {
pvisitor->Visit_elm_dlx(this);
}
通过上面的代码可以看到,各个元素子类的Accept方法中,实际上是通过调用Visitor各个子类的以“Visit_"开头的方法来实现对元素的访问的,尤其应注意给这些以“Visit_”开头的方法所传递的实参都是this,这意味着将当前具体元素类(各个药品子类)对象本身作为参数传递给了这些以“Visit_”开头的方法。
现在我们可以这样使用 收费人员访问子类,以根据药品对我进行收费:
Visitor_SFRY vis;
M_asplcrp mdc;
M_dlx mdcc;
M_fftdnhsp f;
mdc.Accept(&vis);
mdcc.Accept(&vis);
f.Accept(&vis);
cout << vis.getTotalCost();
如果要根据药品单 为患者取药,那么使用取药人员访问者子类Visit_oQYRY实现即可,在main主函数中继续增加如下代码:
Visitor_QYRY qs;
mdc.Accept(&qs);
mdcc.Accept(&qs);
f.Accept(&qs);
相信读者已经从上面的代码中总结出一些规律一一只要针对各个被访问者(药品/元素)对象(mdc_asplcrp、mdc_fftdnhsp、mdc_dlx)调用它们的Accept成员函数,并为Accept传递一个访问者子类对象(visitor_sf、visitor_qy)作为实参,那么这个访问者子类中所代表的算法就会执行。例如,visitor_sf对象代表的是收费人员访问者子类,visitor_qy对象代表的是取药人员访问者子类。从上面程序的输出结果不难看出,这两个访问者子类中的算法(一个用于累计药品价格以向我收取费用,一个用于为我拿药)都已经执行了。
如果现在想扩充一下算法,那么只要增加新的访问者子类就可以做到,例如,本节开头说,如果营养师拿到了这个药品单,他就会根据这个药品单为我配制营养餐,为了实现这个算法,可以增加一个营养师访问者子类,代码如下:
class Visitor_YYS : public Visitor {
public:
virtual void Visit_elm_asplcrp(M_asplcrp* pelem) override {
cout << "营养师建议:多吃粗粮少吃油、可以预防血栓" << endl;
}
virtual void Visit_elm_fftdnhsp(M_fftdnhsp* pelem) override {
cout << "营养师建议:多吃蘑菇、洋葱、猕猴桃,可以降低血脂 " << endl;
}
virtual void Visit_elm_dlx(M_dlx* pelem) override {
cout << "营养师建议:多出去走走呼吸新鲜空气" << endl;
}
};
同理,如果增加一个健身教练访问者子类,则可根据药品单为患者配置一些改善身体当前状况的健身方法(算法),这个子类的实现可以参考Visitor_YYS子类的实现。这充分说明,不同的访问者子类,可以对这些药品(元素)进行不同的访问操作,这样就可以在不修改现有代码的情况下扩展程序的功能,为元素增加新操作。

访问者模式一般有五种角色:
- 抽象访问者(Visitor):对象结构中的每一个元素子类声明一个访问操作,从该操作的名称或参数类型可以明确的知道要访问的元素子类的类型,具体的访问者子类要实现这些访问操作。
- 具体访问者(ConcreteVisitor):实现每个由抽象访问者声明的访问操作,注意,每一个操作用于访问对象结构中的一种类型的元素。这里指
Visitor_SFRY、Visitor_QYRY、Visitor_YYS类。 - 抽象元素(Element):定义了一个Accept方法,该方法的形参通常是一个指向抽象访问者类型的指针。这里指
Medicine类。 - 具体元素(ConcreteElement):实现
Accept方法,在该方法中调用访问者子类中的访问操作(Visit_开头的方法)以让访问者完成对一个元素的操作。这里指M_asplcrp、Mfftdnhsp、M_dlx类。 - 对象结构(ObjectStructure):一个元素的集合,用来存放元素对象并提供遍历其内部元素的接口,一般用于对一批元素进行同一种操作。
访问者模式结构

引人访问者模式的定义(实现意图):提供一个作用于某对象结构中的各元素的操作表示,便可以在不改变各元素类的前提下定义(扩展)作用于这些元素的新操作。
也就是说,允许一个或者多个操作应用到一组对象上,使对象本身和操作解耦。
三、总结
在访问者模式中,数据结构(元素)与针对数据结构的操作/处理(访问者)被分隔开了,通过访问者子类来访问数据结构中的元素,对这些元素的处理交给访问者子类。当需要增加新的处理时,只需要编写新的访问者子类并让数据结构接受(Accept)访问者的访问即可,如果不进行这样的分隔,那么这些操作就得放人元素所属的类中。
而且可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。也可以使用访问者对整个组合模式树执行操作。
更多推荐




所有评论(0)