设计模式(二十四)行为型:访问者模式详解

设计模式(二十四)行为型:访问者模式详解

访问者模式(Visitor Pattern)是 GoF 23 种设计模式中最具争议性但也最强大的行为型模式之一,其核心价值在于将作用于某种数据结构中的各元素的操作分离出来,封装到一个独立的访问者对象中,使得在不改变元素类的前提下可以定义新的操作。它通过“双重分派”(Double Dispatch)机制,解决了在静态类型语言中对异构对象集合进行多态操作扩展的难题。访问者模式是构建编译器(语法树遍历)、文档处理系统、复杂报表生成、UI 渲染引擎、静态代码分析工具等系统的理想选择,是实现“开闭原则”在操作维度上的终极体现。

一、详细介绍

访问者模式解决的是“一个数据结构(如对象树或列表)包含多种类型的元素,且需要对这些元素执行多种不同的、与元素本身无关的操作,且这些操作可能频繁新增”的问题。在传统设计中,通常将操作直接定义在元素类中。这导致:

  • 违反单一职责原则:元素类承担了数据和多种操作的职责。
  • 难以扩展操作:新增操作需要修改所有元素类,违反开闭原则。
  • 代码分散:同一操作的逻辑分散在多个元素类中。

访问者模式的核心思想是:将“数据结构”与“作用于数据的操作”解耦。数据结构中的元素接受一个访问者对象作为参数,回调访问者对象中对应其类型的方法。新的操作只需添加新的访问者类,无需修改任何元素类

该模式包含以下核心角色:

  • Visitor(访问者接口):声明一组 visit() 方法,每个方法对应一种具体的元素类型(如 visit(ElementA), visit(ElementB))。它定义了所有可执行操作的抽象接口。
  • ConcreteVisitor(具体访问者):实现 Visitor 接口,为每种元素类型提供具体的操作实现。每个具体访问者代表一种独立的操作(如打印、计算、导出)。
  • Element(元素接口):声明一个 accept(Visitor) 方法,允许访问者“访问”自身。
  • ConcreteElementA, ConcreteElementB, …(具体元素):实现 Element 接口,实现 accept() 方法。在 accept() 中,调用访问者的 visit(this) 方法,将自身作为参数传入,触发正确的 visit 方法(关键:this 的静态类型是当前类,实现双重分派)。
  • ObjectStructure(对象结构):可选角色,表示包含元素的集合或复合结构(如树、列表)。它提供一个接口,允许访问者遍历其所有元素,并调用每个元素的 accept() 方法。

访问者模式的关键优势:

  • 符合开闭原则(操作维度):新增操作只需添加新的 ConcreteVisitor,无需修改 ElementConcreteElement
  • 符合单一职责原则:元素类只负责数据和 accept,操作逻辑集中在访问者中。
  • 操作集中化:同一操作的逻辑集中在单个访问者类中,易于理解、维护和复用。
  • 支持新操作:可以轻松添加如打印、统计、转换、验证等新操作。

访问者模式的关键挑战(双重分派):

  1. 第一重分派:在 ObjectStructure 中,调用 element.accept(visitor)。由于 element 是多态的,accept() 的调用会根据 element 的实际类型分派到 ConcreteElementA.accept()ConcreteElementB.accept()
  2. 第二重分派:在 ConcreteElementX.accept() 中,调用 visitor.visit(this)this静态类型ConcreteElementX,因此编译器会选择 visitor 上参数类型为 ConcreteElementXvisit 方法。即使 visitor 是多态的,visit 方法的重载选择在编译时基于 this 的静态类型确定。

缺点与限制

  • 违反里氏替换原则accept() 方法暴露了具体类型。
  • 元素类难以修改:新增元素类型需要修改所有 Visitor 接口及其所有实现类,违反开闭原则(结构维度)。
  • 复杂性高:理解双重分派和模式结构需要较高心智负担。
  • 过度设计:对于简单操作或稳定结构,可能不必要。

访问者模式适用于:

  • 数据结构稳定,但操作频繁变化(如编译器 AST)。
  • 需要对复杂对象结构执行多种不同的操作。
  • 操作需要访问元素的私有成员(访问者可通过友元或公共方法访问)。
  • 需要避免在元素类中堆积大量无关操作。

二、访问者模式的UML表示

以下是访问者模式的标准 UML 类图:

