☰
访问者模式详解:分离数据和行为,不改原有类就能加新操作
2026/10/8 6:49:36 网站建设 项目流程

一、什么是访问者模式

定义(GOF):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下,定义作用于这些元素的新操作。

一句话理解:数据结构(元素)保持稳定,但"对这些数据要做什么"却经常变化。与其每次改数据结构,不如把"操作"抽象成一个Visitor接口,新操作 = 新 Visitor 实现类,原类一行都不用动。


二、为什么需要访问者模式(动机)

假设我们有一个公园系统,ParkElement(公园的各部分)已经定义好,并有ParkA、ParkB、Park等具体元素。

某天需求来了:要新增"计算绿化覆盖率"“安全检查”"游客容量评估"三个功能。

  • 不用访问者:只能改ParkElement接口,加 3 个方法,再改ParkA/ParkB/Park全部实现类。类一多,改动面爆炸,还违反了"对修改关闭"。
  • 用访问者:把"操作"抽成Visitor接口,每个新功能就是一个新的Visitor实现类(如GreenVisitor、SafetyVisitor)。原有的元素类完全不动,只新增类 → 符合开闭原则(对扩展开放、对修改关闭)。

访问者模式最适合的场景就是:元素类型基本稳定,但作用在元素上的操作频繁变化。


三、访问者模式的结构

角色和职责

角色职责
Visitor(抽象访问者)为对象结构中每一种具体元素声明一个visit访问操作。方法名/参数决定了能访问哪类元素
ConcreteVisitor(具体访问者)实现 Visitor 声明的每一个visit操作,代表一种具体算法/行为
Element(抽象元素)定义一个accept(Visitor)方法,以访问者为参数,把自己"交给"访问者
ConcreteElement(具体元素)实现accept方法,通常内部写visitor.visit(this)把自身回传
ObjectStructure(对象结构)容纳多个元素(如组合/集合),提供接口让访问者遍历访问所有元素

ObjectStructure 角色是使用访问者模式必备的——它负责枚举元素并驱动遍历。

类结构图(mermaid)

visit(this)

visit(this)

visit(this)

«interface»

Visitor

+visit(Park park)

+visit(ParkA parkA)

+visit(ParkB parkB)

«interface»

Element

+accept(Visitor visitor)

«interface»

ParkElement

ParkA

+accept(Visitor)

ParkB

+accept(Visitor)

Park

-ParkA parkA

-ParkB parkB

+accept(Visitor)

VisitorA

VisitorB

VisitorManager


四、代码举例

// ========== 1. 抽象元素:公园的每一部分 ==========publicinterfaceParkElement{// 用来接纳访问者publicvoidaccept(Visitorvisitor);}// ========== 2. 具体元素 A 部分 ==========publicclassParkAimplementsParkElement{privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}@Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);// 把"自己"回传给访问者,触发双分派}}// ========== 2. 具体元素 B 部分 ==========publicclassParkBimplementsParkElement{privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}@Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);}}// ========== 2. 具体元素:整个公园(也是对象结构角色) ==========publicclassParkimplementsParkElement{privateParkAparkA;privateParkBparkB;publicPark(){this.parkA=newParkA();this.parkB=newParkB();}@Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);// 访问者先访问整体parkA.accept(visitor);// 再深入遍历各组成部分parkB.accept(visitor);}}// ========== 3. 抽象访问者 ==========publicinterfaceVisitor{publicvoidvisit(Parkpark);publicvoidvisit(ParkAparkA);publicvoidvisit(ParkBparkB);}// ========== 4. 具体访问者 A:只负责 ParkA 的卫生 ==========publicclassVisitorAimplementsVisitor{@Overridepublicvoidvisit(Parkpark){/* 不处理整体 */}@Overridepublicvoidvisit(ParkAparkA){System.out.println("清洁工A:完成公园A的卫生");}@Overridepublicvoidvisit(ParkBparkB){/* 不处理 B */}}// ========== 4. 具体访问者 B:只负责 ParkB 的卫生 ==========publicclassVisitorBimplementsVisitor{@Overridepublicvoidvisit(Parkpark){/* 不处理整体 */}@Overridepublicvoidvisit(ParkAparkA){/* 不处理 A */}@Overridepublicvoidvisit(ParkBparkB){System.out.println("清洁工B:完成公园B的卫生");}}// ========== 4. 具体访问者:管理员,做全面检查 ==========publicclassVisitorManagerimplementsVisitor{@Overridepublicvoidvisit(Parkpark){System.out.println("管理员:负责公园卫生检查");}@Overridepublicvoidvisit(ParkAparkA){System.out.println("管理员:负责公园A部分卫生检查");}@Overridepublicvoidvisit(ParkBparkB){System.out.println("管理员:负责公园B部分卫生检查");}}// ========== 5. 测试 ==========publicclassMainClass{publicstaticvoidmain(String[]args){Parkpark=newPark();VisitorAvisitorA=newVisitorA();park.accept(visitorA);// 清洁工A 只扫 AVisitorBvisitorB=newVisitorB();park.accept(visitorB);// 清洁工B 只扫 BVisitorManagervisitorManager=newVisitorManager();park.accept(visitorManager);// 管理员全面检查}}

