访问者模式与多重分派详解
1. 访问者模式的多线程限制与解决方案
在多线程程序中,访问者模式存在一定限制。由于多个线程不能使用同一个访问者对象来访问不同的宠物对象,所以该访问者并非线程安全的。常见的解决办法是为每个线程使用一个访问者对象,通常是在调用访问者的函数栈上创建的局部变量。若此方法不可行,还有更复杂的方案可赋予访问者线程局部状态,但对这些方案的分析暂不展开。
有时,我们希望在多次访问中累积结果,此时将结果存储在访问者对象中的技术就十分适用。同样,我们也可以利用这种方法将参数传递给访问者操作,而无需将参数添加到
visit()
函数中,只需将参数存储在访问者对象内部,就能方便地从访问者中访问这些参数。这种技术在每次调用访问者时参数不变,但不同访问者对象的参数可能不同的情况下尤为有效。
2. 访问复杂对象
2.1 访问复合对象的一般思路
访问复杂对象时,当访问对象本身,我们通常不清楚如何处理每个组件或包含的对象。但针对该对象类型编写的访问者专门用于处理该类对象。因此,处理组件对象的正确方式是逐个访问它们,将问题委托给专门的访问者,这在编程及其他领域都是一种强大的技术。
2.2 简单容器类示例
以
Shelter
类为例,它可包含任意数量代表等待领养宠物的宠物对象:
// Example 06
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
对象被销毁时,向量中的所有
Pet
对象也会被销毁。任何包含唯一指针的容器都拥有其包含的对象,这是将多态对象存储在
std::vector
等容器中的正确方式。
Shelter::accept()
方法决定了如何访问
Shelter
对象。我们不直接在
Shelter
对象上调用访问者,而是将访问委托给每个包含的对象。由于我们的访问者已能处理
Pet
对象,无需额外操作。例如,当
Shelter
被
FeedingVisitor
访问时,庇护所中的每只宠物都会被喂食,无需编写特殊代码。
2.3 家庭对象示例
考虑一个代表有两只家庭宠物(一只狗和一只猫)的家庭对象:
// Example 07
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: // Other family members not shown for brevity
Cat cat_;
Dog dog_;
};
使用
PetVisitor
层次结构中的访问者访问家庭时,会将访问委托给每个
Pet
对象,访问者已有处理这些对象的能力。当然,
Family
对象也可接受其他类型的访问者,需为其编写单独的
accept()
方法。
3. 使用访问者模式进行序列化和反序列化
3.1 序列化和反序列化问题概述
序列化时,每个对象需转换为位序列,这些位需存储、复制或发送。操作的第一部分取决于对象本身(每个对象的转换方式不同),第二部分取决于序列化的具体应用(保存到磁盘与通过网络发送不同)。实现依赖于两个因素,这正是访问者模式提供的双重分派的用武之地。若能序列化某个对象并反序列化(从位序列重建对象),则在该对象包含在其他对象中时,也应使用相同方法。
3.2 二维几何对象层次结构示例
考虑以下二维几何对象的层次结构:
// Example 08
class Geometry {
public:
virtual ~Geometry() {}
};
class Point : public Geometry {
public:
Point() = default;
Point(double x, double y) : x_(x), y_(y) {}
private:
double x_ {};
double y_ {};
};
class Circle : public Geometry {
public:
Circle() = default;
Circle(Point c, double r) : c_(c), r_(r) {}
private:
Point c_;
double r_ {};
};
class Line : public Geometry {
public:
Line() = default;
Line(Point p1, Point p2) : p1_(p1), p2_(p2) {}
private:
Point p1_;
Point p2_;
};
所有对象都派生自抽象的
Geometry
基类,较复杂的对象包含一个或多个较简单的对象。最终,所有对象都由双精度数组成,因此会序列化为数字序列。关键在于知道哪个双精度数代表哪个对象的哪个字段,以便正确还原原始对象。
3.3 序列化和反序列化的实现步骤
3.3.1 声明基访问者类
// Example 08
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;
};
这里可访问双精度值,每个访问者需适当处理它们(写入、读取等)。访问任何几何对象最终都会访问其组成的数字。
3.3.2 基类和派生类接受访问者
// Example 08
class Geometry {
public:
virtual ~Geometry() {}
virtual void accept(Visitor& v) = 0;
};
虽然无法为双精度类型添加
accept()
成员函数,但这并不影响。派生类的
accept()
成员函数会按顺序访问每个数据成员:
// Example 08
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 Line::accept(Visitor& v) {
v.visit(p1_); // Point
v.visit(p2_); // Point
}
3.3.3 具体访问者类负责序列化和反序列化
以将所有对象序列化为字符串为例:
// Example 08
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;
};
// Example 08
Line l(...);
Circle c(...);
StringSerializeVisitor serializer;
serializer.visit(l);
serializer.visit(c);
std::string s(serializer.str());
3.3.4 反序列化访问者
// Example 08
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;
};
// Example 08
Line l1;
Circle c1;
// s is the string from a serializer
StringDeserializeVisitor deserializer(s);
deserializer.visit(l1); // Restored Line l
deserializer.visit(c1); // Restored Circle c
3.4 处理未知对象类型的反序列化
当反序列化时不知道会遇到什么对象,对象存储在可访问的容器中,需确保对象按相同顺序序列化和反序列化。以
Intersection
类为例,它存储表示两个其他几何图形交集的几何图形:
// Example 09
class Intersection : public Geometry {
public:
Intersection() = default;
Intersection(Geometry* g1, Geometry* g2) :
g1_(g1), g2_(g2) {}
void accept(Visitor& v) override {
g1_->accept(v);
g2_->accept(v);
}
private:
std::unique_ptr<Geometry> g1_;
std::unique_ptr<Geometry> g2_;
};
该对象的序列化很直接,将细节委托给包含的对象。但反序列化会失败,因为几何指针为空,尚未分配对象,也不知道应分配何种类型的对象。需要先在序列化流中编码对象类型,再根据这些编码类型构造对象,这可借助工厂模式解决。
3.4.1 定义类型标签和工厂构造函数
// Example 09
class Geometry {
public:
enum type_tag {POINT = 100, CIRCLE, LINE, INTERSECTION};
virtual type_tag tag() const = 0;
};
class Visitor {
public:
static Geometry* make_geometry(Geometry::type_tag tag);
virtual void visit(Geometry::type_tag& tag) = 0;
...
};
在每个派生的
Geometry
类中定义
tag()
方法,例如
Point
类:
// Example 09
class Point : public Geometry {
public:
...
type_tag tag() const override { return POINT; }
};
3.4.2 定义工厂构造函数
// Example 09
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;
}
}
3.4.3 序列化和反序列化类型标签
// Example 09
class Intersection : public Geometry {
public:
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);
}
...
};
// Example 09
class StringSerializeVisitor : public Visitor {
public:
void visit(Geometry::type_tag& tag) override {
S << size_t(tag) << " ";
}
...
};
// Example 09
class StringDeserializeVisitor : public Visitor {
public:
void visit(Geometry::type_tag& tag) override {
size_t t;
S >> t;
tag = Geometry::type_tag(t);
}
...
};
3.5 无环访问者模式
3.5.1 传统访问者模式的问题
传统访问者模式虽能将算法实现与数据对象分离,允许根据运行时的具体对象类型和操作选择正确的实现,但存在一些问题。我们需维护两个平行的类层次结构(可访问对象和访问者),且两者之间的依赖关系复杂,形成循环依赖。每次向层次结构中添加新对象,都需更新每个访问者。此外,传统访问者模式必须处理对象类型和访问者类型的所有可能组合,即便某些组合无意义。
3.5.2 无环访问者模式的实现
无环访问者模式旨在打破依赖循环并允许部分访问。其基可访问类与常规访问者模式相同:
// Example 10
class Pet {
public:
virtual ~Pet() {}
virtual void accept(PetVisitor& v) = 0;
...
};
但基访问者类没有为每个可访问对象提供
visit()
重载,甚至没有
visit()
成员函数:
// Example 10
class PetVisitor {
public:
virtual ~PetVisitor() {}
};
对于原始层次结构中的每个派生类,我们声明相应的访问者类,其中包含
visit()
函数:
// Example 10
class Cat;
class CatVisitor {
public:
virtual void visit(Cat* c) = 0;
};
class Cat : public Pet {
public:
Cat(std::string_view color) : Pet(color) {}
void accept(PetVisitor& v) override {
if (CatVisitor* cv = dynamic_cast<CatVisitor*>(&v)) {
cv->visit(this);
} else { // Handle error
assert(false);
}
}
};
每个访问者只能访问其设计的类。新的
accept()
函数会使用
dynamic_cast
检查访问者类型是否正确,若正确则接受访问,否则处理错误。具体访问者类需派生自公共的
PetVisitor
基类和特定类的基类,如
CatVisitor
:
// Example 10
class FeedingVisitor : public PetVisitor,
public CatVisitor,
public DogVisitor {
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;
}
};
若访问者不设计用于访问层次结构中的某些类,可省略相应的访问者基类,无需实现虚函数重载:
// Example 10
class BathingVisitor : public PetVisitor,
public DogVisitor { // But no CatVisitor
public:
void visit(Dog* d) override {
std::cout << "Wash the " << d->color()
<< " dog" << std::endl;
}
// No visit(Cat*) here!
};
无环访问者模式的调用方式与常规访问者模式相同:
// Example 10
std::unique_ptr<Pet> c(new Cat("orange"));
std::unique_ptr<Pet> d(new Dog("brown"));
FeedingVisitor fv;
c->accept(fv);
d->accept(fv);
BathingVisitor bv;
//c->accept(bv); // Error
d->accept(bv);
3.6 总结
本文详细介绍了访问者模式在多线程环境中的限制及解决方案,深入探讨了如何访问复杂对象,包括复合对象和二维几何对象的序列化与反序列化。同时,针对传统访问者模式的问题,引入了无环访问者模式并阐述其实现方式。通过这些内容,我们能更好地理解和应用访问者模式,处理复杂的对象访问和操作需求。
3.7 流程图
graph TD;
A[开始] --> B[访问复杂对象];
B --> C{是否为容器类};
C -- 是 --> D[委托访问包含对象];
C -- 否 --> E[按对象结构访问];
D --> F[序列化/反序列化];
E --> F;
F --> G{是否已知对象类型};
G -- 是 --> H[常规序列化/反序列化];
G -- 否 --> I[编码类型标签并使用工厂模式];
H --> J[结束];
I --> J;
3.8 表格
| 模式 | 优点 | 缺点 |
|---|---|---|
| 传统访问者模式 | 分离算法与数据,支持双重分派 | 维护两个平行类层次结构,依赖循环,需处理所有组合 |
| 无环访问者模式 | 打破依赖循环,允许部分访问 |
需使用
dynamic_cast
进行类型检查
|
4. 访问者模式的应用场景总结
4.1 适用场景
- 需要对不同类型对象进行多种操作 :当有一组不同类型的对象,并且需要对这些对象执行多种不同的操作时,使用访问者模式可以将操作的实现与对象本身分离。例如,在上述的宠物示例中,我们可以有喂食、洗澡等不同操作,通过访问者模式可以方便地添加新的操作,而无需修改宠物类的代码。
- 对象结构相对稳定,但操作经常变化 :如果对象的结构(即对象的类型和层次结构)相对固定,而对这些对象的操作经常需要修改或扩展,访问者模式是一个很好的选择。因为可以通过创建新的访问者类来实现新的操作,而不会影响到对象本身的代码。
- 需要对对象进行复杂的处理 :对于一些复杂的对象,如包含多个子对象的复合对象,使用访问者模式可以将处理逻辑委托给访问者,使代码更加清晰和易于维护。例如,在几何对象的序列化和反序列化中,通过访问者模式可以方便地处理不同类型的几何对象及其组合。
4.2 不适用场景
- 对象结构经常变化 :如果对象的结构经常发生变化,每次添加新的对象类型都需要修改所有的访问者类,这会导致代码的维护成本增加,此时访问者模式可能不是最佳选择。
- 操作简单且固定 :如果对对象的操作非常简单,并且不会经常变化,使用访问者模式可能会使代码变得过于复杂,增加不必要的开销。
5. 访问者模式与其他设计模式的比较
5.1 与适配器模式的比较
-
适配器模式
:主要用于将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。在上述的
Shelter类示例中,Shelter类本质上是一个适配器,它将std::vector<std::unique_ptr<Pet>>转换为可访问的对象,使得可以使用访问者模式对宠物对象进行操作。 -
访问者模式
:重点在于将操作的实现与对象本身分离,允许在不修改对象类的情况下添加新的操作。虽然
Shelter类使用了适配器模式来使容器可访问,但核心的操作逻辑是通过访问者模式实现的。
5.2 与工厂模式的比较
-
工厂模式
:主要用于创建对象,将对象的创建过程封装起来,使得代码更加灵活和可维护。在处理未知对象类型的反序列化时,我们使用了工厂模式来根据类型标签创建相应的几何对象。例如,
Visitor类中的make_geometry静态函数就是一个工厂方法,根据传入的类型标签创建不同的几何对象。 - 访问者模式 :侧重于对已创建的对象进行操作,而不是对象的创建。在序列化和反序列化过程中,访问者模式负责对对象进行具体的操作,如将对象转换为字符串或从字符串中恢复对象。
6. 访问者模式的性能考虑
6.1 优点
- 可扩展性 :访问者模式具有良好的可扩展性,当需要添加新的操作时,只需要创建一个新的访问者类,而不需要修改现有的对象类。这使得代码的维护和扩展更加方便。
-
代码复用
:不同的访问者可以共享相同的对象结构,提高了代码的复用性。例如,在宠物示例中,
FeedingVisitor和BathingVisitor可以对相同的宠物对象进行不同的操作。
6.2 缺点
-
类型检查开销
:在无环访问者模式中,需要使用
dynamic_cast进行类型检查,这会带来一定的性能开销。特别是在频繁调用的情况下,可能会影响程序的性能。 - 维护成本 :传统访问者模式需要维护两个平行的类层次结构,并且存在依赖循环,这会增加代码的维护成本。每次添加新的对象类型都需要更新所有的访问者类。
7. 总结与建议
7.1 总结
访问者模式是一种强大的设计模式,它可以将操作的实现与对象本身分离,使得代码更加灵活和可维护。通过本文的介绍,我们了解了访问者模式在多线程环境中的限制及解决方案,掌握了如何访问复杂对象,包括复合对象和二维几何对象的序列化与反序列化。同时,我们还学习了无环访问者模式,它可以打破传统访问者模式的依赖循环,允许部分访问。
7.2 建议
- 选择合适的模式 :在实际应用中,需要根据具体的需求和场景选择合适的设计模式。如果需要对不同类型的对象进行多种操作,并且对象结构相对稳定,访问者模式是一个不错的选择。但如果对象结构经常变化,可能需要考虑其他设计模式。
-
注意性能问题
:在使用无环访问者模式时,要注意
dynamic_cast带来的性能开销。可以根据实际情况进行优化,例如减少不必要的类型检查。 - 结合其他模式 :在处理复杂问题时,访问者模式可以与其他设计模式结合使用,如适配器模式和工厂模式,以提高代码的灵活性和可维护性。
8. 流程图
graph TD;
A[需求分析] --> B{是否适用访问者模式};
B -- 是 --> C[设计对象层次结构];
B -- 否 --> D[选择其他模式];
C --> E[创建访问者基类];
E --> F[创建具体访问者类];
F --> G[对象接受访问者];
G --> H[执行操作];
D --> I[结束];
H --> I;
9. 表格
| 模式特点 | 传统访问者模式 | 无环访问者模式 |
|---|---|---|
| 类层次结构 | 两个平行类层次结构,依赖循环 | 打破依赖循环 |
| 操作扩展性 | 需修改所有访问者类 | 可部分访问,无需修改无关访问者类 |
| 类型检查 | 无 |
使用
dynamic_cast
|
| 代码维护成本 | 高 | 相对较低 |
超级会员免费看
962

被折叠的 条评论
为什么被折叠?