implements
implements
implements
implements
implements
calls visit()
calls visit()
calls visit()
contains
calls accept()
«interface»
Visitor
+visit(elementA: ConcreteElementA)
+visit(elementB: ConcreteElementB)
+visit(elementC: ConcreteElementC)
ConcreteVisitor1
+visit(elementA: ConcreteElementA)
+visit(elementB: ConcreteElementB)
+visit(elementC: ConcreteElementC)
ConcreteVisitor2
+visit(elementA: ConcreteElementA)
+visit(elementB: ConcreteElementB)
+visit(elementC: ConcreteElementC)
«interface»
Element
+accept(visitor: Visitor)
ConcreteElementA
+accept(visitor: Visitor)
+operationA()
ConcreteElementB
+accept(visitor: Visitor)
+operationB()
ConcreteElementC
+accept(visitor: Visitor)
+operationC()
ObjectStructure
-elements: List<Element>
+attach(element: Element)
+detach(element: Element)
+accept(visitor: Visitor)

图解说明

  • Element 接口定义 accept(Visitor)
  • ConcreteElementX 实现 accept(),内部调用 visitor.visit(this)
  • Visitor 接口为每种 ConcreteElement 声明一个 visit 重载方法。
  • ConcreteVisitor 实现所有 visit 方法,提供具体操作。
  • ObjectStructure 管理元素集合,并提供 accept(Visitor) 遍历所有元素。

三、一个简单的Java程序实例及其UML图

以下是一个文档处理系统的示例,文档包含段落(Paragraph)、图片(Image)、表格(Table)元素,需要支持打印和统计字数操作。

