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

访问者模式:分离数据结构与操作的设计模式

访问者模式:分离数据结构与操作的设计模式

一、模式核心:将操作从数据结构中分离,支持动态添加新操作

在软件开发中,当数据结构(如树、集合)中的元素类型固定,但需要频繁添加新的操作(如遍历、统计、打印)时,直接修改元素类会违反开闭原则。

访问者模式(Visitor Pattern) 将数据结构与作用于结构上的操作分离,使得操作可以独立于数据结构进行扩展。通过引入访问者(Visitor)角色,客户端可以在不修改数据结构的前提下,新增对数据的操作。核心解决:

  • 操作扩展问题:新增操作时无需修改数据结构类,符合开闭原则。
  • 职责分离:数据结构仅负责存储数据,操作逻辑集中在访问者类中。
  • 复杂操作封装:将跨元素的复杂操作(如树的深度优先遍历 + 统计)封装在访问者中,避免污染数据结构。

核心思想与 UML 类图(PlantUML 语法)

访问者模式包含以下角色:

  1. 抽象访问者(Visitor):定义对每种元素的访问接口。
  2. 具体访问者(Concrete Visitor):实现抽象访问者接口,定义对具体元素的操作。
  3. 抽象元素(Element):定义接受访问者的接口(accept(Visitor))。
  4. 具体元素(Concrete Element):实现接受访问者的方法,调用访问者的对应操作。
  5. 对象结构(Object Structure):管理元素集合,允许访问者遍历所有元素。

PlantUML Diagram

二、核心实现:员工数据统计与可视化

1. 定义抽象元素(员工)

public interface Employee {  void accept(Visitor visitor); // 接受访问者  String getName();  double getSalary();  
}  

2. 实现具体元素(全职员工、兼职员工)

全职员工(有奖金)
public class FullTimeEmployee implements Employee {  private String name;  private double baseSalary;  private double bonus;  public FullTimeEmployee(String name, double baseSalary, double bonus) {  this.name = name;  this.baseSalary = baseSalary;  this.bonus = bonus;  }  @Override  public void accept(Visitor visitor) {  visitor.visitFullTimeEmployee(this); // 调用访问者的对应方法  }  // Getter 方法  public String getName() { return name; }  public double getSalary() { return baseSalary + bonus; }  public double getBonus() { return bonus; }  
}  
兼职员工(按小时计费)
public class PartTimeEmployee implements Employee {  private String name;  private double hourlyRate;  private int hoursWorked;  public PartTimeEmployee(String name, double hourlyRate, int hoursWorked) {  this.name = name;  this.hourlyRate = hourlyRate;  this.hoursWorked = hoursWorked;  }  @Override  public void accept(Visitor visitor) {  visitor.visitPartTimeEmployee(this); // 调用访问者的对应方法  }  // Getter 方法  public String getName() { return name; }  public double getSalary() { return hourlyRate * hoursWorked; }  public int getHoursWorked() { return hoursWorked; }  
}  

3. 定义抽象访问者(统计与打印)

public interface Visitor {  // 访问全职员工  void visitFullTimeEmployee(FullTimeEmployee employee);  // 访问兼职员工  void visitPartTimeEmployee(PartTimeEmployee employee);  
}  

4. 实现具体访问者(薪资统计、员工信息打印)

薪资统计访问者
public class SalaryVisitor implements Visitor {  private double totalSalary = 0;  @Override  public void visitFullTimeEmployee(FullTimeEmployee employee) {  totalSalary += employee.getSalary();  System.out.println("全职员工 " + employee.getName() + " 薪资:" + employee.getSalary());  }  @Override  public void visitPartTimeEmployee(PartTimeEmployee employee) {  totalSalary += employee.getSalary();  System.out.println("兼职员工 " + employee.getName() + " 薪资:" + employee.getSalary());  }  public double getTotalSalary() {  return totalSalary;  }  
}  
员工信息打印访问者
public class InfoPrintVisitor implements Visitor {  @Override  public void visitFullTimeEmployee(FullTimeEmployee employee) {  System.out.println("全职员工信息:");  System.out.println("姓名:" + employee.getName());  System.out.println("基本工资:" + employee.getSalary() - employee.getBonus());  System.out.println("奖金:" + employee.getBonus());  }  @Override  public void visitPartTimeEmployee(PartTimeEmployee employee) {  System.out.println("兼职员工信息:");  System.out.println("姓名:" + employee.getName());  System.out.println("工作时长:" + employee.getHoursWorked() + " 小时");  System.out.println("时薪:" + employee.getHourlyRate());  }  
}  

5. 对象结构(员工列表)

import java.util.ArrayList;  
import java.util.List;  public class EmployeeManager {  private List<Employee> employees = new ArrayList<>();  public void addEmployee(Employee employee) {  employees.add(employee);  }  // 接受访问者,遍历所有员工  public void accept(Visitor visitor) {  for (Employee employee : employees) {  employee.accept(visitor);  }  }  
}  

6. 客户端使用访问者模式

public class ClientDemo {  public static void main(String[] args) {  EmployeeManager manager = new EmployeeManager();  manager.addEmployee(new FullTimeEmployee("Alice", 8000, 2000));  manager.addEmployee(new PartTimeEmployee("Bob", 50, 160));  // 统计薪资  SalaryVisitor salaryVisitor = new SalaryVisitor();  manager.accept(salaryVisitor);  System.out.println("\n总薪资:" + salaryVisitor.getTotalSalary());  // 打印员工信息  InfoPrintVisitor printVisitor = new InfoPrintVisitor();  manager.accept(printVisitor);  }  
}  

输出结果

全职员工 Alice 薪资:10000.0  
兼职员工 Bob 薪资:8000.0  总薪资:18000.0  全职员工信息:  
姓名:Alice  
基本工资:8000.0  
奖金:2000.0  
兼职员工信息:  
姓名:Bob  
工作时长:160 小时  
时薪:50.0  

三、进阶:双分派与类型安全

访问者模式通过双分派(Double Dispatch) 实现类型安全的操作:

  1. 第一分派:元素调用 accept(visitor) 时,根据元素类型确定调用哪个 accept 实现。
  2. 第二分派:在 accept 方法中,调用 visitor.visitXXX(this),根据访问者类型和元素类型确定具体操作。
// 元素的 accept 方法(第一分派)  
public class FullTimeEmployee implements Employee {  @Override  public void accept(Visitor visitor) {  visitor.visitFullTimeEmployee(this); // 第二分派:传入当前元素实例  }  
}  

四、框架与源码中的访问者实践

1. Java 编译器(如 Eclipse JDT)

Java 编译器使用访问者模式遍历抽象语法树(AST),实现词法分析、语法分析和语义分析。例如,ASTVisitor 类定义了对各种节点(如 MethodDeclarationVariableDeclaration)的访问方法。

2. XML/JSON 解析器

解析器将 XML 节点(如 <user><order>)作为元素,访问者实现对节点的验证、转换或统计操作(如验证订单节点的金额是否合法)。

3. Apache Ant 构建工具

Ant 通过访问者模式遍历构建文件(build.xml)中的任务节点(如 <copy><delete>),执行对应的构建操作。

五、避坑指南:正确使用访问者模式的 3 个要点

1. 数据结构稳定性优先

访问者模式适用于数据结构稳定、操作频繁变化的场景。若数据结构经常新增元素类型,需修改所有访问者,违反开闭原则。

2. 避免过度使用双分派

双分派会增加代码复杂度,需确保访问者与元素的类型组合不会导致类爆炸(如元素类型 n,访问者类型 m,需实现 n×m 个方法)。

3. 处理跨元素操作的一致性

若访问者需要维护跨元素的状态(如统计总和),需在访问者中设计状态管理逻辑,避免状态泄漏到数据结构中。

六、总结:何时该用访问者模式?

适用场景核心特征典型案例
数据结构固定,操作多变元素类型不变,但需频繁新增操作编译器优化、文档格式转换(如 Markdown 转 HTML)
跨元素复杂操作操作需要遍历多种元素并执行不同逻辑电商订单统计(同时处理商品、物流、支付元素)
操作与数据解耦操作逻辑与数据存储分离,独立维护财务系统(数据存储在数据库,操作包含报表生成、审计)

访问者模式通过分离数据与操作,为系统提供了强大的扩展能力。下一篇我们将探讨最后一个设计模式 —— 解释器模式,解析如何实现自定义语言的解释器,敬请期待!

扩展思考:访问者模式 vs 策略模式

类型核心差异适用场景
访问者模式操作针对不同元素类型(多态分派)数据结构固定,操作动态扩展
策略模式操作是同接口的不同实现(算法切换)同一元素的不同处理策略

相关文章:

访问者模式:分离数据结构与操作的设计模式

访问者模式&#xff1a;分离数据结构与操作的设计模式 一、模式核心&#xff1a;将操作从数据结构中分离&#xff0c;支持动态添加新操作 在软件开发中&#xff0c;当数据结构&#xff08;如树、集合&#xff09;中的元素类型固定&#xff0c;但需要频繁添加新的操作&#xf…...

【AI训练环境搭建】在IDE(Pycharm或VSCode)上使用WSL2+Ubuntu22.04+Conda+Tensorflow+GPU进行机器学习训练

本次实践将在IDE&#xff08;Pycharm或VSCode&#xff09;上使用WSL2Ubuntu22.04TensorflowGPU进行机器学习训练。基本原理是在IDE中拉起WSL2中的Python解释器&#xff0c;并运行Python程序。要运行CondaTensorflowGPU你可能需要进行以下准备工作。 1. 此示例中将使用一个mnis…...

Leetcode19(亚马逊真题):删除链表的倒是第N个节点

题目分析 删除节点关键&#xff1a;找到被删节点的前一个节点&#xff0c;指针指向 虚拟头节点&#xff0c;方便删除头结点&#xff0c;形成统一操作 为啥要让快指针先行&#xff1f; 我认为更好懂的一种解释&#xff1a;快指针先行n步&#xff0c;这样快慢指针之间形成了一…...

Hadoop+Spark 笔记 2025/4/21

读书笔记 定义 1. 大数据&#xff08;Big Data&#xff09; - 指传统数据处理工具难以处理的海量、高速、多样的数据集合&#xff0c;通常具备3V特性&#xff08;Volume体量大、Velocity速度快、Variety多样性&#xff09;。扩展后还包括Veracity&#xff08;真实性&#x…...

千问2.5-VL-7B的推理、微调、部署_笔记2

接上篇&#xff1a;部署千问2.5-VL-7B_笔记1-CSDN博客 这里主要记录微调过程 一、模型微调 这里也使用ms-swift对qwen2.5和qwen2-vl进行自我认知微调和图像OCR微调&#xff0c;并对微调后的模型进行推理。ms-swift是魔搭社区官方提供的LLM工具箱&#xff0c;支持300大语言模…...

Redis从入门到实战基础篇

前言&#xff1a;Redis的安装包含在Redis从入门到实战先导篇中&#xff0c;需要的可移步至此节 目录 1.Redis简单介绍 2.初始Redis 2.1.认识NoSQL 2.2.认识Redis 2.3.安装Redis 3.Redis常见命令 3.1 Redis数据结构 3.2 通用命令 3.3 String命令 3.4 Key的层级结构 3…...

【Docker】在Ubuntu平台上的安装部署

写在前面 docker作为一种部署项目的辅助工具&#xff0c;真是太好用了需要魔法&#xff0c;不然无法正常运行笔者环境&#xff1a;ubuntu22.04 具体步骤 更新系统包索引 sudo apt update安装必要依赖包 sudo apt install -y apt-transport-https ca-certificates curl softwa…...

Java虚拟机(JVM)家族发展史及版本对比

Java虚拟机&#xff08;JVM&#xff09;家族发展史及版本对比 一、JVM家族发展史 1. 早期阶段&#xff08;1996-2000&#xff09; Classic VM&#xff08;Java 1.0-1.1&#xff09;&#xff1a; 厂商&#xff1a;Sun Microsystems&#xff08;Oracle前身&#xff09;。特点&…...

【学习笔记】Cadence电子设计全流程(三)Capture CIS 原理图绘制(下)

【学习笔记】Cadence电子设计全流程&#xff08;三&#xff09;Capture CIS 原理图绘制&#xff08;下&#xff09; 3.16 原理图中元件的编辑与更新3.17 原理图元件跳转与查找3.18 原理图常见错误设置于编译检查3.19 低版本原理图文件输出3.20 原理图文件的锁定与解锁3.21 Orca…...

数据库对象与权限管理-Oracle数据字典详解

1. 数据字典概念讲解 Oracle数据字典是数据库的核心组件&#xff0c;它存储了关于数据库结构、用户信息、权限设置和系统性能等重要的元数据信息。这些信息对于数据库的日常管理和维护至关重要。数据字典在数据库创建时自动生成&#xff0c;并随着数据库的运行不断更新。 数据…...

计算机图形学实践:结合Qt和OpenGL实现绘制彩色三角形

在Qt项目中结合OpenGL与CMake需要配置正确的依赖关系、链接库以及代码结构设计。以下是具体实现步骤和关键要点&#xff1a; 一、环境准备 安装Qt 确保安装包含OpenGL模块的Qt版本&#xff08;如Qt OpenGL、Qt OpenGLWidgets组件&#xff09;。安装CMake 使用3.10及以上版本&a…...

OpenCV 图形API(54)颜色空间转换-----将图像从 RGB 色彩空间转换到 HSV色彩空间RGB2HSV()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 将图像从 RGB 色彩空间转换为 HSV。该函数将输入图像从 RGB 色彩空间转换到 HSV。R、G 和 B 通道值的常规范围是 0 到 255。 输出图像必须是 8 位…...

SpringBoot 封装统一API返回格式对象 标准化开发 请求封装 统一格式处理

统一HTTP请求代码 public class HttpCode {/*** 操作成功*/public static final int SUCCESS 200;/*** 对象创建成功*/public static final int CREATED 201;/*** 请求已经被接受*/public static final int ACCEPTED 202;/*** 操作已经执行成功&#xff0c;但是没有返回数据…...

#git pull 问题:cannot lock ref ‘xxx‘: ref xxx is at (commitID) but expected ‘xxx‘

问题描述&#xff1a;git在拉取远程代码时出现该提示&#xff0c;拉取失败&#xff0c;导致该问题可能是远程有本地没有跟踪过的&#xff08;小写大写不同&#xff09;重名的分支&#xff0c;git是不区分大小写的&#xff0c;所以比如有一个的分支原先是example1.0&#xff0c;…...

JavaWeb学习打卡-Day1-分层解耦、Spring IOC、DI

三层架构 Controller&#xff08;控制层&#xff09;&#xff1a;接收前端发送的请求&#xff0c;对请求进行处理&#xff0c;并响应数据。Service&#xff08;业务逻辑层&#xff09;&#xff1a;处理具体的业务逻辑。DAO&#xff08;数据访问层/持久层&#xff09;&#xff…...

PostgesSQL外部数据封装FDW

PostgesSQL外部数据封装FDW 1. FDW外部数据配置&#xff08;单表&#xff09;1.1 远端数据库创建测试表1.2 安装扩展postges\_fdw1.3 创建外部服务SERVER1.4 创建用户映射USER MAPPING1.5 创建远程表FOREIGN TABLE1.6 数据库更新测试 2. FDW外部数据配置&#xff08;用户&#…...

redis相关问题整理

Redis 支持多种数据类型&#xff1a; 字符串 示例&#xff1a;存储用户信息 // 假设我们使用 redis-plus-plus 客户端库 auto redis Redis("tcp://127.0.0.1:6379"); redis.set("user:1000", "{name: John Doe, email: john.doeexample.com}"…...

基于 Electron、Vue3 和 TypeScript 的辅助创作工具全链路开发方案:涵盖画布系统到数据持久化的完整实现

基于 Electron、Vue3 和 TypeScript 的辅助创作工具全链路开发方案&#xff1a;涵盖画布系统到数据持久化的完整实现 引言 在数字内容创作领域&#xff0c;高效的辅助工具是连接创意与实现的关键桥梁。创作者需要一款集可视化画布、节点关系管理、数据持久化于一体的专业工具&…...

[Java · 铢积寸累] 数据结构 — 数组类型 - 增 删 改 查

&#x1f31f; 想系统化学习 Java 编程&#xff1f;看看这个&#xff1a;[编程基础] Java 学习手册 在上一章中我们介绍了如何声明与创建数组&#xff0c;还介绍了数组的基本使用方式。本章我们将在上一章的基础上&#xff0c;拓展数组的使用方式&#xff08;可能会涉及一些思…...

前端笔记-Axios

Axios学习目标 Axios与API交互1、Axios配置与使用2、请求/响应拦截器3、API设计模式&#xff08;了解RESTful风格即可&#xff09; 学习参考&#xff1a;起步 | Axios中文文档 | Axios中文网 什么是Axios Axios 是一个基于 Promise 的现代化 HTTP 客户端库&#xff0c;专…...

vue3数据响应式丢失的情况有哪些

在 Vue 3 中&#xff0c;响应式系统使用的是 Proxy 实现&#xff0c;相比 Vue 2 提升很大&#xff0c;很多 Vue 2 中的数据响应式陷阱都被解决了&#xff08;比如数组索引、新增属性等&#xff09;&#xff0c;但依然存在一些可能导致“响应式丢失”的情况。 &#x1f6a8; Vue…...

每日一练(4~23):特别数的和

算法&#xff1a;枚举 题目 题目描述 小明对数位中含有 2、0、1、9 的数字很感兴趣&#xff08;不包括前导 0&#xff09;&#xff0c;在 1 到 40 中这样的数包括 1、2、9、10 至 32、39 和 40&#xff0c;共 28 个&#xff0c;他们的和是 574。 请问&#xff0c;在 1 到 n…...

AR行业应用案例与NXP架构的结合

1. 工业巡检AR头盔 场景示例&#xff1a;宁德核电基地使用AR智能头盔进行设备巡检&#xff0c;通过实时数据叠加和远程指导&#xff0c;将工作效率提升35%。头盔需处理传感器数据、图像渲染和低延迟通信1。 NXP架构支持&#xff1a; 协处理器角色&#xff1a;NXP i.MX RT系列M…...

C# 类型、存储和变量(值类型引用类型)

本章内容 C#程序是一组类型声明 类型是一种模板 实例化类型 数据成员和函数成员 预定义类型 用户定义类型 栈和堆 值类型和引用类型 变量 静态类型和dynamic关键字 可空类型 值类型引用类型 数据项的类型定义了存储数据需要的内存大小及组成该类型的数据成员。类型还决定了对象…...

智慧校园从配电开始:AISD300为校园安全加上智能防护罩

安科瑞刘鸿鹏 摘要 随着校园用电需求不断上升及其安全保障要求的提高&#xff0c;传统低压配电系统已逐渐难以满足现代校园的安全与智能化管理需求。本文基于安科瑞电气推出的AISD300系列三相智能安全配电装置&#xff0c;探讨其在校园电力系统中的应用优势及关键技术特性。…...

如何创建极狐GitLab 议题?

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;关于中文参考文档和资料有&#xff1a; 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 创建议题 (BASIC ALL) 创建议题时&#xff0c;系统会提示您输入议题的字段。 如果您知道要分配给议题的值&#xff0c;则可…...

如何将极狐GitLab 合并请求导出为 CSV?

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;关于中文参考文档和资料有&#xff1a; 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 导出合并请求为 CSV (BASIC ALL) 将从项目的合并请求中收集的所有数据导出到逗号分隔值&#xff08;CSV&#xff09;文件中。…...

一 、环境的安装 Anaconda + Pycharm + PaddlePaddle

《从零到一实践&#xff1a;系统性学习生成式 AI(NLP)》 一 、环境的安装 Anaconda Pycharm PaddlePaddle 1. Anaconda 软件安装 Anaconda 软件安装有大量的教程&#xff0c;此处不在说明&#xff0c;安装完成之后界面如下&#xff1a; 2. 创建 Anaconda 虚拟环境 Paddl…...

十倍开发效率 - IDEA插件之 Maven Helper

0X00 先看效果 第一个选项表示存在冲突的依赖&#xff0c;可以看到图片中 mysql 的连接依赖发生了冲突&#xff0c;在低版本的上面直接右键选择 Exclude&#xff0c;冲突的依赖就被解决掉了。 0X01 安装 在 Plugins 中直接搜索 Maven Helper&#xff0c;选择第一个进行安装&am…...

Go语言和Python 3的协程对比

Go语言和Python 3都支持协程&#xff08;concurrent coroutines&#xff09;&#xff0c;但它们的实现机制、使用方式、调度方式和性能表现差异很大。下面是对比分析&#xff1a; 一、基本概念对比 特性Go 协程&#xff08;goroutine&#xff09;Python3 协程&#xff08;asyn…...