访问者模式有哪些角色?调用流程是怎样的?
简化版
访问者模式主要有 Visitor、ConcreteVisitor、Element、ConcreteElement、ObjectStructure 五类角色。调用流程是对象结构遍历元素,元素调用 accept(visitor),再由元素把自己传给访问者的 visitXxx(this) 方法,最终执行具体操作。
详细版
角色说明:
- Visitor:声明访问不同元素的方法。
- ConcreteVisitor:实现具体操作,如统计、导出、校验。
- Element:声明
accept(Visitor visitor)。 - ConcreteElement:实现
accept,并把自身传给 visitor。 - ObjectStructure:保存元素集合并负责遍历。
流程:
- 客户端创建具体访问者。
- 对象结构遍历内部元素。
- 每个元素执行
accept(visitor)。 - 元素内部调用
visitor.visitXxx(this)。 - 访问者根据元素真实类型执行对应逻辑。
这个流程的关键是 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 步复核:
- 先说明“Visitor”的核心机制:声明每种元素的 visit 重载;再交代边界:定义操作覆盖面。
- 接着分析“Element”:声明 accept(Visitor);不能遗漏对应代价或结果:提供分派入口。
- 最后用“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)。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。