← 返回题目列表

访问者模式有哪些角色?调用流程是怎样的?

高频 中等 第 4 / 25 题 更新于 2026/07/28
访问者模式角色调用流程accept

简化版

访问者模式主要有 Visitor、ConcreteVisitor、Element、ConcreteElement、ObjectStructure 五类角色。调用流程是对象结构遍历元素,元素调用 accept(visitor),再由元素把自己传给访问者的 visitXxx(this) 方法,最终执行具体操作。

详细版

角色说明:

  • Visitor:声明访问不同元素的方法。
  • ConcreteVisitor:实现具体操作,如统计、导出、校验。
  • Element:声明 accept(Visitor visitor)
  • ConcreteElement:实现 accept,并把自身传给 visitor。
  • ObjectStructure:保存元素集合并负责遍历。

流程:

  1. 客户端创建具体访问者。
  2. 对象结构遍历内部元素。
  3. 每个元素执行 accept(visitor)
  4. 元素内部调用 visitor.visitXxx(this)
  5. 访问者根据元素真实类型执行对应逻辑。

这个流程的关键是 accept。它让元素把自己的真实类型“交回”给访问者,从而实现针对不同元素类型的不同处理。

完整版教学

一、为什么角色看起来比普通模式多

访问者模式要解决的是“同一批元素上有很多操作”的问题,所以它天然涉及两个方向:元素层级和操作层级。

元素层级负责表示业务对象,例如文件、目录、图片、文本、员工、部门。操作层级负责表示动作,例如压缩、导出、统计、校验、渲染。

角色多,是因为它把这两个变化方向拆开了。

二、accept 的作用

accept 是访问者模式最核心的方法。它不是普通回调,而是连接元素和访问者的桥。

class PdfFile implements ResourceFile {
    public void accept(Visitor visitor) {
        visitor.visitPdfFile(this);
    }
}

客户端只知道元素是 ResourceFile,但元素自己知道真实类型是 PdfFile。通过 accept,它把这个真实类型传递给访问者,于是访问者可以执行 PDF 专属逻辑。

三、ObjectStructure 的作用

ObjectStructure 不是必须非常复杂,它可以只是一个集合,也可以是一棵树、一个组合对象、一个文件系统结构。

它的职责是管理元素并触发访问:

for (Element element : elements) {
    element.accept(visitor);
}

访问者不负责遍历结构,元素也不负责保存集合。职责分开后,操作扩展更清晰。

四、常见误区与工程判断

有些同学会把访问者模式写成访问者直接判断类型:

if (element instanceof PdfFile) { ... }

这样虽然也能运行,但破坏了访问者模式的关键思想:利用多态分发,而不是到处写类型判断。

工程中如果 ObjectStructure 很复杂,例如树形结构,可以结合组合模式一起使用:组合模式负责结构遍历,访问者模式负责节点操作。这是非常常见的组合用法。

五、把调用链讲成一条清楚的路径

访问者模式容易让人觉得绕,是因为调用方向不是“visitor 主动判断对象类型”,而是“元素接收 visitor,再把自己的真实类型交回去”。面试时可以按调用链讲:对象结构遍历元素,调用 element.accept(visitor);具体元素在 accept 里调用 visitor.visitXxx(this);具体访问者执行对应元素的处理逻辑。

这条路径里每个角色都有明确职责。ObjectStructure 负责“有哪些元素、按什么顺序遍历”;Element 负责“允许外部访问者进入”;ConcreteElement 负责“把自身类型交给访问者”;Visitor 负责“声明可以处理哪些元素”;ConcreteVisitor 负责“实现某一种操作”。这样拆开后,访问者模式就不是一堆类名,而是一条职责链。

面试官如果追问“为什么 Element 不直接调用统一的 visit(Element)”,可以指出:那样访问者拿到的只是抽象类型,仍然要通过 instanceof 或类型判断分发;访问者模式通过 visitEmployee(Employee)visitDepartment(Department) 这类重载方法,把不同类型的处理入口显式化,减少手写类型判断。

六、用工程约束检验答案

一次完整调用至少涉及 4 个角色:对象结构选择元素,元素执行 accept,具体元素回调 visit,具体访问者完成操作。若集合中有 100 个元素,遍历发生 100 次,每个元素都走这条调用链,而不是 Visitor 自己偷偷遍历集合。

检查项核心判断工程含义
Visitor声明每种元素的 visit 重载定义操作覆盖面
Element声明 accept(Visitor)提供分派入口
ObjectStructure保存或枚举元素负责遍历而非业务操作

把关键关系压缩成一条可复述的路径:

client -> objectStructure.foreach
       -> employee.accept(v)
       -> v.visitEmployee(employee)
       -> visitor 执行业务

面试中把调用顺序讲清楚比背角色名更重要,尤其不要颠倒 accept 与 visit 的方向。

落地前可以再按下面 3 步复核:

  1. 先说明“Visitor”的核心机制:声明每种元素的 visit 重载;再交代边界:定义操作覆盖面。
  2. 接着分析“Element”:声明 accept(Visitor);不能遗漏对应代价或结果:提供分派入口。
  3. 最后用“ObjectStructure”检查方案:保存或枚举元素;验收时确认负责遍历而非业务操作。

这三项构成完整判断链:先讲清Visitor,再说明Element,最后用ObjectStructure检验实现是否越界。

面试中若能给出违反“负责遍历而非业务操作”的反例,再说明修正办法,答案就从模式定义落到了可验证的工程决策。

七、常见误区与追问

  • 误区:只看到“Visitor”就认为方案成立。 必须同时说明核心机制“声明每种元素的 visit 重载”和工程边界“定义操作覆盖面”。
  • 误区:把“Element”当成无条件结论。 只有在“声明 accept(Visitor)”成立时,才能据此讨论“提供分派入口”。
  • 追问:谁发起第一次调用? 客户端或 ObjectStructure 对元素调用 accept。
  • 追问:谁决定调用哪个 visit 重载? ConcreteElement 的 accept 方法以 this 的静态具体类型选择对应重载。
  • 追问:ObjectStructure 是必需接口吗? 它是典型角色但不一定单独建类,现有集合或树也可承担遍历。
  • 追问:Visitor 能否保存累计结果? 可以,例如统计访问者持有计数器,但要明确实例复用和线程安全。
  • 追问:元素能否拒绝某个访问者? 技术上能做权限判断,但会增加耦合,通常应在访问者或调用边界控制。

八、加强记忆

记忆时抓住这条主线:Element 提供 accept,Visitor 提供 visit;ConcreteElement 在 accept 中把自己传给 Visitor;ObjectStructure 负责保存和遍历元素;访问者模式的流程核心是 element.accept(visitor)。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。