访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。
访问者(Visitor)模式是一种对象行为型模式,其主要优点如下。
1 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
2 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
3 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
4 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
访问者(Visitor)模式的主要缺点如下。
1 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
2 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
3 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
类图如下:
先看下访问者模式的实现:
#include <iostream>
#include <memory>
using namespace std;
struct ConcreteElement1;
struct ConcreteElement2;
// 访问者基类
struct Visitor
{
virtual ~Visitor();
// 被访问者1
virtual void Visit(ConcreteElement1* element) = 0;
// 被访问者2
virtual void Visit(ConcreteElement2* element) = 0;
};
// 被访问者基类
struct Element
{
virtual ~Element();
virtual void Accept(Visitor& visitor) = 0;
};
// 具体的访问者
struct ConcreteVisitor : public Visitor
{
void Visit(ConcreteElement1* element)
{
cout << "Visit ConcreteElement1" << endl;
}
void Visit(ConcreteElement2* element)
{
cout << "Visit ConcreteElement2" << endl;
}
};
// 具体的被访问者1
struct ConcreteElement1 : public Element
{
void Accept(Visitor& visitor)
{
visitor.Visit(this);
}
// 定义新的操作
void OperationA()
{
cout << "OperationA ConcreteElement1" << endl;
}
};
// 具体的被访问者2
struct ConcreteElement2 : public Element
{
void Accept(Visitor& visitor)
{
visitor.Visit(this);
}
// 定义新的操作
void OperationB()
{
cout << "OperationB ConcreteElement2" << endl;
}
};
void TestVisitor()
{
ConcreteVisitor v;
std::unique_ptr<Element> emt1(new ConcreteElement1);
std::unique_ptr<Element> emt2(new ConcreteElement2);
emt1->Accept(v);
emt2->Accept(v);
}
int main()
{
TestVisitor();
return 0;
}
输出结果:
Visit ConcreteElement1
OperationA ConcreteElement1
Visit ConcreteElement2
OperationB ConcreteElement2
理解下访问者模式:
访问者模式适用于Element继承体系很少改变,但经常需要在此结构上定义新的操作(OperationA,OperationB)。也就是说,在访问者模式中被访问者应该是一个稳定的继承体系,如果这个继承体系经常变化,就会导致经常修改Visitor基类。
因为Visitor基类中定义了需要访问的对象类型,所以每增加一种访问类型就要增加一个对应的纯虚函数。例如,上例中如果需要增加一个被访问者ConcreteElement3,则需要在Visitor基类中增加一个纯虚函数:
// 被访问者3
virtual void Visit(ConcreteElement3* element) = 0;
面向接口的编程原则:我们应该依赖于接口而不应该依赖于实现,因为接口是稳定的,不会变化。
如此看来,如果访问者模式中的被访问者继承体系不够稳定,需要修改Visitor基类接口,那会导致整个系统的不稳定。如何做到增加新的被访问者而不修改Visitor接口呢?我们可以通过C++11的可变参数模板来实现一个稳定的Visitor接口层。
C++11改进后的visitor模式:
#include <iostream>
// 声明一个带可变参数列表的Visitor类
template<typename... Types>
struct Visitor;
template<typename T, typename... Types>
struct Visitor<T, Types...> : Visitor<Types...>
{
// 通过using避免隐藏基类的Visit同名方法
using Visitor<Types...>::Visit;
virtual void Visit(const T&) = 0;
};
// 定义一个带可变参数列表的Visitor类
template<typename... Types>
struct Visitor<T>
{
virtual void Visit(const T&) = 0;
};
// 以下使用Visitor访问“被访问的继承体系”
struct stA;
struct stB;
// 被访问者基类
struct Base
{
// 定义通用的访问者类型,它可以访问stA和stB
typedef Visitor<stA, stB> MyVisitor;
virtual void Accept(MyVisitor&) = 0;
};
// 具体的被访问者1
struct stA : Base
{
double val;
void Accept(Base::MyVisitor& v)
{
v.Visit(*this);
}
}
// 具体的被访问者2
struct stB : Base
{
double val;
void Accept(Base::MyVisitor& v)
{
v.Visit(*this);
}
}
// 具体的访问者
struct ConcreteVisitor: Base::MyVisitor
{
void Visit(const stA& a)
{
cout << "from stA: " << a.val << endl;
}
void Visit(const stB& b)
{
cout << "from stB: " << b.val << endl;
}
};
测试代码如下:
void TestVisitor()
{
ConcreteVisitor visitor;
stA a;
a.val = 1;
stB b;
b.val = 2;
Base base = &a;
a->Accept(visitor);
base = &b;
b->Accept(visitor);
}
测试结果如下:
from stA: 1
from stA: 2
上例中:
typedef Visitor<stA, stB> MyVisitor;
会自动生成stA和stB的Visit虚函数。
struct Visitor<stA, stB>
{
virtual void Visit(const stA&) = 0;
virtual void Visit(const stB&) = 0;
};
如果需要新增被访问者类型,只需添加一个类型就可以了。
typedef Visitor<stA, stB, stC> MyVisitor;
相比较原来的访问者模式,C++11改进后的访问者模式不会因为被访问者继承体系的变化而修改接口层,更加稳定。改进后的访问者模式把Visitor接口层的变化转移到被访问者基类对象(Base)中了。
参考网址: