当前位置: 首页 > news >正文

Java 23 种设计模式:从踩坑到精通 | 访问者模式 —— 数据结构稳定但操作多变?试试访问者

Java 23 种设计模式:从踩坑到精通 | 访问者模式 —— 数据结构稳定但操作多变?试试访问者

摘要:当数据结构相对稳定,但需要在其上定义多种互不相关的操作时,将操作分散到各个数据类中会导致职责混乱、扩展困难。访问者模式将“操作”从“数据结构”中分离出来,封装为独立的访问者对象,使得新增操作无需修改数据结构本身。本文从电商订单的报表计算场景出发,完整讲解访问者模式的原理、UML、代码实现、双分派机制,与迭代器模式进行对比,并结合 ASM 字节码操作、Spring IoC 的BeanDefinitionVisitor、代码格式化工具等应用,帮你掌握“操作与结构分离”的设计精髓。

🗺️本文阅读地图(3 分钟速览)

  • 为什么每新增一种报表就要改所有商品类?
  • 双分派:访问者模式的核心机制
  • 手写电商报表系统(销售报表 + 库存报表)
  • ASM 字节码框架如何体现代访问者模式
  • 面试必问:“访问者和迭代器有什么区别?什么是双分派?”

📖《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 上一篇:模版方法模式 |当前:访问者模式| 下一篇:系列总结
🔗 返回系列总目录


文章目录

  • Java 23 种设计模式:从踩坑到精通 | 访问者模式 —— 数据结构稳定但操作多变?试试访问者
    • 1. 从一个“报表计算”的纠结说起
      • 1.1 你的场景该不该用访问者?
    • 2. 模式定义与 UML 结构
      • 图文解析(配合上述 UML 图)
    • 3. 核心机制:双分派详解
    • 4. 代码实现:电商商品报表系统
      • 4.1 抽象元素
      • 4.2 具体元素
      • 4.3 抽象访问者
      • 4.4 具体访问者:销售报表
      • 4.5 具体访问者:库存报表
      • 4.6 对象结构:订单
      • 4.7 客户端
    • 5. 优缺点一览
    • 6. 访问者模式 vs 迭代器模式
    • 7. 框架与实践中的应用
      • 7.1 ASM 字节码操作框架
      • 7.2 Spring IoC:`BeanDefinitionVisitor`
      • 7.3 代码格式化工具(Checkstyle、PMD)
    • 8. 面试必问 + 面试官追问连环炮
    • 9. 六大设计原则在访问者模式中的体现
    • 🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

1. 从一个“报表计算”的纠结说起

假设你开发了一个电商系统,订单中包含多种商品:电子产品、服装、食品。每种商品的计费规则、税费计算、包装方式都不同。现在需要生成多种报表:销售报表、库存报表、物流报表……

如果在每种商品类中写死这些报表逻辑:

classElectronicProduct{voidgenerateSalesReport(){/* 电子产品的销售报表逻辑 */}voidgenerateInventoryReport(){/* 电子产品的库存报表逻辑 */}voidgenerateLogisticsReport(){/* 电子产品的物流报表逻辑 */}}classClothingProduct{voidgenerateSalesReport(){/* 服装的销售报表逻辑 */}voidgenerateInventoryReport(){/* 服装的库存报表逻辑 */}voidgenerateLogisticsReport(){/* 服装的物流报表逻辑 */}}classFoodProduct{voidgenerateSalesReport(){/* 食品的销售报表逻辑 */}voidgenerateInventoryReport(){/* 食品的库存报表逻辑 */}voidgenerateLogisticsReport(){/* 食品的物流报表逻辑 */}}

这种写法的致命问题

  • 商品类膨胀:每种商品类塞满了各种报表方法,职责混乱;
  • 违反开闭原则:新增一种报表(如“损耗报表”)就要修改所有商品类;
  • 报表逻辑分散:同一种报表的逻辑散落在不同商品类中,难以统一管理和优化;
  • 测试困难:测试销售报表需要实例化所有商品类型。

