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 IoCBeanDefinitionVisitor7.3 代码格式化工具Checkstyle、PMD8. 面试必问 面试官追问连环炮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.namename;this.priceprice;}publicStringgetName(){returnname;}publicdoublegetPrice(){returnprice;}Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);// 第二次分派把自己传给访问者}}publicclassClothingProductimplementsProduct{privateStringname;privatedoubleprice;privateStringsize;publicClothingProduct(Stringname,doubleprice,Stringsize){this.namename;this.priceprice;this.sizesize;}publicStringgetName(){returnname;}publicdoublegetPrice(){returnprice;}publicStringgetSize(){returnsize;}Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);}}publicclassFoodProductimplementsProduct{privateStringname;privatedoubleprice;privateLocalDateexpiryDate;publicFoodProduct(Stringname,doubleprice,LocalDateexpiryDate){this.namename;this.priceprice;this.expiryDateexpiryDate;}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{privatedoubletotalSales0;Overridepublicvoidvisit(ElectronicProductproduct){doubletaxproduct.getPrice()*0.13;System.out.println(电子产品product.getName()售价product.getPrice()税费tax);totalSalesproduct.getPrice()tax;}Overridepublicvoidvisit(ClothingProductproduct){doubletaxproduct.getPrice()*0.08;System.out.println(服装product.getName()(product.getSize())售价product.getPrice()税费tax);totalSalesproduct.getPrice()tax;}Overridepublicvoidvisit(FoodProductproduct){doubletaxproduct.getPrice()*0.05;System.out.println(食品product.getName()售价product.getPrice()税费tax保质期至product.getExpiryDate());totalSalesproduct.getPrice()tax;}publicdoublegetTotalSales(){returntotalSales;}}白话销售报表知道每种商品怎么算税费——电子产品 13%、服装 8%、食品 5%。这些逻辑全部集中在访问者中商品类完全不知道。4.5 具体访问者库存报表publicclassInventoryReportVisitorimplementsVisitor{Overridepublicvoidvisit(ElectronicProductproduct){System.out.println(【库存-电子】product.getName()建议库存50 件);}Overridepublicvoidvisit(ClothingProductproduct){Stringsuggestionproduct.getSize().equals(XL)?建议库存20 件大码少备货:建议库存50 件;System.out.println(【库存-服装】product.getName()(product.getSize())suggestion);}Overridepublicvoidvisit(FoodProductproduct){booleannearExpiryproduct.getExpiryDate().isBefore(LocalDate.now().plusDays(30));StringsuggestionnearExpiry?警告临近保质期建议库存10 件:建议库存100 件;System.out.println(【库存-食品】product.getName()suggestion);}}白话库存报表完全不关心税费它关心的是“该备多少货”。这就是访问者模式的核心价值——每种操作独立成类互不干扰。4.6 对象结构订单publicclassOrder{privateListProductproductsnewArrayList();publicvoidaddProduct(Productproduct){products.add(product);}publicvoidaccept(Visitorvisitor){for(Productproduct:products){product.accept(visitor);// 遍历每个商品让访问者处理}}}白话订单就是一个“商品集合”它负责遍历所有商品让访问者逐个处理。4.7 客户端OrderordernewOrder();order.addProduct(newElectronicProduct(机械键盘,599));order.addProduct(newClothingProduct(羽绒服,899,XL));order.addProduct(newFoodProduct(有机牛奶,15,LocalDate.now().plusDays(20)));System.out.println( 销售报表 );SalesReportVisitorsalesVisitornewSalesReportVisitor();order.accept(salesVisitor);System.out.println(销售总额salesVisitor.getTotalSales());System.out.println(\n 库存报表 );InventoryReportVisitorinventoryVisitornewInventoryReportVisitor();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 使用访问者模式解析字节码ClassReaderreadernewClassReader(com.example.MyClass);ClassWriterwriternewClassWriter(reader,0);ClassVisitorvisitornewMyClassVisitor(writer);// 自定义访问者reader.accept(visitor,0);// 遍历字节码访问者处理每个部分7.2 Spring IoCBeanDefinitionVisitorSpring 的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接口隔离原则ISPVisitor为每种元素定义独立的visit()方法迪米特法则LoD对象结构封装遍历逻辑客户端只与访问者交互 《Java 23 种设计模式从踩坑到精通》快速导航开篇系列介绍与目录上一篇模版方法模式 —— 定义算法骨架交给子类填充细节当前访问者模式 —— 数据结构稳定但操作多变试试访问者你在这里下一篇系列总结25 篇文章我们学到了什么创建型模式汇总单例、工厂、建造者、原型结构型模式汇总适配器、装饰器、代理……行为型模式汇总观察者、策略、模板方法…… 关注《Java 23 种设计模式从踩坑到精通》用 25 篇文章彻底吃透设计模式。福利预告全系列代码及 UML 源码将在完结时统一打包开放点击「关注」「收藏」第一时间获取。下一篇系列总结25 篇文章我们学到了什么 除了设计模式我也在深挖智能物流实战WMS、托盘调度、机器学习落地。欢迎点击头像看看专栏 《出版社物流WMS智能调度实战》。技术相通思路可鉴。