运行结果

清洁工A:完成公园A的卫生 清洁工B:完成公园B的卫生 管理员:负责公园卫生检查 管理员:负责公园A部分卫生检查 管理员:负责公园B部分卫生检查

可以看到:同一个park.accept(visitor)调用,传入不同访问者就自动"分发"到了对应的visit重载方法。


五、核心原理:双分派(Double Dispatch)

这是访问者模式最灵魂、也最容易被面试追问的部分。

Java 是单分派语言:方法调用具体执行哪个版本,在编译期就由调用者(receiver)的静态类型决定了(重载overload是编译期绑定,重写override是运行期绑定,但都只盯一个维度)。

访问者模式用两次动态绑定绕开了这个限制:

  1. 第一次分派:park.accept(visitor)
    • accept具体执行哪个,由元素的运行时类型(Park/ParkA/ParkB)决定 → 动态
  2. 第二次分派:accept内部调用visitor.visit(this)
    • 这里this把元素的真实类型传进去,visit(this)调哪个重载,由访问者的运行时类型 + this 的类型共同决定 → 又是动态

正是靠"元素先accept把this回传",才让visit的重载选择延迟到运行期、由"元素类型 × 访问者类型"两个维度共同决定。这就是Double Dispatch(双分派)。普通的方法重载(如visit(Park)/visit(ParkA))在单分派语言里本无法按运行时类型自动选,访问者模式通过这层"回传 this"把它实现了。


六、开闭原则的权衡:访问者模式的"阿喀琉斯之踵"

访问者模式在开闭原则上是不对称的,这点必须讲清:

变化点是否好扩展原因
新增操作(新 Visitor)✅ 容易只加一个ConcreteVisitor类,原有元素类一行都不用改
新增元素(新节点类型,如ParkC)❌ 困难必须改Visitor接口(加visit(ParkC))+所有ConcreteVisitor实现,牵一发动全身

所以一句话选型:

元素类型稳定、操作频繁变化 → 用访问者;如果元素类型本身也天天变,别用访问者(改用其他方案,或重新审视设计)。


七、真实世界中的访问者模式(JDK / 框架)

  • java.nio.file.FileVisitor/SimpleFileVisitor:遍历目录树时,对"文件"和"目录"做不同操作(如统计大小、查找特定文件)。元素(文件/目录)稳定,操作(复制/删除/统计)多变——访问者模式的教科书级落地。
  • ASM 字节码框架:ClassVisitor在解析.class文件时,对字段、方法、注解、指令等不同类型节点做不同处理,是访问者模式的经典应用。
  • Spring:BeanDefinitionVisitor用来访问并修改BeanDefinition中的占位符/值。
  • 编译器:抽象语法树(AST)的遍历,对不同类型的语法节点(赋值、循环、函数调用)做语义分析、优化、代码生成,几乎都用访问者。

八、应用场景与优缺点

适用场景

  • 对象结构(元素集合)稳定,但经常需要在其上定义新的操作。
  • 需要对一个对象结构中的多个元素做互不相干的操作,希望把这些操作"集中"而不是散落在各个元素类里。
  • 元素类需要避免暴露内部状态给外部操作(访问者可通过受控接口访问)。

优点

  1. 易扩展新操作:新增功能 = 新增 Visitor 类,符合开闭原则的操作侧。
  2. 操作集中:相关的行为集中到一个 Visitor 里(如"所有卫生检查逻辑"都在VisitorManager),便于维护,也方便做"累加器"式统计(.visit 里累加结果)。
  3. 元素类保持纯净:数据类只管数据,不堆砌各种业务方法。

缺点

  1. 新增元素极难:要改 Visitor 接口和所有实现,违反开闭原则的元素侧(见第六节)。
  2. 破坏封装:访问者往往需要元素暴露内部细节才能操作(元素通常得提供getXxx给 visitor 读)。
  3. 结构变复杂:多了 Visitor 接口、双分派、ObjectStructure, newcomer 理解成本高于简单 if-else。

九、总结

一句话:访问者模式 = 把"操作"从"数据"里抽出来,通过双分派让同一份数据被不同的访问者以不同方式处理,元素不动、操作随便加。

记忆口诀:

数据结构稳如山,操作天天在变 → 上访问者;
元素也想常增减 → 访问者先靠边。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询