12.5.1 例题1:访问者模式
本节一开始先说明下午面向对象程序设计题的基本规则:它是 Java/C++ 二选一的代码填空题,不是现场完整程序设计题。真正要训练的是“在已有代码框架中恢复缺失代码”的能力。访问者模式只是本题的业务外壳,填空时还要同时掌握类、抽象方法、接口、对象引用等语法细节。
先把考试规则听明白
| 规则 | 课堂强调 | 对备考的影响 |
|---|---|---|
| 选答题 | 软件设计师下午题中少见的选答形式 | Java 和 C++ 只选一个作答,答题卡要涂题号 |
| 代码语言 | 常见为 Java/C++ 两套代码 | 哪种熟练就选哪种;零基础更建议从 Java 入手 |
| 得分目标 | 15 分题不必追求满分 | 先稳住语法空和模式基础空,目标 6-9 分 |
| 填空位置 | 类声明、方法声明、方法体、主函数都可能缺 | 先判断空所在位置,再决定该填声明还是语句 |
C++ 语法底座
访问者例题前,课程先补了 C++ 常考语法。原因很直接:很多空不是设计模式难,而是语法细节错。
| 语法点 | 标准理解 | 代码填空提醒 |
|---|---|---|
| 访问控制 | public 对外可访问,private 只在类内部访问,protected 可被继承相关代码访问 | 属性常设为私有,对外暴露公有方法,体现封装 |
| 纯虚函数 | 只有声明,没有函数体,用 = 0 表示 | virtual 返回类型 函数名(参数) = 0;,参数列表不能凭感觉省略 |
| 继承写法 | class Child : public Parent | 冒号后写继承方式和父类名 |
| 类外定义 | ReturnType ClassName::method(...) | :: 是作用域分辨符,前面是类名 |
| 对象/引用/指针访问 | 对象名和引用用 .,指针用 -> | 看到 ClassName *p 才是指针,调用成员用箭头 |
cpp
class Visitor {
public:
virtual void visitBook(Book *book) = 0;
virtual void visitArticle(Article *article) = 0;
};
void Book::accept(Visitor *visitor) {
visitor->visitBook(this);
}这段代码里有两个高频空:接口中的纯虚函数声明,以及 accept 方法内部把当前对象 this 交给访问者。
访问者模式解决的问题
访问者模式适合“对象结构比较稳定,但对这些对象的操作经常变化”的场景。以课程中的书籍、论文、打印/访问统计为例,Book、Article 这些元素类型不常变,但你可能不断新增“打印信息”“统计页数”“导出引用格式”等操作。如果把这些操作都写进元素类,每加一种操作就要改一批元素类,结构会越来越乱。
访问者模式把操作挪到 Visitor 中:
mermaid
flowchart LR
Client["客户端/对象结构"] --> E1["Book.accept(visitor)"]
Client --> E2["Article.accept(visitor)"]
E1 --> V1["visitor.visitBook(this)"]
E2 --> V2["visitor.visitArticle(this)"]
V1 --> Op["具体访问操作"]
V2 --> Op角色与代码线索
| 角色 | 课程中的直觉 | 填空时怎么看 |
|---|---|---|
| Visitor 接口 | 规定访问不同元素的方法 | 看它是否有 visit(Book)、visit(Article)、print() 等方法 |
| ConcreteVisitor | 实现具体访问操作 | 方法体通常根据不同元素输出或统计 |
| Element 抽象 | 规定“接受访问” | 常见空是 accept(Visitor v) 的声明 |
| ConcreteElement | 具体元素,如书籍、论文 | accept 内部调用访问者方法,并传入 this |
| ObjectStructure | 保存一组元素 | 遍历元素,逐个调用 accept |
双分派不是玄学
访问者的核心是“先由元素接收访问者,再由访问者根据元素类型执行对应操作”。代码层面只有两步:
- 客户端调用元素的
accept(visitor)。 - 元素在
accept内部调用visitor.visitX(this)。
为什么要传 this?因为访问者要知道当前访问的是哪一个具体元素。课程里提到 Article 的 accept 方法中,参数是访问者接口对象,访问者接口中有访问论文、访问书籍、打印等方法;当当前类是 Article 时,就应该调用访问论文的方法,并把本类对象传进去。
java
interface Visitor {
void visit(Article article);
void visit(Book book);
void print();
}
class Article {
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
class Book {
public void accept(Visitor visitor) {
visitor.visit(this);
}
}代码填空推导方法
| 空的位置 | 推导路径 | 典型答案形态 |
|---|---|---|
| 接口/抽象类方法声明 | 看子类实现了哪些同名方法 | public abstract void accept(Visitor v); 或 C++ 纯虚函数 |
具体元素 accept 方法体 | 看方法参数是谁、Visitor 能调用哪些 visit | visitor.visit(this); |
| 访问者接口 | 看具体访问者实现了哪些方法 | void visit(Book book); |
| 对象结构遍历 | 看集合元素类型和元素暴露的方法 | element.accept(visitor); |
易错点拆解
| 易错点 | 为什么错 | 正确处理 |
|---|---|---|
把 visit 和 accept 方向写反 | 访问者模式不是访问者去找元素,而是元素把自己交给访问者 | element.accept(visitor),内部 visitor.visit(this) |
忘记 this | 访问者方法需要具体元素对象作为参数 | 当前类是 Article 就传当前 Article 对象 |
| 抽象方法漏关键字 | Java 抽象类中的抽象方法必须标注 abstract | 类是 abstract class,方法无方法体时也要 abstract |
| C++ 漏参数列表 | 纯虚函数的参数列表是签名的一部分 | 从上下文照抄完整参数 |
模式优劣与演进意义
| 维度 | 优点 | 代价 |
|---|---|---|
| 新增操作 | 增加一个 ConcreteVisitor 即可,不动元素类 | 操作集中后,访问者类可能变多 |
| 新增元素 | 元素结构稳定时很舒服 | 新增元素要改 Visitor 接口及所有 ConcreteVisitor |
| 封装性 | 操作逻辑从元素类中分离出来 | 访问者往往需要了解元素内部信息 |
它不是“替代所有面向对象方法”的新技术,而是在特定变化方向下的取舍:当“操作变化快、元素类型变化慢”时,它比把所有操作堆进元素类更容易维护;当元素类型经常新增时,它反而不合适。
例题
具体元素类的 `accept` 方法中通常调用:
自查清单
- 能否解释为什么访问者适合“结构稳定、操作变化”?
- 能否从子类实现反推抽象类/接口中的方法声明?
- 能否区分
accept(visitor)与visitor.visit(this)的调用方向? - C++ 纯虚函数是否能写完整:
virtual 返回类型 函数名(参数) = 0;?