一、访问者模式核心概念
访问者模式(Visitor Pattern)是一种行为型设计模式,它的核心思想是:将数据结构与对数据的操作分离。简单来说,就是当你有一组固定的对象结构,但需要频繁新增不同的操作逻辑时,不需要修改这些对象本身,而是把操作逻辑封装成 "访问者",让对象接受访问者的 "访问" 并执行对应的操作。
可以用一个比喻理解:博物馆里的展品(固定的数据结构)不会变,但不同的参观者(访问者)会对展品做不同的操作(游客只是看、研究员记录信息、保洁员清洁),展品只需要提供一个 "接受访问" 的接口,具体操作由参观者决定。
核心角色
- 抽象元素(Element) :定义一个接受访问者的接口(通常是
accept(Visitor&)方法)。 - 具体元素(ConcreteElement):实现抽象元素的接口,调用访问者的对应方法。
- 抽象访问者(Visitor) :为每个具体元素定义一个访问方法(
visit(ConcreteElement&))。 - 具体访问者(ConcreteVisitor):实现抽象访问者的方法,定义对具体元素的具体操作。
- 对象结构(ObjectStructure):管理元素的集合,提供遍历元素的接口,供访问者遍历访问。
二、C++ 代码示例
我们以 "动物园" 为场景:动物园里有老虎、猴子两种固定的动物(数据结构),需要新增 "喂食""体检" 两种操作(访问者),用访问者模式实现这个需求。
cpp
#include <iostream>
#include <vector>
#include <memory>
// 前置声明:抽象元素需要知道抽象访问者,反之亦然
class Visitor;
// ---------------------- 1. 抽象元素(Element) ----------------------
class Animal {
public:
virtual ~Animal() = default;
// 核心接口:接受访问者的访问
virtual void accept(Visitor& visitor) = 0;
};
// ---------------------- 2. 具体元素(ConcreteElement) ----------------------
// 老虎
class Tiger : public Animal {
public:
// 接受访问者,调用访问者针对老虎的方法
void accept(Visitor& visitor) override {
visitor.visit(*this); // 把自身传递给访问者
}
// 老虎的特有属性/方法
void roar() const {
std::cout << "老虎:嗷呜~" << std::endl;
}
std::string getName() const { return "老虎"; }
};
// 猴子
class Monkey : public Animal {
public:
void accept(Visitor& visitor) override {
visitor.visit(*this);
}
// 猴子的特有属性/方法
void jump() const {
std::cout << "猴子:蹦蹦跳跳~" << std::endl;
}
std::string getName() const { return "猴子"; }
};
// ---------------------- 3. 抽象访问者(Visitor) ----------------------
class Visitor {
public:
virtual ~Visitor() = default;
// 为每个具体元素定义访问方法
virtual void visit(Tiger& tiger) = 0;
virtual void visit(Monkey& monkey) = 0;
};
// ---------------------- 4. 具体访问者(ConcreteVisitor) ----------------------
// 喂食访问者
class FeedVisitor : public Visitor {
public:
void visit(Tiger& tiger) override {
std::cout << "给" << tiger.getName() << "喂肉" << std::endl;
tiger.roar(); // 可以调用元素的特有方法
}
void visit(Monkey& monkey) override {
std::cout << "给" << monkey.getName() << "喂香蕉" << std::endl;
monkey.jump();
}
};
// 体检访问者
class CheckVisitor : public Visitor {
public:
void visit(Tiger& tiger) override {
std::cout << "给" << tiger.getName() << "检查牙齿和爪子" << std::endl;
}
void visit(Monkey& monkey) override {
std::cout << "给" << monkey.getName() << "检查尾巴和视力" << std::endl;
}
};
// ---------------------- 5. 对象结构(ObjectStructure) ----------------------
class Zoo {
private:
std::vector<std::unique_ptr<Animal>> animals; // 管理元素集合
public:
// 添加动物
void addAnimal(std::unique_ptr<Animal> animal) {
animals.push_back(std::move(animal));
}
// 让访问者遍历所有动物并执行操作
void accept(Visitor& visitor) {
for (auto& animal : animals) {
animal->accept(visitor);
}
}
};
// ---------------------- 测试代码 ----------------------
int main() {
// 1. 创建动物园并添加动物
Zoo zoo;
zoo.addAnimal(std::make_unique<Tiger>());
zoo.addAnimal(std::make_unique<Monkey>());
// 2. 喂食操作
std::cout << "===== 执行喂食操作 =====" << std::endl;
FeedVisitor feedVisitor;
zoo.accept(feedVisitor);
// 3. 体检操作
std::cout << "\n===== 执行体检操作 =====" << std::endl;
CheckVisitor checkVisitor;
zoo.accept(checkVisitor);
return 0;
}
代码运行结果
cpp
===== 执行喂食操作 =====
给老虎喂肉
老虎:嗷呜~
给猴子喂香蕉
猴子:蹦蹦跳跳~
===== 执行体检操作 =====
给老虎检查牙齿和爪子
给猴子检查尾巴和视力
代码关键解释
-
核心接口
accept:- 动物类(Element)的
accept方法接收一个访问者对象,并调用访问者的visit方法,同时把自身作为参数传递。 - 这一步是 "双重分派":先根据动物类型(Tiger/Monkey)调用对应的
accept,再根据访问者类型(Feed/Check)调用对应的visit,最终确定执行哪个操作。
- 动物类(Element)的
-
新增操作的扩展 :如果需要新增 "打扫笼子" 操作,只需要新增一个
CleanVisitor类实现Visitor接口,无需修改任何动物类和现有访问者类,符合 "开闭原则"。 -
对象结构
Zoo:封装了元素的集合管理和遍历逻辑,让客户端只需调用zoo.accept(visitor)就能批量执行操作,简化了客户端代码。
三、访问者模式的适用场景
- 数据结构稳定(如示例中的动物类型固定),但需要频繁新增不同的操作逻辑。
- 需要对一组对象进行多种不相关的操作,且不想让这些操作污染对象本身。
- 需要集中管理一组对象的某类操作(如所有动物的体检逻辑都在
CheckVisitor中,便于维护)。
四、访问者模式的优缺点
表格
| 优点 | 缺点 |
|---|---|
| 操作与数据结构分离,新增操作只需加访问者,符合开闭原则 | 新增元素类型时,需要修改所有访问者的接口,违反开闭原则 |
| 集中管理同类操作,代码可读性、维护性更高 | 访问者可能需要访问元素的私有属性,破坏封装性(可通过友元 / 提供接口解决) |
| 可以在访问者中积累状态(如统计所有动物的喂食总量) | 双重分派逻辑增加了代码复杂度,新手不易理解 |
总结
- 访问者模式的核心是分离数据结构和操作,通过 "接受 - 访问" 的双重分派机制实现对不同元素的差异化操作。
- C++ 实现的关键是:抽象元素定义
accept接口,抽象访问者为每个具体元素定义visit接口,具体元素调用访问者的对应visit方法。 - 适用场景是 "数据结构固定、操作频繁变化",缺点是新增元素类型时成本高,需权衡使用。