目录:
trick:Hands-On Design Patterns With C++(零)前言zhuanlan.zhihu.com本文概要:本文是访问者模式第一篇,将对基本的访问者模式及对复杂对象的应用进行讨论,下一篇文章将会着重讲解现代C++中访问者模式的应用。
本文代码链接:
https://github.com/PacktPublishing/Hands-On-Design-Patterns-with-CPP/tree/master/Chapter18github.com访问者模式与多分派
访问者模式是另一种经典的面向对象设计模式,它也是在《设计模式-可复用面向对象软件的基础》一书中介绍的设计模式之一。在面向对象编程兴起的黄金时代,它是最受欢迎的模式之一,原因是它使大型类层次结构更易于维护。 近年来,由于大型复杂的层次结构不是很普遍,所以C++中对访问者模式的使用有所减少,并且访问者模式是一个相当复杂的实现模式。 泛型编程(尤其是C++ 11和C++ 14中添加的新功能)使实现和维护访问者更加容易,其中有很多有意思的内容。
本章将讨论以下问题
- 什么是访问者模式
- 如何使用C++实现访问者模式
- 泛型编程如何简化访问者类
- 使用访问者模式组合对象
- 编译期的访问者模式与反射
访问者模式
什么是访问者模式
访问者模式是一种将算法与对象结构分离的模式,对象结构是算法的数据。通过访问者模式我们可以向类层次结构中添加新操作而无需修改类本身。访问者模式遵循开闭原则(对扩展开放,对修改关闭)。一旦类向其客户端提供接口,该接口就应该保持稳定,不应为维护软件或继续开发而修改类。同时,应该对扩展开放:可以添加新功能以满足新需求。当然实际项目中很难严格的遵循其规则,但是我们一年更改尽量遵守。
访问者模式允许我们向类或整个类层次结构在添加功能的同时无需修改类。在处理公共API时,访问者模式起了作用:使用API的用户可以通过其他操作扩展它,而无需修改源代码。我们用示例来说明此问题:
class Base {
virtual void f() = 0;
};
class Derived1 : public Base {
void f() override;
};
class Derived2 : public Base {
void f() override;
};
如果我们通过指向b基类的指针调用b-> f()
虚拟函数,则根据对象的实际类型,将调用分派到Derived1::f()
或Derived2::f()
。 这是单个调度,实际调用的函数由对象类型确定。现在,我们假设f()
函数也带有一个参数,并且它也是指向基类的指针:
class Base {
// 传入基类指针
virtual void f(Base* p) = 0;
};
class Derived1 : public Base {
void f(Base* p) override;
};
class Derived2 : public Base {
void f(Base* p) override;
};
*p
对象的实际类型也是派生类之一。 现在,b->f(p)
调用可以具有四个不同的版本(Derived1
与Derived2
分别输入为Derived1*
或Derived2*
,四种版本),四个版本的内部实现分别不同是合理的。 这是双重调度:最终运行的代码由两个独立的因素决定。 虚函数没有提供直接实现双重调度的方法,但是访问者模式恰好做到了。(后面几段举例了使用访问者模式可解决的场景,比如序列化、反序列化操作等,不同的序列化反序列化方式可以变为多个访问者来解决,不再翻译)
下面我们从基本访问者模式开始进行讲解吧。
C++实现的基本访问者模式
我们通过一个例子讲解访问者模式,宠物基类为Pet,两个继承类为Cat与Dog:
class Pet {
public:
virtual ~Pet() {}
Pet(const std::string& color) : color_(color) {}
const std::string& color() const { return color_; }
virtual void accept(PetVisitor& v) = 0;
private:
std::string color_;
};
class Cat : public Pet {
public:
Cat(const std::string& color) : Pet(color) {}
void accept(PetVisitor& v) override { v.visit(this); }
};
class Dog : public Pet {
public:
Dog(const std::string& color) : Pet(color) {}
void accept(PetVisitor& v) override { v.visit(this); }
};
现在,我们要在类中添加一些操作,比如喂宠物和玩宠物,实现方式取决与宠物实际的类型。如果基类中声明虚函数,必须将实现添加到每个类中,这样做费时费力(比如上述两个操作喂/玩宠物,需要在基类中添加两个虚接口)。但是通过访问者模式就可以解决次问题,首先我们创建一个PetVisitor
类:
class Cat;
class Dog;
class PetVisitor {
public:
virtual void visit(Cat* c) = 0;
virtual void visit(Dog* d) = 0;
};
我们必须前向声明Pet层次结构类,因为必须在具体的Pet类之前声明PetVisitor
。 现在,我们需要使Pet层次结构可访问,我们需要在基类中增加虚进口,但无论以后要添加多少操作,只需修改一次虚接口。 我们需要添加一个虚函数来接受访问者模式到每个可以访问的类:
class Pet {
public:
virtual void accept(PetVisitor& v) = 0;
.....
};
class Cat : public Pet {
public:
void accept(PetVisitor& v) override { v.visit(this); }
.....
};
class Dog : public Pet {
public:
void accept(PetVisitor& v) override { v.visit(this); }
.....
};
现在我们的Pet层次结构是可访问的,并且我们有了一个抽象的PetVisitor
类。下面我们要对抽象类进行实现。 (请注意,到目前为止,我们所做的任何事情都取决于要添加的操作;我们已经创建了必须实施一次的访问基础结构)。 通过实现派生自PetVisitor
的具体访问者类来添加操作:
class FeedingVisitor : public PetVisitor {
public:
void visit(Cat* c) override { std::cout << "Feed tuna to the " << c->color() << " cat" << std::endl; }
void visit(Dog* d) override { std::cout << "Feed steak to the " << d->color() << " dog" << std::endl; }
};
class PlayingVisitor : public PetVisitor {
public:
void visit(Cat* c) override { std::cout << "Play with feather with the " << c->color() << " cat" << std::endl; }
void visit(Dog* d) override { std::cout << "Play fetch with the " << d->color() << " dog" << std::endl; }
};
根据accept()
传入的访问者类型,我们就可以实现其需要的操作:
Cat c("orange");
FeedingVisitor fv;
c.accept(fv); // Feed tuna to the orange cat
我们还可以按如下方式多态进行调用:
std::unique_ptr<Pet> p(new Cat("orange")); // 基类指针
.....
FeedingVisitor fv;
p->accept(fv);
此外,访问者也可以多态使用:
std::unique_ptr<Pet> p(new Cat("orange"));
.....
std::unique_ptr<PetVisitor> v(new FeedingVisitor); // 多态使用Visitor
.....
p->accept(*v);
我们可以通过一个dispatch()
函数将Pet
与PerVisitor
结合起来:
void dispatch(Pet* p, PetVisitor* v) { p->accept(*v); } // 入参是Pet及PetVisitor
Pet* p = .....; // 多态Pet
PetVisitor* v = .....; // 多态Visitor
dispatch(p, v); // Double dispatch
上面就是C++实现的基本访问者模式的示例,它展示了访问者模式的两方面功能:(1) 在不对类本身实现更改的情况下添加新的功能 (2) 通过调用accept()
方法实现了双重调度。
我们同时也发现了一个缺点,对于基类访问者,如果增加一个纯虚新方法,那其下所有派生访问者无论是否需要此方法,都需要进行更新。后面有一节我们将讨论缓解此问题的一种实现方式。
访问者归纳及其限制
我们改造Pet
的accept()
函数定义,Pet
类中通过vector
跟踪其子对象,同时增加add_child
成员函数向vector
中增加对象,最后对accept
方法进行修改:
class Pet {
public:
....
void add_child(Pet* p) { children_.push_back(p); }
virtual void accept(PetVisitor& v, Pet* p = nullptr) = 0;
private:
std::string color_;
std::vector<Pet*> children_; // 更改
};
子类实现如下:
class Cat : public Pet {
public:
Cat(const std::string& color) : Pet(color) {}
void accept(PetVisitor& v, Pet* p = nullptr) override {
v.visit(this, p);
}
};
class Dog : public Pet {
public:
Dog(const std::string& color) : Pet(color) {}
void accept(PetVisitor& v, Pet* p = nullptr) override {
v.visit(this, p);
}
};
下面我们对访问者基类进行更改,访问者现在要支持add_child
,所以访问者基类PetVisitor
需要接受两个参数:
class PetVisitor {
public:
// 增加第二参数Pet*
virtual void visit(Cat* c, Pet* p) = 0;
virtual void visit(Dog* d, Pet* p) = 0;
};
我们实现BirthVisitor
表示增加Pet操作:
class BirthVisitor : public PetVisitor {
public:
void visit(Cat* c, Pet* p) override {
// assert保证p是Cat类型
assert(dynamic_cast<Cat*>(p));
c->add_child(p);
}
void visit(Dog* d, Pet* p) override {
// assert保证p是Dog类型
assert(dynamic_cast<Dog*>(p));
d->add_child(p);
}
};
上述代码我们通过assert保证visit
函数的第二参数Pet* p
与第一参数类型一致。现在的访问者使用方式如下:
Pet *parent(new Cat("father")); // A cat;
BirthVisitor bv;
Pet *child(new Cat("child"));
parent->accept(bv, child);
下面我们实现打印Pet Family的访问者:
class FamilyTreeVisitor : public PetVisitor {
public:
void visit(Cat* c, Pet*) override {
std::cout << "Kittens: ";
for (auto k : c->children_) {
std::cout << k->color() << " ";
}
std::cout << std::endl;
}
void visit(Dog* d, Pet*) override {
std::cout << "Puppies: ";
for (auto p : d->children_) {
std::cout << p->color() << " ";
}
std::cout << std::endl;
}
};
这时候按照BirthVisitor
的调用方式将会编译出错,原因是FamilyTreeVisitor
访问了Pet的私有成员变量。这时候我们只能在Pet类中将FamilyTreeVisitor
声明为友元,声明为友元后可以正常使用FamilyTreeVisitor
:
class Pet {
......
friend class FamilyTreeVisitor;
};
......
Pet *parent(new Cat("father")); // A cat;
FamilyTreeVisitor fv;
Pet *child(new Cat("child"));
parent->accept(fv, child); // 声明友元后此行不会报错
上述BirthVisitor
与FamilyTreeVisitor
返回值都是void
,下面的例子将改造FamilyTreeVisitor
,让它返回family中child的个数,我们在派生类中增加私有成员变量:
class FamilyTreeVisitor : public PetVisitor {
public:
FamilyTreeVisitor() : child_count_(0) {}
void visit(Cat* c, Pet*) override {
visit_impl(c, "Kittens: ");
}
void visit(Dog* d, Pet*) override {
visit_impl(d, "Puppies: ");
}
void reset() { child_count_ = 0; } // 重置child_count_
size_t child_count() const { return child_count_; } // 返回child_count_
private:
template <typename T>
void visit_impl(T* t, const char* s) {
std::cout << s;
for (auto p : t->children_) {
std::cout << p->color() << " ";
++child_count_;
}
std::cout << std::endl;
}
size_t child_count_; // 私有成员变量
};
注意:上述代码计算child_count_
的方式是有问题的,此时单一FamilyTreeVisitor
计算的是调用它的对象宠物个数的总和,如下代码:
std::unique_ptr<Pet> c(new Cat("orange"));
std::unique_ptr<Pet> d(new Dog("brown"));
BirthVisitor bv;
Cat c1("tabby"), c2("calico");
Dog d1("black");
dispatch(*c, bv, &c1); // cat增加c1
dispatch(*c, bv, &c2); // cat增加c2
dispatch(*d, bv, &d1); // dog增加d1
FamilyTreeVisitor tv;
d->accept(tv);
std::cout << tv.child_count() << " children" << std::endl; // 返回1
c->accept(tv);
std::cout << tv.child_count() << " children" << std::endl; // 返回3
所以如果想分别统计Cat与Dog的个数,需要使用两个FamilyTreeVisitor
对象分别传给Cat与Dog。下一节我们将讲解访问者模式对复杂对象的处理,包括复杂对象的序列化/反序列化问题!
访问复杂对象
上一节中我们回顾了基本的访问者模式,本节我们来访问复杂对象结构,我们将考虑复杂对象引起的问题,同时讨论有效的序列化/反序列化解决方案。
访问复合对象
访问复杂对象的难点在于访问对象本身时,我们通常不知道如何处理每个组件或其包含对象的所有细节。而访问者仅仅是对当前对象的处理,对于复合对象,正确做法是通过委托的方式将Visitor
委托给其内部的每个对象:
举个例子,我们创建一个容器Shelter
类,它包含任意类型的宠物:
class Shelter {
public:
void add(Pet* p) { // 增加宠物
pets_.emplace_back(p);
}
void accept(PetVisitor& v) { // 对每个宠物执行访问者的方法
for (auto& p : pets_) {
p->accept(v);
}
}
private:
std::vector<std::unique_ptr<Pet>> pets_; // 宠物集合
};
关注Shelter
的accept
方法,我们并没有在它的内部直接调用Visitor
,而是委托给vector
中的每个对象。
复合对象又几个较小的对象组成,我们必须对每一个小对象进行访问,比如如下的Family
类:
class Family {
public:
Family(const char* cat_color, const char* dog_color) :
cat_(cat_color), dog_(dog_color)
{}
void accept(PetVisitor& v) {
cat_.accept(v);
dog_.accept(v);
}
private:
Cat cat_;
Dog dog_;
};
Family
类包含猫和狗两种宠物,同样,accept
方法将Visitor分别委托给Cat与Dog进行处理。下面我们就要进入序列化/反序列化问题的讨论!
序列化与反序列化
为了展示使用访问者模式的类层次结构的序列化/反序列化,我们使用一个更加复杂的类结构进行示例,考虑一下二维几何对象的层次结构,示例中几何对象基类Geometry
包含三个派生类:点(Point
),圆(Circle
),线('Line')
class Geometry {
public:
virtual ~Geometry() {}
virtual void accept(Visitor& v) = 0;
};
class Point : public Geometry {
public:
Point() = default;
Point(double x, double y) : x_(x), y_(y) {}
void accept(Visitor& v) override {
v.visit(x_);
v.visit(y_);
}
private:
double x_;
double y_;
};
class Circle : public Geometry {
public:
Circle() = default;
Circle(Point c, double r) : c_(c), r_(r) {}
void accept(Visitor& v) override {
v.visit(c_);
v.visit(r_);
}
private:
Point c_;
double r_;
};
class Line : public Geometry {
public:
Line() = default;
Line(Point p1, Point p2) : p1_(p1), p2_(p2) {}
void accept(Visitor& v) override {
v.visit(p1_);
v.visit(p2_);
}
private:
Point p1_;
Point p2_;
};
所有对象都是Geometry
基类派生,每个派生类属性都更复杂。 Point
对象由两个double组成、Circle
由一个Point
对象和一个double类型的半径组成、直线由两个Point
对象定义。所以我们的Visitor基类必须包含上述所有类型:
class Visitor {
public:
virtual void visit(double& x) = 0;
virtual void visit(Point& p) = 0;
virtual void visit(Circle& c) = 0;
virtual void visit(Line& l) = 0;
};
double是基本属性,我们没有办法为其单独设置一个accept
方法,但是可以通过派生类的accept
方法将double类型传递给Visitor。所以我们的Geometry
类及其派生类的accept
方法实现如下:
class Geometry {
public:
virtual ~Geometry() {}
virtual void accept(Visitor& v) = 0;
};
void Point::accept(Visitor& v) {
v.visit(x_); // double
v.visit(y_); // double
}
void Circle::accept(Visitor& v) {
v.visit(c_); // Point
v.visit(r_); // double
}
void Point::accept(Visitor& v) {
v.visit(p1_); // Point
v.visit(p2_); // Point
}
下面是序列化为字符串的Visitor:
class StringSerializeVisitor : public Visitor {
public:
void visit(double& x) override { S << x << " "; }
void visit(Point& p) override { p.accept(*this); } // 使用
void visit(Circle& c) override { c.accept(*this); }
void visit(Line& l) override { l.accept(*this); }
std::string str() const { return S.str(); }
private:
std::stringstream S;
};
上述visit
除了方法除了入参为double&
的函数外都直接调用传入对象的accept
方法,是因为最后其他对象都会转化到double实现的方法中来。比如Circle::accept
分别调用了Point::accept
和double,而Point::accept
调用了两次double。
其使用方法如下:
Line l(Point(1, 2), Point(5, 2));
Circle c(Point(1, 2), 3);
StringSerializeVisitor serializer;
serializer.visit(l); // visitor给line使用
serializer.visit(c); // visitor给circle使用
std::string s(serializer.str());
下面是反序列化操作,输入序列化后的字符串反解出对象:
class StringDeserializeVisitor : public Visitor {
public:
StringDeserializeVisitor(const std::string& s) { S.str(s); }
void visit(double& x) override { S >> x; }
void visit(Point& p) override { p.accept(*this); }
void visit(Circle& c) override { c.accept(*this); }
void visit(Line& l) override { l.accept(*this); }
private:
std::stringstream S;
};
其反序列化实现如下:
Line l1;
Circle c1;
StringDeserializeVisitor deserializer(s); // String from serializer
deserializer.visit(l1); // 恢复 Line l
deserializer.visit(c1); // 恢复 Circle c
注意,Demo中序列化过程我们是先存Line再存Circle,反序列化过程我们也要先恢复Line再恢复Circle,否则会出错。
实际情况中,我们在反序列化时并不知道对象类型是什么,这些对象存储在可访问的容器中,类似于前面示例中的Shelter
,它必须确定对象序列化和反序列化的顺序。 例如下面这个类Intersection
,该类存储两个几何图形交集的几何图形:
class Intersection : public Geometry {
public:
Intersection() = default;
Intersection(Geometry *g1, Geometry *g2) : g1_(g1), g2_(g2) {}
void accept(Visitor &v) override {
Geometry::type_tag tag;
if (g1_) tag = g1_->tag();
v.visit(tag);
if (!g1_) g1_.reset(Visitor::make_geometry(tag));
g1_->accept(v);
if (g2_) tag = g2_->tag();
v.visit(tag);
if (!g2_) g2_.reset(Visitor::make_geometry(tag));
g2_->accept(v);
}
type_tag tag() const override { return INTERSECTION; }
private:
std::unique_ptr<Geometry> g1_; // 图形g1
std::unique_ptr<Geometry> g2_; // 图形g2
};
我们仍然将visitor委托给复合对象g1_
和g2_
,但是我们无法直接调用accept,因为我们不知道* g1_
和* g2_
的类型且其可能为空。所以我们需要在Visitor中添加两个方法,一个是获取tag来表明对象类型,一个是当对象为空时创建新对象的方法:
class Geometry {
public:
// 两个虚函数保持不变
virtual ~Geometry() {}
virtual void accept(Visitor& v) = 0;
enum type_tag { POINT = 100, CIRCLE, LINE, INTERSECTION }; // 增加type_tag枚举
virtual type_tag tag() const = 0; // 基类添加返回tag的纯虚方法
};
class Visitor {
public:
static Geometry* make_geometry(Geometry::type_tag tag); // visitor增加创建新对象的方法(静态方法,基类实现)
virtual void visit(Geometry::type_tag& tag) = 0; // 新增访问tag的方法,将类型值进行序列化(纯虚,需要派生类实现)
......
virtual void visit(Intersection& l) = 0;
};
class StringSerializeVisitor : public Visitor {
public:
void visit(Geometry::type_tag& tag) override { S << size_t(tag) << " "; } // 派生类实现visit(type_tag)方法,将自己的类型值进行序列化
......
}
class Point : public Geometry {
public:
.....
type_tag tag() const override { return POINT; } // 每个派生类要实现tag方法表明自己的类型
};
Visitor创建新对象方法实现如下:
Geometry* Visitor::make_geometry(Geometry::type_tag tag) {
switch (tag) {
case Geometry::POINT: return new Point;
case Geometry::CIRCLE: return new Circle;
case Geometry::LINE: return new Line;
case Geometry::INTERSECTION: return new Intersection;
}
return NULL; // 仅做示例,外部需要判空!本例中省去了这些异常处理操作
}
我们现在回顾Intersection::accept
方法:
class Intersection : public Geometry {
public:
Intersection() = default;
Intersection(Geometry* g1, Geometry* g2) : g1_(g1), g2_(g2) {}
void accept(Visitor& v) override {
Geometry::type_tag tag;
// 对g1_的操作
if (g1_) tag = g1_->tag(); // 有g1的情况取g1的tag
v.visit(tag); // 序列化g1_的类型值
if (!g1_) g1_.reset(Visitor::make_geometry(tag)); // 没有g1_先通过make_geometry创建g1_再reset一下
g1_->accept(v); // 序列化g1_本身
// g2_操作与g1_一致
if (g2_) tag = g2_->tag();
v.visit(tag);
if (!g2_) g2_.reset(Visitor::make_geometry(tag));
g2_->accept(v);
}
type_tag tag() const override { return INTERSECTION; }
private:
std::unique_ptr<Geometry> g1_;
std::unique_ptr<Geometry> g2_;
};
所以此时反序列化操作如下,将tag从序列化字符串中取出:
class StringDeserializeVisitor : public Visitor {
public:
StringDeserializeVisitor(const std::string& s) { S.str(s); }
void visit(Geometry::type_tag& tag) override {
size_t t;
S >> t;
tag = Geometry::type_tag(t); // 输出tag
}
void visit(double& x) override { S >> x; }
......
void visit(Intersection& x) override { x.accept(*this); }
private:
std::stringstream S;
};
一旦StringDeserializeVisitor
恢复了tag,Intersection
对象就可以调用工厂构造函数来构造正确的几何对象。 现在,我们可以从流中反序列化此对象,并且将Intersection
恢复为我们序列化的对象的精确副本。
下一篇文章我们将讨论访问者模式在现代C++中的应用。