设计模式——访问者设计模式(行为型)
摘要
访问者设计模式是一种行为型设计模式,它将数据结构与作用于结构上的操作解耦,允许在不修改数据结构的前提下增加新的操作行为。该模式包含关键角色如元素接口、具体元素类、访问者接口和具体访问者类。通过访问者模式,可以在不改变对象结构的情况下,定义新的操作行为。文章通过示例场景和类图、时序图等详细介绍了访问者设计模式的结构和实现方式,并探讨了其适用场景和实战示例。
1. 访问者设计模式定义
将数据结构与作用于结构上的操作解耦,使得在不修改数据结构的前提下,可以增加新的操作行为。访问者模式允许你在不改变对象结构(如树、图、元素集合)的前提下,定义新的操作行为,通过将这些操作封装到独立的 "访问者" 对象中。
1.1. 关键角色说明
角色 | 说明 |
| 定义 |
| 实现 |
| 抽象访问者,定义访问每个元素的接口 |
| 实现 |
1.2. 示例场景(通俗类比)
假设你有一个对象结构为:公司组织结构,每个节点可以是 员工、部门。你希望在不修改员工、部门类的前提下,分别实现:
- 统计薪资总额
- 导出组织结构为 HTML
- 打印汇报关系图
通过访问者模式,你可以创建多个 ConcreteVisitor
来实现上述功能,而无需改动 Element 本身代码。
2. 访问者设计模式结构
- 访问者 (Visitor) 接口声明了一系列以对象结构的具体元素为参数的访问者方法。 如果编程语言支持重载, 这些方法的名称可以是相同的, 但是其参数一定是不同的。
- 具体访问者 (Concrete Visitor) 会为不同的具体元素类实现相同行为的几个不同版本。
- 元素 (Element) 接口声明了一个方法来 “接收” 访问者。 该方法必须有一个参数被声明为访问者接口类型。
- 具体元素 (Concrete Element) 必须实现接收方法。 该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。 请注意, 即使元素基类实现了该方法, 所有子类都必须对其进行重写并调用访问者对象中的合适方法。
- 客户端 (Client) 通常会作为集合或其他复杂对象 (例如一个组合(opens new window)树) 的代表。 客户端通常不知晓所有的具体元素类, 因为它们会通过抽象接口与集合中的对象进行交互。
2.1. 访问者设计模式类图
2.2. 访问者设计模式时序图
3. 访问者设计模式实现方式
访问者设计模式的实现方式,核心在于:将作用于对象结构的操作行为封装到独立的访问者类中,并通过 accept(Visitor)
方法把访问者“注入”到元素中,从而实现对结构中不同元素的不同处理。
3.1. 步骤 1:定义元素接口 Element
public interface Element {void accept(Visitor visitor);
}
3.2. 步骤 2:实现具体元素类(ConcreteElement)
public class Employee implements Element {private String name;private double salary;public Employee(String name, double salary) {this.name = name;this.salary = salary;}// 提供访问者访问自己@Overridepublic void accept(Visitor visitor) {visitor.visit(this);}// getterpublic String getName() { return name; }public double getSalary() { return salary; }
}
public class Department implements Element {private String deptName;public Department(String deptName) {this.deptName = deptName;}@Overridepublic void accept(Visitor visitor) {visitor.visit(this);}public String getDeptName() { return deptName; }
}
3.3. 步骤 3:定义访问者接口 Visitor
public interface Visitor {void visit(Employee employee);void visit(Department department);
}
3.4. 步骤 4:实现具体访问者(ConcreteVisitor)
public class ReportVisitor implements Visitor {@Overridepublic void visit(Employee employee) {System.out.println("员工:" + employee.getName() + ",薪资:" + employee.getSalary());}@Overridepublic void visit(Department department) {System.out.println("部门:" + department.getDeptName());}
}
3.5. 步骤 5:使用访问者
public class Client {public static void main(String[] args) {List<Element> elements = Arrays.asList(new Employee("张三", 12000),new Employee("李四", 15000),new Department("风控部"));Visitor visitor = new ReportVisitor();for (Element element : elements) {element.accept(visitor); // 每个元素调用 visitor.visit(this)}}
}
3.6. 说明总结
点位 | 描述 |
解耦操作和结构 | 不改动 Element 的结构代码,就能添加任意多种访问逻辑。 |
遵循开闭原则 | 新增操作时,只需新建访问者类即可。 |
单一职责更清晰 | 每个访问者只关注自己的行为。 |
缺点:扩展结构麻烦 | 每次新增 Element 子类,所有 |
4. 访问者设计模式适合场景
以下是访问者(Visitor)设计模式适合与不适合使用的场景清单,便于你快速判断在实战开发中是否应当使用此模式。
4.1. ✅ 适合使用访问者设计模式的场景
场景 | 说明 |
✅ 需要对对象结构中不同元素进行不同操作 | 如处理复杂对象树时,不同节点类型需要不同处理(如 AST 抽象语法树、HTML DOM、组织结构等)。 |
✅ 需要在不修改类的前提下增加新操作 | 元素类稳定,但经常添加新功能,适合将操作逻辑外移成访问者类。符合开闭原则。 |
✅ 多个操作跨多个类共享处理逻辑 | 如统计报表、导出功能、数据校验,每种功能可封装为访问者,避免元素类职责膨胀。 |
✅ 数据结构较复杂,逻辑需要分离 | 尤其在组合模式(Composite)中遍历树状结构时,访问者是理想搭档。 |
✅ 需要记录访问轨迹/数据收集/行为链式执行 | 访问者可收集上下文数据,实现功能链、审计等操作。 |
4.2. ❌ 不适合使用访问者设计模式的场景
场景 | 原因 |
❌ 对象结构不稳定,经常增删元素类型 | 每新增一个元素类,所有访问者都必须修改,违反开闭原则,维护成本高。 |
❌ 操作种类少,变化不频繁 | 如果只有一两种操作,直接放到元素类中即可,访问者反而引入了额外复杂性。 |
❌ 操作需要频繁访问元素内部状态/私有字段 | 访问者访问元素的内部字段时会暴露实现细节,可能破坏封装性。 |
❌ 数据驱动而非行为驱动系统 | 如果处理逻辑更多是对数据表进行规则驱动处理,不如使用策略模式、责任链、状态机等。 |
❌ 系统对性能极度敏感,层层函数调用不可接受 | 访问者调用链过长,对性能要求极高的系统中不推荐使用。 |
4.3. ✅ 实战使用建议
建议点 | 内容 |
👍 推荐与组合模式搭配使用 | 树形结构遍历 + 多种处理逻辑,非常适合访问者模式。 |
👍 可与责任链、模板方法组合 | 在访问者中执行链式操作或分步骤逻辑。 |
⚠️ 避免与频繁变更的领域模型搭配 | 如果业务模型变化频繁,访问者维护成本非常高。 |
5. 访问者设计模式实战示例
以下是一个基于访问者设计模式的 Spring 项目实战示例,应用于金融风控场景,使用注解方式注入对象,涵盖了完整的结构。
金融风控中,需要对不同类型的用户(例如:个人、企业)进行多维度风险评估,比如信用评分、交易行为分析、黑名单检查等。不同用户类型暴露的数据结构不同,但我们希望将“风险评估逻辑”从数据结构中解耦出来。
使用访问者模式可实现:
- 新增风险评估逻辑(访问者)无需修改用户结构;
- 用户数据结构与评估操作解耦,符合开闭原则。
5.1. 📦 项目结构
risk-visitor
├── model
│ ├── User.java
│ ├── PersonalUser.java
│ └── CompanyUser.java
├── visitor
│ ├── RiskVisitor.java
│ ├── CreditScoreVisitor.java
│ └── FraudCheckVisitor.java
├── config
│ └── VisitorConfig.java
└── RiskEvaluationService.java
5.2. 用户对象层(Element)
public interface User {void accept(RiskVisitor visitor);
}
@Data
public class PersonalUser implements User {private String name;private String idCard;private int creditScore;@Overridepublic void accept(RiskVisitor visitor) {visitor.visit(this);}
}
@Data
public class CompanyUser implements User {private String companyName;private String licenseNumber;private double registeredCapital;@Overridepublic void accept(RiskVisitor visitor) {visitor.visit(this);}
}
5.3. 风控访问者接口与实现
public interface RiskVisitor {void visit(PersonalUser personalUser);void visit(CompanyUser companyUser);
}
5.3.1. 信用评分访问者
@Component
public class CreditScoreVisitor implements RiskVisitor {@Overridepublic void visit(PersonalUser personalUser) {System.out.println("[信用评分] 用户 " + personalUser.getName() + " 分数: " + personalUser.getCreditScore());}@Overridepublic void visit(CompanyUser companyUser) {System.out.println("[信用评分] 公司 " + companyUser.getCompanyName() + " 注册资本: " + companyUser.getRegisteredCapital());}
}
5.3.2. 欺诈检测访问者
@Component
public class FraudCheckVisitor implements RiskVisitor {@Overridepublic void visit(PersonalUser personalUser) {System.out.println("[欺诈检查] 检查身份证是否黑名单:" + personalUser.getIdCard());}@Overridepublic void visit(CompanyUser companyUser) {System.out.println("[欺诈检查] 检查营业执照是否异常:" + companyUser.getLicenseNumber());}
}
5.4. 风控服务类(注解注入访问者)
@Service
public class RiskEvaluationService {private final List<RiskVisitor> visitors;@Autowiredpublic RiskEvaluationService(List<RiskVisitor> visitors) {this.visitors = visitors;}public void evaluate(User user) {for (RiskVisitor visitor : visitors) {user.accept(visitor);}}
}
5.5. 启动与使用
@SpringBootApplication
public class RiskApp implements CommandLineRunner {@Autowiredprivate RiskEvaluationService evaluationService;@Overridepublic void run(String... args) {PersonalUser personalUser = new PersonalUser();personalUser.setName("张三");personalUser.setIdCard("123456789");personalUser.setCreditScore(750);CompanyUser companyUser = new CompanyUser();companyUser.setCompanyName("风控科技");companyUser.setLicenseNumber("XYZ-2025");companyUser.setRegisteredCapital(5000_000);evaluationService.evaluate(personalUser);evaluationService.evaluate(companyUser);}public static void main(String[] args) {SpringApplication.run(RiskApp.class, args);}
}
5.6. ✅ 总结优点
- 易于扩展新评估策略,无需改动用户结构;
- Spring 自动注入访问者集合,灵活组合;
- 清晰分离了数据结构与操作行为。
6. 访问者设计模式思考
访问者设计模式(Visitor Pattern)在实际开发中常常与其他设计模式组合使用,以增强系统的可扩展性、解耦能力和灵活性。下面列出访问者模式常与哪些设计模式组合使用,以及各组合的典型应用场景和优势。
6.1. ✅ 访问者模式常用组合设计模式
组合模式 | 组合目的/优势 | 应用场景示例 |
组合模式(Composite) | 用于遍历和访问复杂对象结构,访问者可递归处理整个树形结构 | 文档结构、组织架构、产品分类树等 |
迭代器模式(Iterator) | 统一遍历容器结构,配合访问者实现对集合中元素的操作(如批量处理) | 批量风控评估、设备监控列表操作 |
责任链模式(Chain of Responsibility) | 多个访问者对象串联处理,解耦多个处理逻辑,每个访问者判断是否处理 | 多规则风控审批流程,每个处理节点负责一类校验 |
模板方法模式(Template Method) | 访问者中封装处理通用流程,将子类特定行为抽象为钩子方法 | 通用风险评估框架,子类定义评分规则 |
策略模式(Strategy) | 将访问者作为策略进行注入或切换,使不同访问行为可配置化 | 不同国家/行业的风险评估策略 |
状态模式(State) | 被访问对象的状态决定了访问者逻辑流程,用于基于状态执行不同操作 | 用户行为轨迹、风控状态迁移等 |
工厂模式(Factory) | 访问者工厂根据上下文动态创建访问者对象,适配不同对象结构或执行策略 | 动态风控策略调度系统,按对象类型或场景创建访问者 |
观察者模式(Observer) | 访问者中执行完后通知监听者,适用于监控、日志、审计等异步行为 | 风控决策日志记录、报警事件触发 |
博文参考
- 访问者设计模式
- 设计模式之访问者模式 | DESIGN
相关文章:

设计模式——访问者设计模式(行为型)
摘要 访问者设计模式是一种行为型设计模式,它将数据结构与作用于结构上的操作解耦,允许在不修改数据结构的前提下增加新的操作行为。该模式包含关键角色如元素接口、具体元素类、访问者接口和具体访问者类。通过访问者模式,可以在不改变对象…...

实验设计与分析(第6版,Montgomery著,傅珏生译) 第10章拟合回归模型10.9节思考题10.1 R语言解题
本文是实验设计与分析(第6版,Montgomery著,傅珏生译) 第10章拟合回归模型10.9节思考题10.1 R语言解题。主要涉及线性回归、回归的显著性、回归系数的置信区间。 vial <- seq(1, 10, 1) Viscosity <- c(160,171,175,182,184,181,188,19…...
《对象创建的秘密:Java 内存布局、逃逸分析与 TLAB 优化详解》
大家好呀!今天我们来聊聊Java世界里那些"看不见摸不着"但又超级重要的东西——对象在内存里是怎么"住"的,以及JVM这个"超级管家"是怎么帮我们优化管理的。放心,我会用最接地气的方式讲解,保证连小学…...

LeetCode 高频 SQL 50 题(基础版) 之 【高级查询和连接】· 下
上部分链接:LeetCode 高频 SQL 50 题(基础版) 之 【高级查询和连接】 上 题目:1164. 指定日期的产品价格 题解: select product_id,10 price from Products group by product_id having min(change_date) > 201…...
Java并发编程:读写锁与普通互斥锁的深度对比
在Java并发编程中,锁是实现线程安全的重要工具。其中,普通互斥锁(如synchronized和ReentrantLock)和读写锁(ReentrantReadWriteLock)是两种常用的同步机制。本文将从多个维度深入分析它们的区别、适用场景及…...
Spring Boot Actuator未授权访问漏洞修复
方案1:在网关的配置文件里增加以下配置 management:endpoints:web:exposure:include: []enabled-by-default: falseendpoint:health:show-details: ALWAYS 方案二:直接在nginx配置拦截actuator相关接口 location /actuator { return 403; …...

机器学习——SVM
1.什么是SVM 支持向量机(support vector machines,SVM)是一种二分类模型,它将实例的特征向量映射为空间中的一些点,SVM 的目的就是想要画出一条线,以 “最好地” 区分这两类点,以至如果以后有了…...

【音视频】FFmpeg 硬件(NVDIA)编码H264
FFmpeg 与x264的关系 ffmpeg软编码是使⽤x264开源项⽬,也就是说ffmpeg软编码H264最终是调⽤了x264开源项⽬,所以我们要先理解ffmpeg和x264的调⽤关系,这⾥我们主要关注x264_init。对于x264的参数都在 ffmpeg\libavcodec \libx264.c x264\co…...

贪心算法应用:超图匹配问题详解
贪心算法应用:超图匹配问题详解 贪心算法在超图匹配问题中有着广泛的应用。下面我将从基础概念到具体实现,全面详细地讲解超图匹配问题及其贪心算法解决方案。 一、超图匹配问题基础 1. 超图基本概念 **超图(Hypergraph)**是普…...
OpenCV CUDA模块结构分析与形状描述符------计算指定阶数的矩(Moments)所需的总数量函数:numMoments
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 该函数用于计算指定阶数的矩(Moments)所需的总数量。 在图像处理中,矩(moments)是一…...

【Web应用】若依框架:基础篇13 源码阅读-前端代码分析
文章目录 ⭐前言⭐一、课程讲解过程⭐二、自己动手实操⭐总结 标题详情作者JosieBook头衔CSDN博客专家资格、阿里云社区专家博主、软件设计工程师博客内容开源、框架、软件工程、全栈(,NET/Java/Python/C)、数据库、操作系统、大数据、人工智能、工控、网…...

[java八股文][JavaSpring面试篇]SpringCloud
了解SpringCloud吗,说一下他和SpringBoot的区别 Spring Boot是用于构建单个Spring应用的框架,而Spring Cloud则是用于构建分布式系统中的微服务架构的工具,Spring Cloud提供了服务注册与发现、负载均衡、断路器、网关等功能。 两者可以结合…...
深度学习篇---face-recognition的优劣点
face_recognition库是一个基于 Python 的开源人脸识别工具,封装了 dlib 库的深度学习模型,具有易用性高、集成度强的特点。以下从技术实现、应用场景等维度分析其优劣势: 一、核心优势 1. 极简 API 设计,开发效率极高 代码量少:几行代码即可实现人脸检测、特征提取和比对…...

基于分布式状态机的集装箱智能道口软件架构方法
集装箱码头对进出场道口的通过能力始终是要求最高的,衡量道口的直接指标为道口通行效率,道口通行效率直接体现了集装箱码头的作业效率以及对外服务水平,进而直接影响到码头的综合能力。所以,码头普遍使用智能道口实现24小时无人值…...
Oracle的Hint
racle的Hint是用来提示Oracle的优化器,用来选择用户期望的执行计划。在许多情况下,Oracle默认的执行方式并不总是最优的,只不过由于平时操作的数据量比较小,所以,好的执行计划与差的执行计划所消耗的时间差异不大&…...
手动事务的使用
使用原因: 公司需要写一个定时任务,涉及增改查操作, 定时将前端页面配置的字典数据(标签数据)同步到数据库特定的表(标签表) 查询字典表数据 字典有,数据库表没有新增 都有,判断名称,名称不同修…...

Vue 树状结构控件
1、效果图如下所示: 2、网络请求的数据结构如下: 3、新建插件文件:menu-tree.vue,插件代码如下: <template><div class"root"><div class"parent" click"onParentClick(pare…...
Spring Boot的启动流程,以及各个扩展点的执行顺序
目录 1. 初始化阶段执行顺序 1.1 Bean的构造方法(构造函数) 1.2 PostConstruct 注解方法 1.3 InitializingBean 的 afterPropertiesSet() 1.4 Bean(initMethod "自定义方法") 2. 上下文就绪后的扩展点 2.1 ApplicationContext 事件监听…...

【LUT技术专题】图像自适应3DLUT代码讲解
本文是对图像自适应3DLUT技术的代码解读,原文解读请看图像自适应3DLUT文章讲解 1、原文概要 结合3D LUT和CNN,使用成对和非成对的数据集进行训练,训练后能够完成自动的图像增强,同时还可以做到极低的资源消耗。下图为整个模型的…...
Apache Doris 在数据仓库中的作用与应用实践
在当今数字化时代,企业数据呈爆炸式增长,数据仓库作为企业数据管理和分析的核心基础设施,其重要性不言而喻。而 Apache Doris,作为一款基于 MPP(Massively Parallel Processing,大规模并行处理)…...

vscode使用“EIDE”和“Cortex-Debug”插件利用st-link插件实现程序烧写以及调试工作
第一步:安装vscode插件“EIDE”EIDE和“Cortex-Debug”。 第二步:配置EIDE 2.1安装“实用工具”: 2.2 EIDE插件配置:根据安装的keil C51 keil MDK IAR的相关路径设置 第三步:配置Cortex-Debug插件 点击settings.jso…...

Spring @Value注解的依赖注入实现原理
Spring Value注解的依赖注入实现原理 一,什么是Value注解的依赖注入二,实现原理三,代码实现1. 定义 Value 注解2. 实现 InstantiationAwareBeanPostProcessor3. 实现 AutowiredAnnotationBeanPostProcessor4. 占位符解析逻辑5. 定义 StringVa…...

三、kafka消费的全流程
五、多线程安全问题 1、多线程安全的定义 使用多线程访问一个资源,这个资源始终都能表现出正确的行为。 不被运行的环境影响、多线程可以交替访问、不需要任何额外的同步和协同。 2、Java实现多线程安全生产者 这里只是模拟多线程环境下使用生产者发送消息&…...
商品模块中的多规格设计:实现方式与电商/ERP系统的架构对比
在商品管理系统中,多规格设计(Multi-Specification Product Design)是一个至关重要但又极具挑战性的领域。无论是面向消费者的电商系统,还是面向企业管理的ERP系统,对商品规格的处理方式直接影响库存管理、订单履约、数…...
(三)动手学线性神经网络:从数学原理到代码实现
1 线性回归 线性回归是一种基本的预测模型,用于根据输入特征预测连续的输出值。它是机器学习和深度学习中最简单的模型之一,但却是理解更复杂模型的基础。 1.1 线性回归的基本元素 概念理解: 线性回归假设输入特征和输出之间存在线性关系。…...

Axure形状类组件图标库(共8套)
点击下载《月下倚楼图标库(形状组件)》 原型效果:https://axhub.im/ax9/02043f78e1b4386f/#g1 摘要 本图标库集锦精心汇集了8套专为Axure设计的形状类图标资源,旨在为产品经理、UI/UX设计师以及开发人员提供丰富多样的设计素材,提升原型设计…...

20250530-C#知识:String与StringBuilder
String与StringBuilder string字符串在开发中经常被用到,不过在需要频繁对字符串进行增加和删除时,使用StringBuilder有利于提升效率。 1、String string是一种引用类型而非值类型(某些方面像值类型)使用“”进行两个string对象的…...

从 Docker 到 Containerd:Kubernetes 容器运行时迁移实战指南
一、背景 Kubernetes 自 v1.24 起移除了 dockershim,不再原生支持 Docker Engine,用户需迁移至受支持的 CRI 兼容运行时,如: Containerd(推荐,高性能、轻量级) CRI-O(专为 Kuberne…...

uniapp中view标签使用范围
不止用于微信小程序。兼容型号,是uniapp内置组件之一,在uniapp中进行了跨平台适配。支持所有uniapp的平台。如微信小程序、h5、app、支付宝小程序...
Celery 核心概念详解及示例
Celery 核心概念详解及示例 Celery 是一个简单、灵活且可靠的分布式系统,用于处理大量消息,提供对任务队列的操作,并支持任务的调度和异步执行。它常用于深度优化 Web 应用的性能和响应速度,通过将耗时的操作移到后台异步执行&am…...