Java 程序实例
// 访问者接口
interface DocumentElementVisitor {void visit(Paragraph paragraph);void visit(Image image);void visit(Table table);
}// 元素接口
interface DocumentElement {void accept(DocumentElementVisitor visitor);
}// 具体元素:段落
class Paragraph implements DocumentElement {private String text;public Paragraph(String text) {this.text = text;}public String getText() {return text;}// accept 实现:回调访问者,传入自身(this)@Overridepublic void accept(DocumentElementVisitor visitor) {visitor.visit(this); // 双重分派的关键:this 是 Paragraph 类型}public void spellCheck() {System.out.println("🔍 段落拼写检查: " + text);}
}// 具体元素:图片
class Image implements DocumentElement {private String filename;private int width;private int height;public Image(String filename, int width, int height) {this.filename = filename;this.width = width;this.height = height;}public String getFilename() {return filename;}public int getWidth() {return width;}public int getHeight() {return height;}@Overridepublic void accept(DocumentElementVisitor visitor) {visitor.visit(this); // this 是 Image 类型}public void compress() {System.out.println("🗜️  压缩图片: " + filename);}
}// 具体元素:表格
class Table implements DocumentElement {private String[][] data;private int rows;private int cols;public Table(String[][] data) {this.data = data;this.rows = data.length;this.cols = data.length > 0 ? data[0].length : 0;}public String[][] getData() {return data;}public int getRows() {return rows;}public int getCols() {return cols;}@Overridepublic void accept(DocumentElementVisitor visitor) {visitor.visit(this); // this 是 Table 类型}public void validate() {System.out.println("✅ 表格数据验证: " + rows + "x" + cols + " 表格");}
}// 具体访问者:打印访问者
class PrintVisitor implements DocumentElementVisitor {@Overridepublic void visit(Paragraph paragraph) {System.out.println("🖨️  打印段落: \"" + paragraph.getText() + "\"");}@Overridepublic void visit(Image image) {System.out.println("🖼️  打印图片: " + image.getFilename() + " (" + image.getWidth() + "x" + image.getHeight() + ")");}@Overridepublic void visit(Table table) {System.out.println("📊 打印表格: " + table.getRows() + " 行, " + table.getCols() + " 列");for (int i = 0; i < table.getRows(); i++) {for (int j = 0; j < table.getCols(); j++) {System.out.print("[" + table.getData()[i][j] + "] ");}System.out.println();}}
}// 具体访问者:字数统计访问者
class WordCountVisitor implements DocumentElementVisitor {private int wordCount = 0;@Overridepublic void visit(Paragraph paragraph) {String[] words = paragraph.getText().split("\\s+");int count = words.length;wordCount += count;System.out.println("📝 段落字数: \"" + paragraph.getText() + "\" -> " + count + " 词");}@Overridepublic void visit(Image image) {// 图片无文字,不计数,但可记录System.out.println("🖼️  图片: " + image.getFilename() + " (0 词)");}@Overridepublic void visit(Table table) {int count = 0;for (int i = 0; i < table.getRows(); i++) {for (int j = 0; j < table.getCols(); j++) {if (table.getData()[i][j] != null && !table.getData()[i][j].trim().isEmpty()) {count += table.getData()[i][j].split("\\s+").length;}}}wordCount += count;System.out.println("📊 表格字数: " + count + " 词");}// 获取统计结果public int getWordCount() {return wordCount;}
}// 对象结构:文档
class Document {private java.util.List<DocumentElement> elements = new java.util.ArrayList<>();public void addElement(DocumentElement element) {elements.add(element);}public void removeElement(DocumentElement element) {elements.remove(element);}// 接受访问者,遍历所有元素public void accept(DocumentElementVisitor visitor) {for (DocumentElement element : elements) {element.accept(visitor);}}
}// 客户端使用示例
public class VisitorPatternDemo {public static void main(String[] args) {System.out.println("📄 文档处理系统 - 访问者模式示例\n");// 创建文档和元素Document document = new Document();document.addElement(new Paragraph("这是一个关于设计模式的文档。"));document.addElement(new Image("diagram.png", 800, 600));document.addElement(new Paragraph("访问者模式非常强大。"));document.addElement(new Table(new String[][]{{"模式", "类型", "用途"},{"访问者", "行为型", "分离操作"},{"策略", "行为型", "替换算法"}}));document.addElement(new Paragraph("总结:访问者模式适用于稳定结构。"));// 使用打印访问者System.out.println("--- 执行打印操作 ---");PrintVisitor printVisitor = new PrintVisitor();document.accept(printVisitor); // 遍历元素,触发 accept -> visitSystem.out.println("\n--- 执行字数统计操作 ---");WordCountVisitor wordCountVisitor = new WordCountVisitor();document.accept(wordCountVisitor);System.out.println("📊 文档总字数: " + wordCountVisitor.getWordCount() + " 词");// 演示新增操作无需修改元素类System.out.println("\n--- 新增操作:元素类型检查 ---");// 只需定义新访问者class TypeCheckVisitor implements DocumentElementVisitor {@Overridepublic void visit(Paragraph paragraph) {System.out.println("✅ 元素: 段落, 内容长度: " + paragraph.getText().length());}@Overridepublic void visit(Image image) {System.out.println("✅ 元素: 图片, 文件: " + image.getFilename() + ", 尺寸: " + image.getWidth() + "x" + image.getHeight());}@Overridepublic void visit(Table table) {System.out.println("✅ 元素: 表格, 大小: " + table.getRows() + "x" + table.getCols());}}TypeCheckVisitor typeCheckVisitor = new TypeCheckVisitor();document.accept(typeCheckVisitor);}
}
实例对应的UML图(简化版)
implements
implements
implements
implements
implements
implements
calls visit()
calls visit()
calls visit()
contains
calls accept()
«interface»
DocumentElementVisitor
+visit(paragraph: Paragraph)
+visit(image: Image)
+visit(table: Table)
PrintVisitor
+visit(paragraph: Paragraph)
+visit(image: Image)
+visit(table: Table)
WordCountVisitor
-wordCount: int
+visit(paragraph: Paragraph)
+visit(image: Image)
+visit(table: Table)
+getWordCount()
TypeCheckVisitor
+visit(paragraph: Paragraph)
+visit(image: Image)
+visit(table: Table)
«interface»
DocumentElement
+accept(visitor: DocumentElementVisitor)
Paragraph
-text: String
+accept(visitor: DocumentElementVisitor)
+getText()
Image
-filename: String
-width: int
-height: int
+accept(visitor: DocumentElementVisitor)
+getFilename()
Table
-data: String[][]
+accept(visitor: DocumentElementVisitor)
+getData()
Document
-elements: List<DocumentElement>
+addElement(element: DocumentElement)
+accept(visitor: DocumentElementVisitor)

运行说明

  • DocumentElement 定义 accept()
  • Paragraph, Image, Table 实现 accept(),内部调用 visitor.visit(this)
  • DocumentElementVisitor 为每种元素声明 visit 重载。
  • PrintVisitor, WordCountVisitor 实现 visit 方法,提供具体操作。
  • Documentaccept() 遍历所有元素,调用其 accept()
  • 新增 TypeCheckVisitor 无需修改任何元素类,完美体现开闭原则。

四、总结

特性说明
核心目的分离数据结构与操作,支持在不修改元素的情况下新增操作
实现机制双重分派:元素 accept 访问者,访问者 visit 元素
优点符合开闭原则(操作维度)、操作集中化、支持新操作、符合单一职责
缺点违反里氏替换、元素新增困难(违反开闭原则-结构维度)、复杂性高、依赖具体类型
适用场景稳定数据结构(如AST)、多操作需求、编译器、文档处理、报表生成
不适用场景结构频繁变化、操作简单、避免继承/重载的语言

访问者模式使用建议

