Skip to content
难度基础(★)
建议时长45分钟

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 交给访问者。

访问者模式解决的问题

访问者模式适合“对象结构比较稳定,但对这些对象的操作经常变化”的场景。以课程中的书籍、论文、打印/访问统计为例,BookArticle 这些元素类型不常变,但你可能不断新增“打印信息”“统计页数”“导出引用格式”等操作。如果把这些操作都写进元素类,每加一种操作就要改一批元素类,结构会越来越乱。

访问者模式把操作挪到 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

双分派不是玄学

访问者的核心是“先由元素接收访问者,再由访问者根据元素类型执行对应操作”。代码层面只有两步:

  1. 客户端调用元素的 accept(visitor)
  2. 元素在 accept 内部调用 visitor.visitX(this)

为什么要传 this?因为访问者要知道当前访问的是哪一个具体元素。课程里提到 Articleaccept 方法中,参数是访问者接口对象,访问者接口中有访问论文、访问书籍、打印等方法;当当前类是 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 能调用哪些 visitvisitor.visit(this);
访问者接口看具体访问者实现了哪些方法void visit(Book book);
对象结构遍历看集合元素类型和元素暴露的方法element.accept(visitor);

易错点拆解

易错点为什么错正确处理
visitaccept 方向写反访问者模式不是访问者去找元素,而是元素把自己交给访问者element.accept(visitor),内部 visitor.visit(this)
忘记 this访问者方法需要具体元素对象作为参数当前类是 Article 就传当前 Article 对象
抽象方法漏关键字Java 抽象类中的抽象方法必须标注 abstract类是 abstract class,方法无方法体时也要 abstract
C++ 漏参数列表纯虚函数的参数列表是签名的一部分从上下文照抄完整参数

模式优劣与演进意义

维度优点代价
新增操作增加一个 ConcreteVisitor 即可,不动元素类操作集中后,访问者类可能变多
新增元素元素结构稳定时很舒服新增元素要改 Visitor 接口及所有 ConcreteVisitor
封装性操作逻辑从元素类中分离出来访问者往往需要了解元素内部信息

它不是“替代所有面向对象方法”的新技术,而是在特定变化方向下的取舍:当“操作变化快、元素类型变化慢”时,它比把所有操作堆进元素类更容易维护;当元素类型经常新增时,它反而不合适。

例题

单选
具体元素类的 `accept` 方法中通常调用:

自查清单

  1. 能否解释为什么访问者适合“结构稳定、操作变化”?
  2. 能否从子类实现反推抽象类/接口中的方法声明?
  3. 能否区分 accept(visitor)visitor.visit(this) 的调用方向?
  4. C++ 纯虚函数是否能写完整:virtual 返回类型 函数名(参数) = 0;