访问者模式(Visitor Pattern)正是为此而生:它将“操作”从“数据结构”中分离出来,封装为独立的访问者对象。新增操作只需新增一个访问者类,无需修改任何数据结构。

1.1 你的场景该不该用访问者?

判断标准是 → 用访问者否 → 用其他方式
数据结构稳定(元素类型很少变化)
需要频繁新增对数据结构的操作
操作之间互不相关,需要独立管理
数据结构频繁变化(经常新增元素类型)直接在元素类中加方法
操作很少变化直接在元素类中写死即可

2. 模式定义与 UML 结构

访问者模式表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。它属于行为型设计模式

图文解析(配合上述 UML 图)

访问者模式的核心角色:

  • 抽象访问者(Visitor:为每种具体元素声明一个visit()方法,方法名相同但参数类型不同(方法重载)。这是实现双分派的关键。
  • 具体访问者(ConcreteVisitor:实现每个visit()方法,定义对每种元素的具体操作。例如SalesReportVisitor实现销售报表逻辑。
  • 抽象元素(Element:定义一个accept(Visitor)方法,接受一个访问者对象。这是双分派的第一次分派——客户端将访问者传给元素。
  • 具体元素(ElementA/ElementB:实现accept()方法,在方法内部调用visitor.visit(this)。这是双分派的第二次分派——元素将自己传给访问者的对应重载方法。
  • 对象结构(ObjectStructure:持有元素集合,提供遍历所有元素的方法,通常包含accept(Visitor)批量接受访问者。

3. 核心机制:双分派详解

访问者模式的核心是双分派(Double Dispatch),它解决了“如何根据两个对象的类型来决定调用哪个方法”的问题。

Java 是单分派语言:方法调用由接收者类型参数编译时类型决定。访问者模式通过两次方法调用实现了伪双分派:

// 客户端:第一次分派——选择哪个访问者element.accept(visitor);// 元素内部:第二次分派——根据元素类型选择 visit 的重载版本publicvoidaccept(Visitorvisitor){visitor.visit(this);// this 的类型是确定的:ElectronicProduct}
调用步骤分派内容决定因素
product.accept(visitor)选择哪个访问者客户端传入的具体访问者类型
visitor.visit(this)选择visit的哪个重载版本this的具体元素类型

💬白话:第一次分派是“你要做什么报表”(销售报表 or 库存报表),第二次分派是“你要对什么商品做报表”(电子产品 or 服装 or 食品)。

4. 代码实现:电商商品报表系统

4.1 抽象元素

publicinterfaceProduct{voidaccept(Visitorvisitor);}

💬白话:所有商品都必须能“接待”访问者——你来了,我让你处理我。

4.2 具体元素

publicclassElectronicProductimplementsProduct{privateStringname;privatedoubleprice;publicElectronicProduct(Stringname,doubleprice){this.name=name;this.price=price;}publicStringgetName(){returnname;}publicdoublegetPrice(){returnprice;}@Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);// 第二次分派:把自己传给访问者}}publicclassClothingProductimplementsProduct{privateStringname;privatedoubleprice;privateStringsize;publicClothingProduct(Stringname,doubleprice,Stringsize){this.name=name;this.price=price;this.size=size;}publicStringgetName(){returnname;}publicdoublegetPrice(){returnprice;}publicStringgetSize(){returnsize;}@Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);}}publicclassFoodProductimplementsProduct{privateStringname;privatedoubleprice;privateLocalDateexpiryDate;publicFoodProduct(Stringname,doubleprice,LocalDateexpiryDate){this.name=name;this.price=price;this.expiryDate=expiryDate;}publicStringgetName(){returnname;}publicdoublegetPrice(){returnprice;}publicLocalDategetExpiryDate(){returnexpiryDate;}@Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);}}

💬白话:每个商品类的accept()都只有一行——调用visitor.visit(this)。商品类不关心访问者做什么,只管把自己交出去。

4.3 抽象访问者

publicinterfaceVisitor{voidvisit(ElectronicProductproduct);voidvisit(ClothingProductproduct);voidvisit(FoodProductproduct);}

💬白话:访问者必须为每种商品类型准备一个visit()方法。三个方法名相同但参数类型不同——这是 Java 的方法重载

4.4 具体访问者:销售报表

publicclassSalesReportVisitorimplementsVisitor{privatedoubletotalSales=0;@Overridepublicvoidvisit(ElectronicProductproduct){doubletax=product.getPrice()*0.13;System.out.println("电子产品:"+product.getName()+",售价:"+product.getPrice()+",税费:"+tax);totalSales+=product.getPrice()+tax;}@Overridepublicvoidvisit(ClothingProductproduct){doubletax=product.getPrice()*0.08;System.out.println("服装:"+product.getName()+"("+product.getSize()+"),售价:"+product.getPrice()+",税费:"+tax);totalSales+=product.getPrice()+tax;}@Overridepublicvoidvisit(FoodProductproduct){doubletax=product.getPrice()*0.05;System.out.println("食品:"+product.getName()+",售价:"+product.getPrice()+",税费:"+tax+",保质期至:"+product.getExpiryDate());totalSales+=product.getPrice()+tax;}publicdoublegetTotalSales(){returntotalSales;}}

💬白话:销售报表知道每种商品怎么算税费——电子产品 13%、服装 8%、食品 5%。这些逻辑全部集中在访问者中,商品类完全不知道。

4.5 具体访问者:库存报表

publicclassInventoryReportVisitorimplementsVisitor{@Overridepublicvoidvisit(ElectronicProductproduct){System.out.println("【库存-电子】"+product.getName()+",建议库存:50 件");}@Overridepublicvoidvisit(ClothingProductproduct){Stringsuggestion=product.getSize().equals("XL")?"建议库存:20 件(大码少备货)":"建议库存:50 件";System.out.println("【库存-服装】"+product.getName()+"("+product.getSize()+"),"+suggestion);}@Overridepublicvoidvisit(FoodProductproduct){booleannearExpiry=product.getExpiryDate().isBefore(LocalDate.now().plusDays(30));Stringsuggestion=nearExpiry?"警告:临近保质期,建议库存:10 件":"建议库存:100 件";System.out.println("【库存-食品】"+product.getName()+","+suggestion);}}

💬白话:库存报表完全不关心税费,它关心的是“该备多少货”。这就是访问者模式的核心价值——每种操作独立成类,互不干扰。

4.6 对象结构:订单

publicclassOrder{privateList<Product>products=newArrayList<>();publicvoidaddProduct(Productproduct){products.add(product);}publicvoidaccept(Visitorvisitor){for(Productproduct:products){product.accept(visitor);// 遍历每个商品,让访问者处理}}}

💬白话:订单就是一个“商品集合”,它负责遍历所有商品,让访问者逐个处理。

4.7 客户端

Orderorder=newOrder();order.addProduct(newElectronicProduct("机械键盘",599));order.addProduct(newClothingProduct("羽绒服",899,"XL"));order.addProduct(newFoodProduct("有机牛奶",15,LocalDate.now().plusDays(20)));System.out.println("===== 销售报表 =====");SalesReportVisitorsalesVisitor=newSalesReportVisitor();order.accept(salesVisitor);System.out.println("销售总额:"+salesVisitor.getTotalSales());System.out.println("\n===== 库存报表 =====");InventoryReportVisitorinventoryVisitor=newInventoryReportVisitor();order.accept(inventoryVisitor);

输出:

===== 销售报表 ===== 电子产品:机械键盘,售价:599.0,税费:77.87 服装:羽绒服(XL),售价:899.0,税费:71.92 食品:有机牛奶,售价:15.0,税费:0.75,保质期至:2026-06-16 销售总额:1663.54 ===== 库存报表 ===== 【库存-电子】机械键盘,建议库存:50 件 【库存-服装】羽绒服(XL),建议库存:20 件(大码少备货) 【库存-食品】有机牛奶,警告:临近保质期,建议库存:10 件

新增一种报表(如物流报表)只需新增一个LogisticsReportVisitor类,所有商品类零修改。这就是访问者模式的核心价值。

5. 优缺点一览

优点缺点
操作与结构分离:新增操作只需新增访问者,符合开闭原则元素类必须稳定:新增元素类型需要修改所有访问者
职责集中:相同操作集中在访问者中,便于维护破坏封装:访问者通常需要访问元素的内部细节
双分派机制:巧妙解决根据两个类型选择方法的问题双分派理解成本高:初学时不易理解调用过程
访问者可以积累状态:如销售报表统计总额不符合开闭原则(元素维度):新增元素类型违反开闭原则

6. 访问者模式 vs 迭代器模式

对比维度访问者模式迭代器模式
目的在数据结构上执行操作顺序访问数据结构中的元素
操作与遍历访问者定义操作,遍历由对象结构控制迭代器负责遍历,客户端执行操作
扩展方向操作维度(新增访问者)遍历方式(新增迭代器)
典型应用ASM 字节码分析、报表生成集合遍历

💡 两者可以组合使用:用迭代器遍历集合,在遍历中对每个元素应用访问者。访问者模式关注“对元素做什么”,迭代器模式关注“如何遍历元素”。

7. 框架与实践中的应用

7.1 ASM 字节码操作框架

ASM 是访问者模式最经典的实现。ClassVisitor是抽象访问者,提供visitField()visitMethod()等方法。开发者通过继承ClassVisitor实现对字节码的修改和分析,无需了解字节码的内部结构。

// ASM 使用访问者模式解析字节码ClassReaderreader=newClassReader("com.example.MyClass");ClassWriterwriter=newClassWriter(reader,0);ClassVisitorvisitor=newMyClassVisitor(writer);// 自定义访问者reader.accept(visitor,0);// 遍历字节码,访问者处理每个部分

7.2 Spring IoC:BeanDefinitionVisitor

Spring 的BeanDefinitionVisitor使用访问者模式来遍历和修改BeanDefinition的属性值,常用于占位符替换、属性覆盖等场景。

7.3 代码格式化工具(Checkstyle、PMD)

这些静态分析工具通过访问者模式遍历 AST(抽象语法树),对不同类型的节点执行不同的检查规则。每种检查规则都是一个独立的访问者。


8. 面试必问 + 面试官追问连环炮

基础必问

  • 访问者模式的核心角色?→ Visitor、ConcreteVisitor、Element、ConcreteElement、ObjectStructure。
  • 什么是双分派?→ 第一次分派选择访问者(element.accept(visitor)),第二次分派根据元素类型选择对应方法(visitor.visit(this))。
  • 访问者模式为什么要求元素类稳定?→ 新增元素需要修改抽象访问者及所有具体访问者,违反开闭原则。

面试官追问

  • “访问者模式破坏了封装性吗?”
    👉 确实需要元素暴露部分内部状态给访问者。如果封装性要求极高,可以用迭代器模式代替,或让元素自己提供操作接口。
  • “ASM 用了什么设计模式?”
    👉 访问者模式。ClassVisitor是抽象访问者,开发者通过继承它实现对字节码的修改和分析。
  • “访问者模式和迭代器模式如何配合使用?”
    👉 迭代器负责遍历集合,访问者负责执行操作。例如用迭代器遍历订单中的商品,每个商品再接受访问者处理。
  • “为什么说访问者模式在元素维度上违反开闭原则?”
    👉 新增一种元素类型(如新增SportProduct),需要修改Visitor接口及所有具体访问者类。这是访问者模式最大的局限性。

🎉恭喜:如果你能立刻画出访问者模式的 UML 图,并解释双分派的两次方法调用过程,你已经掌握了 GoF 中最复杂的行为型模式之一。面试中回答“访问者模式 = 双分派 + 操作与结构分离”,会让面试官眼前一亮。


9. 六大设计原则在访问者模式中的体现

设计原则在访问者模式中的体现
单一职责原则(SRP)元素类负责数据,访问者负责操作,职责分离
开闭原则(OCP)新增操作只需新增访问者类,无需修改元素类;但新增元素类型需修改访问者
里氏替换原则(LSP)所有元素实现Element接口,所有访问者实现Visitor接口,可替换
依赖倒置原则(DIP)元素依赖抽象Visitor,访问者依赖抽象Element
接口隔离原则(ISP)Visitor为每种元素定义独立的visit()方法
迪米特法则(LoD)对象结构封装遍历逻辑,客户端只与访问者交互

🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

  • 开篇:系列介绍与目录
  • 上一篇:模版方法模式 —— 定义算法骨架,交给子类填充细节
  • 当前:访问者模式 —— 数据结构稳定但操作多变?试试访问者(你在这里)
  • 下一篇:系列总结:25 篇文章,我们学到了什么?
  • 创建型模式汇总:单例、工厂、建造者、原型
  • 结构型模式汇总:适配器、装饰器、代理……
  • 行为型模式汇总:观察者、策略、模板方法……

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。
🚀下一篇:系列总结:25 篇文章,我们学到了什么?

📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

http://www.cnnetsun.cn/news/3409805.html

相关文章:

  • ESP8266刷写WI-PWN固件实战:从零打造便携式WiFi安全测试工具
  • AtCoder新手入门:从注册到首场ABC比赛的完整实战指南
  • 【Bug已解决】_symmetric_memory NCCL rendezvous 失败(Failed to window register:invalid argument)解决方案
  • 西贡旧梦主题越南融合餐厅整体设计方案
  • 毕设项目 深度学习交通车流量计数系统(源码+论文)
  • Solana 状态压缩技术详解:账户数据 Merkle 化与并发写入的冲突解决方案
  • C语言轻量级RSA库集成指南:三步实现嵌入式数据加密
  • 【claude code实践】让 Claude Code 生成 Mock 数据:提高本地开发效率
  • 淘宝新店一般要熬几个月?淘宝新店破周期提速实操方案
  • 亲测!DeepSeek去除论文AI痕迹终极指南:15条黄金指令,AIGC率直降到2%以下
  • 模板驱动文档自动化:从填空题到印刷级PDF的工程化实践
  • LLM 服务网关的性能优化:连接池、批量推理与请求合并的工程方案
  • 八、SALV 列属性进阶:动态调整列宽与标签国际化实战
  • wps内联mathtype后,上方工具栏仍没有mathtype
  • Spark 3.x 动态分区裁剪:为什么它能自动跳过 90% 的数据扫描
  • 2026年最新北京市轨道交通图和 北京轨道交通规划图 附图
  • LVGL学习(四)- Objects
  • 2026市面上目前靠谱的扫码点餐小程序服务商哪家专业好用?实测推荐
  • JDK 12 新特性详解
  • Redis典型应用 - 分布式锁,引入过期时间,Lua脚本原子解锁操作,看门狗机制实现过期时间动态续约,Redlock算法
  • 新材料制造工业数据中台解决方案
  • 构建高性能存储网络:NVMe-oF、SPDK与RDMA的融合实践
  • 2026免费数字人平台怎么选:基于功能、成本、体验的实测选型框架,新手、老板IP和商用团队分别看什么
  • HTTP状态码404:从“死链接”到“软404”的SEO陷阱与实战修复
  • 小白程序员转行AI必看:Agent开发vs大模型开发,谁更适合你?
  • Gemini 3.5 到底适合做什么?2026最新大模型实用边界与选型攻略
  • 《VLOOKUP/XLOOKUP 匹配不到受局限!Python写一个 多条件模糊匹配,更强的提升excel处理效率》
  • LTC4417IUF#TRPBF是一款36V 工业级防反灌电源控制器
  • springboot外卖点餐系统00198-计算机课程设计/毕业设计
  • 【Springboot毕设全套源码+文档】基于springboot图书管理系统的设计与实现(丰富项目+远程调试+讲解+定制)