設計模式(二十四)行為型:訪問者模式詳解

設計模式(二十四)行為型:訪問者模式詳解

訪問者模式(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
英文地址,請注明出處:http://en.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…