1 了解访问者模式
访问者模式的应用场景不多,它可以在不改变类成员的前提下定义作用于这些元素的新的操作,是一种数据元素和数据操作分离的设计模式。
2 访问者模式的使用场景
在《设计模式》书籍中,访问者模式的使用场景主要有以下几个方面:
- 需要对对象结构中的对象进行分别处理,又不想影响或者改变原有的对象结构。
- 类结构非常稳定,但是有需要新增新的操作
- 一个对象结构中有很多类对象且包含不同的接口,又想对这些对象实施一些依赖于具体类的操作。
3 访问者模式的类结构
图1 经典访问者模式类结构
类结构说明如下:
- Vistor :访问者抽象类,通过VistorConcrete方法决定可以访问的对象;
- ConcreteVistor:访问者具体类,实现抽象类中的方法,决定访问到该对象类之后实际干什么。
- Element:被访问者抽象类,定义了支持接受访问得到类,通过Accept方法进行获取。
- ConcereElementA:实现基类中的方法。
4 经典访问者模式的实现
代码语言:javascript复制#include <iostream>
#include <memory>
class ConcreteElementA;
class ConcreteElementB;
//访问者类
class Visitor
{
public:
virtual void VisitorConcreteElementA(ConcreteElementA* pElementA)=0;
virtual void VisitorConcreteElementB(ConcreteElementB* pElementB)=0;
};
//访问者类具体实现
class ConcreteVisitorA : public Visitor
{
public:
void VisitorConcreteElementA(ConcreteElementA* pElementA){
std::cout<<"visitor ConcreteElementA"<<std::endl;
};
void VisitorConcreteElementB(ConcreteElementB* pElementB){
std::cout<<"visitor ConcreteElementB"<<std::endl;
};
};
//被访问基类
class Element
{
public:
virtual void Accept(Visitor &pVisitor)=0;
};
//被访问类实例类
class ConcreteElementA : public Element
{
public:
void Accept(Visitor &pVisitor){
pVisitor.VisitorConcreteElementA(this);
};
};
int main(){
std::unique_ptr<Element> pConEle(new ConcreteElementA());
ConcreteVisitorA Convis;
pConEle->Accept(Convis);
return 0;
}
上面的代码就实现了一个经典的访问者模式类,代码运行结果为:
代码语言:javascript复制visitor ConcreteElementA
代码发布上线后,如果改动频繁,就会给生产带来很多的风险,尤其是没有经过充分测试、验证的代码,轻则产生业务故障,重则影响整个业务。在《设计模式》一书中,也强调了访问者模式中被访问者应该是一个稳定的继承结构,如果经常发生变更,就需要经常变更代码。如在上面的代码中,如果需要新增一个被访问对象,就需要修改虚基类Visitor的结构。
5 改进后的访问者模式
根据面向对象的原则,在实际开发中应该是依赖于接口,但是不要依赖于接口的实现为原则,上面经典模式的实现就违反了这一个原则。要想解决这一问题,就需要将Visitor定义成一个稳定的接口层。既:不会因为接口的增加而修改。使用C 11的可变参数模板就可以帮助我们解决这一问题。
可变参数模版可以让接口支撑任意个参数,这一特性也有助于我们实现一个稳定的接口层,下面的代码就通过可变参数模板实现一个稳定的接口层。
代码语言:javascript复制template <typename... Types>
class Visitor;
template <typename T, typename... Types>
class Visitor<T,Types ...>:public Visitor<Types...>{
public:
using Visitor<Types...>::Visit;
virtual void Visit(const T&)=0;
}
template <typename T>
class Visitor <T>{
public:
virtual void Visit(const T&) = 0;
}
上面的代码为每一种类型都定义了一个纯虚函数Visit。下面的代码演示了经过改进后的访问者模式。
代码语言:javascript复制//定义被访问者基类
class ConcreteA;
class ConcreteB;
class Element{
public:
typedef Visitor<ConcreteA,ConcreteB> MyVisitor;
virtual void Accept(MyVisitor&)=0;
};
//具体的被访问者
class ConcreteA:public Element{
public:
std::string strName;
void Accept(Element::MyVisitor &v){
v.Visit(*this);
}
};
class ConcreteB:public Element{
public:
std::string strName;
void Accept(Element::MyVisitor &v){
v.Visit(*this);
}
};
//具体的访问者
class ConcreteVisitor:public Element::MyVisitor{
public:
void Visit(const ConcreteA &concreteA){
cout<<"访问者:"<<concreteA.strName<<endl;
}
void Visit(const ConcreteB &concreteB){
cout<<"访问者:"<<concreteB.strName<<endl;
}
};
int main()
{
ConcreteA vA;
vA.strName="nameA";
ConcreteB vB;
vB.strName="nameB";
Element *ele = &vA;
//定义一个访问者
ConcreteVisitor conVisitor;
ele->Accept(conVisitor);
ele = &vB;
ele->Accept(conVisitor);
return 0;
}
上述代码运行结果如下:
代码语言:javascript复制访问者:nameA
访问者:nameB
在上面的代码中,typedef Visitor<ConcreteA,ConcreteB> MyVisitor会自动生成ConcreteA和ConcreteB的Visit虚拟函数。如下所示:
代码语言:javascript复制struct Visitor<ConcreteA, ConcreteB>
{
virtual void Visit(const ConcreteA&) = 0;
virtual void Visit(constConcreteB&) = 0;
};
如果需要新增新的被访问者,只需要在typedef Visitor<ConcreteA,ConcreteB>中增加新的类型即可。如下所示:
代码语言:javascript复制typedef Visitor<ConcreteA,ConcreteB,ConcreteC,ConcreteD>
新的类型也会重新生成新的访问者接口。
6 总结
从上面的代码示例可知,改进后的Visitor可自动生成虚函数,增加新的被访问者后,不需要修改Visitor访问接口层的代码。和经典的访问者模式实现相比,接口层更加稳定。C 11实现的访问者模式,将之前接口层的变化大部分转移到了被访问者的修改,接口层只需要增加新的类型即可。从维护角度来说,由于变更导致的风险变小了,也更加易于维护。