什么是访问者模式?它解决了什么问题?
简化版
访问者模式把作用在一组对象结构上的操作封装到访问者对象中,让对象结构相对稳定时,可以在不修改元素类的情况下新增操作。它适合元素类型稳定、操作经常变化的场景。
详细版
访问者模式的核心是把“数据结构”和“作用在数据结构上的操作”分离。
典型角色:
- Visitor:访问者接口,定义访问不同元素的方法。
- ConcreteVisitor:具体访问者,实现某一类操作。
- Element:元素接口,提供
accept(visitor)。 - ConcreteElement:具体元素,在
accept中回调 visitor。 - ObjectStructure:对象结构,保存并遍历元素集合。
它解决的问题是:当对象结构中元素类型比较固定,但对这些元素的操作不断增加时,不想频繁修改元素类。新增一个报表、导出、校验、统计逻辑时,只需要新增访问者。
缺点也很明显:如果元素类型经常变化,每新增一种元素,就要修改所有访问者,维护成本会变高。
完整版教学
一、为什么会需要访问者模式
假设有一套固定的对象结构:员工、部门、项目。系统一开始只需要计算薪资,后来又要导出报表、做权限审计、做绩效统计、生成组织图。如果把这些操作都写进每个元素类,元素类会越来越臃肿。
访问者模式的思路是:元素类只保留自己的核心数据和 accept 入口,把外部操作交给访问者。这样新增一种操作时,不必修改员工、部门、项目这些元素类,而是新增一个访问者类。
这就是“对象结构稳定、操作扩展频繁”时的典型解法。
二、它的核心结构
元素接口通常长这样:
interface Element {
void accept(Visitor visitor);
}
访问者接口通常为每种元素定义一个访问方法:
interface Visitor {
void visitEmployee(Employee employee);
void visitDepartment(Department department);
}
具体元素在 accept 中调用访问者:
class Employee implements Element {
public void accept(Visitor visitor) {
visitor.visitEmployee(this);
}
}
这一步看似绕,其实是访问者模式的关键:元素把自己的真实类型交给访问者,让访问者能执行针对该类型的操作。
三、访问者模式真正换来的是什么
它换来的是操作维度的扩展能力。新增“导出 Excel 访问者”“权限检查访问者”“薪资统计访问者”时,不需要改元素类。
但它牺牲的是元素类型维度的扩展能力。新增一个 Contractor 元素时,访问者接口要增加 visitContractor,所有访问者实现类都要跟着改。
所以访问者模式不是通用解耦神器,它只适合某一类变化方向非常明确的系统。
四、常见误区与工程判断
面试中最常见的误区是只说“访问者可以访问对象”,这太表面。更准确的表达是:访问者模式把对象结构中的操作抽离出来,适合操作多变、元素稳定的场景。
工程中可以用一个判断标准:如果你经常新增“对同一批对象的不同处理逻辑”,访问者模式值得考虑;如果你经常新增对象类型,就不要轻易用访问者模式。
另一个判断点是封装性。访问者通常需要读取元素内部数据,如果为了访问者暴露太多 getter,元素封装可能被削弱。这也是它的典型代价。
五、面试时要把“变化方向”讲出来
这道题如果只回答定义,分数通常不会太高。访问者模式真正考的是你能不能识别系统里有两条变化轴:一条是“元素类型会不会变”,另一条是“针对元素的操作会不会变”。访问者模式选择保护的是元素类,让元素类不要被各种统计、导出、校验、审计逻辑污染;它放弃的是新增元素类型的便利性。
面试官追问“为什么不用在元素类里直接加方法”时,可以从职责角度回答:员工、部门、项目这些类应该表达业务对象自身,不应该随着报表、风控、导出格式的增加而不断塞新方法。访问者把这些横向操作集中到访问者类里,每个访问者代表一种完整业务处理流程,代码阅读时也更容易看到“这次遍历到底要做什么”。
如果追问“访问者是否违反开闭原则”,要回答得更细:它对“新增操作”开放、对“新增元素类型”不友好。设计模式不是绝对符合所有变化方向的开闭原则,而是先判断哪类变化更频繁,再围绕这类变化做结构取舍。
六、用工程约束检验答案
以员工、部门、项目 3 种稳定元素为例,系统若先后增加薪资、审计、导出 3 种操作,访问者让每种操作集中在一个类中。反过来新增第 4 种元素时,3 个既有访问者都必须补方法,这个数字对比正好说明它优化的是“操作轴”,不是“类型轴”。
| 检查项 | 核心判断 | 工程含义 |
|---|---|---|
| 新增一种操作 | 增加 1 个 ConcreteVisitor | 元素类通常不变 |
| 新增一种元素 | Visitor 接口增加 visit 方法 | 全部访问者同步修改 |
| 访问元素数据 | 通过稳定的公开接口 | 避免为访问者暴露可写状态 |
把关键关系压缩成一条可复述的路径:
ObjectStructure
-> element.accept(visitor)
-> visitor.visitConcreteElement(element)
-> 完成一种横向操作
判断是否采用访问者,先统计未来更常变化的是元素种类还是操作种类;模式选择必须服从变化方向。
落地前可以再按下面 3 步复核:
- 先说明“新增一种操作”的核心机制:增加 1 个 ConcreteVisitor;再交代边界:元素类通常不变。
- 接着分析“新增一种元素”:Visitor 接口增加 visit 方法;不能遗漏对应代价或结果:全部访问者同步修改。
- 最后用“访问元素数据”检查方案:通过稳定的公开接口;验收时确认避免为访问者暴露可写状态。
这三项构成完整判断链:先讲清新增一种操作,再说明新增一种元素,最后用访问元素数据检验实现是否越界。
面试中若能给出违反“避免为访问者暴露可写状态”的反例,再说明修正办法,答案就从模式定义落到了可验证的工程决策。
七、常见误区与追问
- 误区:只看到“新增一种操作”就认为方案成立。 必须同时说明核心机制“增加 1 个 ConcreteVisitor”和工程边界“元素类通常不变”。
- 误区:把“新增一种元素”当成无条件结论。 只有在“Visitor 接口增加 visit 方法”成立时,才能据此讨论“全部访问者同步修改”。
- 追问:访问者是否消除了条件分支? 它把按元素类型分派的规则放进 accept/visit 协作中,但具体操作内部仍可能有业务分支。
- 追问:为什么元素必须提供 accept? 它让元素把自身具体类型传给访问者,形成第二次分派。
- 追问:新增访问者为何通常容易? 元素接口不变,只需实现一组既定 visit 方法并在调用处使用新访问者。
- 追问:新增元素为何昂贵? Visitor 契约必须出现新重载,所有现有 ConcreteVisitor 都要响应它。
- 追问:它会天然破坏封装吗? 不会天然破坏,但访问者需要的数据应通过只读且有业务含义的接口提供。
八、加强记忆
记忆时抓住这条主线:访问者模式分离对象结构和操作;元素稳定、操作多变时适合使用;新增访问者容易,新增元素类型困难;它扩展的是操作维度,不是元素类型维度。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。