← 返回题目列表

什么是访问者模式?它解决了什么问题?

高频 中等 第 8 / 25 题 更新于 2026/07/28
访问者模式行为型模式操作扩展对象结构

简化版

访问者模式把作用在一组对象结构上的操作封装到访问者对象中,让对象结构相对稳定时,可以在不修改元素类的情况下新增操作。它适合元素类型稳定、操作经常变化的场景。

详细版

访问者模式的核心是把“数据结构”和“作用在数据结构上的操作”分离。

典型角色:

  • 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. 先说明“新增一种操作”的核心机制:增加 1 个 ConcreteVisitor;再交代边界:元素类通常不变。
  2. 接着分析“新增一种元素”:Visitor 接口增加 visit 方法;不能遗漏对应代价或结果:全部访问者同步修改。
  3. 最后用“访问元素数据”检查方案:通过稳定的公开接口;验收时确认避免为访问者暴露可写状态。

这三项构成完整判断链:先讲清新增一种操作,再说明新增一种元素,最后用访问元素数据检验实现是否越界。

面试中若能给出违反“避免为访问者暴露可写状态”的反例,再说明修正办法,答案就从模式定义落到了可验证的工程决策。

七、常见误区与追问

  • 误区:只看到“新增一种操作”就认为方案成立。 必须同时说明核心机制“增加 1 个 ConcreteVisitor”和工程边界“元素类通常不变”。
  • 误区:把“新增一种元素”当成无条件结论。 只有在“Visitor 接口增加 visit 方法”成立时,才能据此讨论“全部访问者同步修改”。
  • 追问:访问者是否消除了条件分支? 它把按元素类型分派的规则放进 accept/visit 协作中,但具体操作内部仍可能有业务分支。
  • 追问:为什么元素必须提供 accept? 它让元素把自身具体类型传给访问者,形成第二次分派。
  • 追问:新增访问者为何通常容易? 元素接口不变,只需实现一组既定 visit 方法并在调用处使用新访问者。
  • 追问:新增元素为何昂贵? Visitor 契约必须出现新重载,所有现有 ConcreteVisitor 都要响应它。
  • 追问:它会天然破坏封装吗? 不会天然破坏,但访问者需要的数据应通过只读且有业务含义的接口提供。

八、加强记忆

记忆时抓住这条主线:访问者模式分离对象结构和操作;元素稳定、操作多变时适合使用;新增访问者容易,新增元素类型困难;它扩展的是操作维度,不是元素类型维度。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。