  • 仅在数据结构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/91372.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/91372.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

USRP X440 和USRP X410 直接RF采样架构的优势

USRP X440 和USRP X410 直接RF采样架构的优势概述什么是直接RF采样&#xff1f;如何实现直接采样&#xff1f;什么情况下应考虑使用直接RF采样架构&#xff1f;概述 转换器技术每年都在发展。主要半导体公司的模数转换器(ADC)和数模转换器(DAC)的采样速率比十年前的产品快了好…

P4568 [JLOI2011] 飞行路线

P4568 [JLOI2011] 飞行路线 题目描述 Alice 和 Bob 现在要乘飞机旅行&#xff0c;他们选择了一家相对便宜的航空公司。该航空公司一共在 nnn 个城市设有业务&#xff0c;设这些城市分别标记为 000 到 n−1n-1n−1&#xff0c;一共有 mmm 种航线&#xff0c;每种航线连接两个城市…

MySQL 中的聚簇索引和非聚簇索引的区别

MySQL 中的聚簇索引和非聚簇索引的区别 总结性回答 聚簇索引和非聚簇索引的主要区别在于索引的组织方式和数据存储位置。聚簇索引决定了表中数据的物理存储顺序&#xff0c;一个表只能有一个聚簇索引&#xff1b;而非聚簇索引是独立于数据存储的额外结构&#xff0c;一个表可以…

全局异常处理,可以捕捉到过滤器中的异常吗?

全局异常处理,可以捕捉到过滤器中的异常吗? 全局异常处理器(如Spring的@ControllerAdvice+@ExceptionHandler)默认无法直接捕获过滤器(Filter)中抛出的异常,这是由过滤器和Spring MVC的执行顺序及职责边界决定的。具体原因和解决方案如下: 一、为什么全局异常处理器默…

市政道路积水监测系统:守护城市雨天出行安全的 “智慧防线”

市政道路积水监测系统&#xff1a;守护城市雨天出行安全的 “智慧防线”柏峰【BF-DMJS】每逢汛期&#xff0c;强降雨引发的城市道路积水问题&#xff0c;不仅会造成交通拥堵&#xff0c;更可能危及行人和车辆安全&#xff0c;成为困扰城市管理的一大难题。传统的积水监测主要依…

搭建HAProxy高可用负载均衡系统

一、HAProxy简介Haproxy 是一个使用C语言编写的自由及开放源代码软件&#xff0c;其提供高可用性、负载均衡&#xff0c;以及基于TCP和HTTP的应用程序代理。haproxy优点 1. Haproxy支持两种代理模式 TCP&#xff08;四层&#xff09;和HTTP&#xff08;七层&#xff09;&#x…

GO语言 go get 下载 下来的包存放在哪里

在 Go 中&#xff0c;通过 go get&#xff08;或 Go Modules 下的自动下载&#xff09;获取的第三方包&#xff0c;具体存储位置取决于你是否启用了 Go Modules&#xff08;推荐方式&#xff09;。✅ 1. 如果你使用了 Go Modules&#xff08;Go 1.11 默认开启&#xff09;当前 …

PostgreSQL 14.4 ARM64 架构源码编译安装指南

PostgreSQL 14.4 ARM64 架构源码编译安装指南文章目录PostgreSQL 14.4 ARM64 架构源码编译安装指南说明环境要求操作系统1. 系统环境准备1.1 更新系统包1.2 创建 PostgreSQL 用户2. 解压 PostgreSQL 14.4 源码包3. 配置编译选项4. 编译源代码5. 安装 PostgreSQL6. 初始化数据库…

【科普】在STM32中有哪些定时器?

在 STM32 单片机中&#xff0c;定时器种类丰富&#xff0c;不同系列&#xff08;如 F1、F4、H7 等&#xff09;略有差异&#xff0c;以下是常见的定时器类型及核心特点&#xff1a;1. 基本定时器&#xff08;TIM6、TIM7&#xff09;功能&#xff1a;仅具备定时计数功能&#xf…

git使用秘诀(详解0到1)

前言&#xff1a; 不知道大家有没有使用git提交代码或者拉取代码的经历&#xff0c;自从上一家公司实习结束以后&#xff0c;对git的使用历历在目&#xff0c;从一开始的add、commit到后来的pull都有着许多的疑惑。 自从有一次merge代码以后&#xff0c;被师兄批了一顿以后(不小…

RHEL 9.5 离线安装 Ansible 完整教程

文章目录RHEL 9.5 离线安装 Ansible 完整教程环境准备系统要求准备工作清单方法一&#xff1a;使用 RPM 包离线安装步骤 1&#xff1a;在联网机器上下载必要的 RPM 包步骤 2&#xff1a;创建本地仓库元数据步骤 3&#xff1a;在离线服务器上安装方法二&#xff1a;使用 Python …

44、鸿蒙HarmonyOS Next开发:视频播放 (Video)组件和进度条 (Progress)组件的使用

目录 视频播放 (Video) 创建视频组件 加载视频资源 加载本地视频 加载沙箱路径视频 加载网络视频 添加属性 事件调用 Video控制器使用 其他说明 示例代码 进度条 (Progress) 创建进度条 设置进度条样式 场景示例 视频播放 (Video) Video组件用于播放视频文件并…

6、微服务架构常用十种设计模式

目录 1、微服务架构 2、微服务架构的优点 3、微服务架构的缺点 4、何时使用微服务架构 5、微服务架构常用十种设计模式 ① 独享数据库&#xff08;Database per Microservice&#xff09; ② 事件源&#xff08;Event Sourcing&#xff09; ③ 命令和查询职责分离&…

Docker 初学者需要了解的几个知识点 (六):docker-compose.yml (ThinkPHP)

下面这个文 docker-compose.yml 文件定义了一个包含 PHP、Nginx、MySQL、Redis 的完整 ThinkPHP 开发环境&#xff0c;各配置项的含义如下&#xff1a;version: 3.8services:# PHP-FPM 服务php-fpm:image: php:8.1-fpmvolumes:- ./tp-demo:/var/www/html- ./php.ini:/usr/local…

TiDB 详解

TiDB 详解&#xff1a;架构、特性与应用实践 TiDB 是 PingCAP 公司开发的开源分布式 NewSQL 数据库&#xff0c;采用 “计算-存储分离” 架构设计&#xff0c;兼具传统关系型数据库的 ACID 事务特性和 NoSQL 系统的水平扩展能力。以下是 TiDB 的全面技术解析。一、核心架构设计…

推客小程序商业模型设计:合规分佣体系×盈利模式×LTV提升策略

一、推客小程序的市场背景与商业价值在当今移动互联网红利逐渐消退的背景下&#xff0c;社交电商正成为流量增长的新突破口。推客小程序作为一种基于社交关系的分销工具&#xff0c;完美融合了社交传播与电商变现的双重优势&#xff0c;为企业和个人创业者提供了全新的商业机会…

Matlab处理多个循环的判断的方式:

1、使用正则表达式&#xff1a;pattern strcat(\b, strjoin(tuple, \b|\b), \b);% 4. 逐行处理文件内容 modifiedContents {}; % 存储修改后的内容 for i 1:length(fileContents)line fileContents{i};% 使用正则表达式检查当前行是否包含元组中的任何元素if ~isempty(reg…

从字符串中“薅出”最长子串:LeetCode 340 Swift 解法全解析

文章目录摘要描述题解答案题解代码分析详细解析&#xff1a;示例测试及结果结果解释&#xff1a;时间复杂度总结摘要 在日常开发中&#xff0c;我们经常需要处理字符串&#xff0c;比如分析用户输入、文本挖掘、数据清洗等等。而这道题就特别实用&#xff1a;如何找到一个字符…

时序数据库厂商 TDengine 发布 AI 原生的工业数据管理平台 IDMP,“无问智推”改变数据消费范式

在工业企业越来越依赖数据驱动决策的今天&#xff0c;数据的获取不再是难题&#xff0c;难的是从纷繁复杂的数据中提炼出有用的信息。而 AI 的崛起&#xff0c;正在重塑整个数据分析的逻辑。 7 月 29 日晚&#xff0c;TDengine 发布了一款全新产品 —— TDengine IDMP&#xf…

HBase、MongoDB 和 Redis 的区别详解

这三者都是流行的 NoSQL 数据库&#xff0c;但设计目标、数据模型和适用场景有显著差异。以下是它们的核心对比&#xff1a; 1. 数据模型对比特性HBaseMongoDBRedis数据模型宽列存储&#xff08;类似 BigTable&#xff09;文档存储&#xff08;BSON/JSON&#xff09;键值存储&a…