訪問者模式(Visitor Pattern)詳解
一、訪問者模式簡介
訪問者模式(Visitor Pattern) 是一種 行為型設計模式(對象行為型模式),它允許你在不修改對象結構的前提下,為對象結構中的元素添加新的操作。
你可以這樣理解:
“有一個公司組織結構圖(包含多個部門、員工),現在你想分別計算工資總額、打印員工名單、生成報表等不同功能。如果每次都要修改每個類來支持新功能,會非常麻煩。而訪問者模式就像請來不同的‘專家’(訪問者)——一個財務專家算工資,一個人事專家做花名冊——他們各自去‘訪問’每個員工并完成自己的任務。”
核心思想是:將數據結構與作用于結構上的操作分離。
它為操作存儲不同類型元素的對象結構提供了一種解決方案。
用戶可以對不同類型的元素施加不同的操作。
訪問者模式包含以下5個角色:
Visitor(抽象訪問者)
ConcreteVisitor(具體訪問者)
Element(抽象元素)
ConcreteElement(具體元素)
ObjectStructure(對象結構)
二、解決的問題類型
訪問者模式主要用于解決以下問題:
- 需要對一個復雜的對象結構(如樹、列表)執行多種不同的操作,且這些操作可能會頻繁增加。
- 不想修改現有類來添加新功能(避免破壞封裝性或違反開閉原則)。
- 希望將相關操作集中在一個類中(即訪問者),而不是分散在各個數據類中。
三、使用場景
場景 | 示例 |
---|---|
編譯器設計 | 抽象語法樹(AST)的解析、類型檢查、代碼生成等 |
文檔處理系統 | 對文檔中的段落、圖片、表格進行渲染、導出PDF、統計字數等 |
UI組件樹操作 | 遍歷組件樹進行布局、繪制、事件分發等 |
報表生成 | 對一組對象進行匯總、分析、生成統計報告 |
一個對象結構包含多個類型的對象,希望對這些對象實施一些依賴其具體類型的操作。
需要對一個對象結構中的對象進行很多不同的且不相關的操作,并需要避免讓這些操作“污染”這些對象的類,也不希望在增加新操作時修改這些類。
對象結構中對象對應的類很少改變,但經常需要在此對象結構上定義新的操作。
四、核心概念
- Visitor(訪問者接口):定義對每種元素的訪問方法,如
visit(ElementA)
、visit(ElementB)
。 - ConcreteVisitor(具體訪問者):實現訪問者接口,完成具體操作(如打印、計算、導出等)。
- Element(元素接口):定義
accept(Visitor)
方法,用于接收訪問者。 - ConcreteElement(具體元素):實現
accept
方法,調用訪問者的對應visit
方法。 - ObjectStructure(對象結構):如集合、樹等,能枚舉元素并讓訪問者遍歷它們。
五、實際代碼案例(Java)
我們以一個“文檔編輯器”為例,文檔包含文本段落和圖片,我們需要實現“渲染”和“導出為HTML”兩種操作。
1. 定義訪問者接口
// 訪問者接口
interface DocumentVisitor {void visit(Paragraph paragraph);void visit(Image image);
}
2. 定義元素接口
// 元素接口
interface DocumentElement {void accept(DocumentVisitor visitor);
}
3. 創建具體元素類
// 段落元素
class Paragraph implements DocumentElement {private String content;public Paragraph(String content) {this.content = content;}public String getContent() {return content;}@Overridepublic void accept(DocumentVisitor visitor) {visitor.visit(this); // 反向調用訪問者,傳入自己}
}// 圖片元素
class Image implements DocumentElement {private String url;public Image(String url) {this.url = url;}public String getUrl() {return url;}@Overridepublic void accept(DocumentVisitor visitor) {visitor.visit(this);}
}
4. 創建具體訪問者類
// 渲染訪問者
class RenderVisitor implements DocumentVisitor {@Overridepublic void visit(Paragraph paragraph) {System.out.println("🖥? 渲染段落: " + paragraph.getContent());}@Overridepublic void visit(Image image) {System.out.println("🖼? 渲染圖片: [顯示圖片 " + image.getUrl() + "]");}
}// 導出為HTML訪問者
class HtmlExportVisitor implements DocumentVisitor {private StringBuilder html = new StringBuilder();@Overridepublic void visit(Paragraph paragraph) {html.append("<p>").append(paragraph.getContent()).append("</p>\n");}@Overridepublic void visit(Image image) {html.append("<img src=\"").append(image.getUrl()).append("\" />\n");}public String getHtml() {return html.toString();}
}
5. 創建對象結構(文檔)
import java.util.ArrayList;
import java.util.List;// 文檔結構(對象結構)
class Document {private List<DocumentElement> elements = new ArrayList<>();public void addElement(DocumentElement element) {elements.add(element);}// 接受訪問者遍歷所有元素public void accept(DocumentVisitor visitor) {for (DocumentElement element : elements) {element.accept(visitor);}}
}
6. 客戶端測試類
public class Client {public static void main(String[] args) {Document doc = new Document();doc.addElement(new Paragraph("歡迎使用我們的文檔系統。"));doc.addElement(new Image("https://example.com/logo.png"));doc.addElement(new Paragraph("這是一個示例文檔。"));System.out.println("=== 渲染文檔 ===");RenderVisitor renderVisitor = new RenderVisitor();doc.accept(renderVisitor);System.out.println("\n=== 導出為HTML ===");HtmlExportVisitor htmlVisitor = new HtmlExportVisitor();doc.accept(htmlVisitor);System.out.println(htmlVisitor.getHtml());}
}
輸出結果:
=== 渲染文檔 ===
🖥? 渲染段落: 歡迎使用我們的文檔系統。
🖼? 渲染圖片: [顯示圖片 https://example.com/logo.png]
🖥? 渲染段落: 這是一個示例文檔。=== 導出為HTML ===
<p>歡迎使用我們的文檔系統。</p>
<img src="https://example.com/logo.png" />
<p>這是一個示例文檔。</p>
典型代碼
典型的抽象訪問者類代碼:
abstract class Visitor
{public abstract void Visit(ConcreteElementA elementA);public abstract void Visit(ConcreteElementB elementB);public void Visit(ConcreteElementC elementC){//元素ConcreteElementC的操作代碼}
}
典型的具體訪問者類代碼:
class ConcreteVisitor : Visitor
{
public override void Visit(ConcreteElementA elementA) {//元素ConcreteElementA的操作代碼}public override void Visit(ConcreteElementB elementB) {//元素ConcreteElementB的操作代碼}
}
典型的抽象元素類代碼:
interface Element
{void Accept(Visitor visitor);
}
典型的具體元素類代碼:
class ConcreteElementA : Element
{public void Accept(Visitor visitor) {visitor.Visit(this);}public void OperationA() {//業務方法}
}
典型的對象結構代碼:
using System;
using System.Collections.Generic;
class ObjectStructure
{private List<Element> list = new List<Element>(); //定義一個集合用于存儲元素對象
//接受訪問者的訪問操作
public void Accept(Visitor visitor)
{
foreach (Object obj in list){((Element)obj).Accept(visitor); //遍歷訪問集合中的每一個元素
}
}public void AddElement(Element element){list.Add(element);}
public void RemoveElement(Element element){list.Remove(element);}
}
訪問者模式的結構與實現
雙重分派機制
(1) 調用具體元素類的Accept(Visitor visitor)方法,并將Visitor子類對象作為其參數
(2) 在具體元素類Accept(Visitor visitor)方法內部調用傳入的Visitor對象的Visit()方法,例如Visit(ConcreteElementA elementA),將當前具體元素類對象(this)作為參數,例如visitor.Visit(this)
(3) 執行Visitor對象的Visit()方法,在其中還可以調用具體元素對象的業務方法
ConcreteElementA. Accept(Visitor visitor)↓
ConcreteVisitorA. Visit(ConcreteElementA elementA)<ConcreteVisitorA. Visit(this)>↓
ConcreteElementA. OperationA()
其他案例
- 某公司OA系統中包含一個員工信息管理子系統,該公司員工包括正式員工和臨時工,每周人力資源部和財務部等部門需要對員工數據進行匯總,匯總數據包括員工工作時間、員工工資等。該公司基本制度如下:
(1) 正式員工每周工作時間為40小時,不同級別、不同部門的員工每周基本工資不同;如果超過40小時,超出部分按照100元/小時作為加班費;如果少于40小時,所缺時間按照請假處理,請假所扣工資以80元/小時計算,直到基本工資扣除到零為止。除了記錄實際工作時間外,人力資源部需記錄加班時長或請假時長,作為員工平時表現的一項依據。
(2) 臨時工每周工作時間不固定,基本工資按小時計算,不同崗位的臨時工小時工資不同。人力資源部只需記錄實際工作時間。
人力資源部和財務部工作人員可以根據各自的需要對員工數據進行匯總處理,人力資源部負責匯總每周員工工作時間,而財務部負責計算每周員工工資。
現使用訪問者模式設計該系統,繪制類圖。
- 購物車
顧客在超市中將選擇的商品,如蘋果、圖書等放在購物車中,然后到收銀員處付款。在購物過程中,顧客需要對這些商品進行訪問,以便確認這些商品的質量,之后收銀員計算價格時也需要訪問購物車內顧客所選擇的商品。此時,購物車作為一個ObjectStructure(對象結構)用于存儲各種類型的商品,而顧客和收銀員作為訪問這些商品的訪問者,他們需要對商品進行檢查和計價。不同類型的商品其訪問形式也可能不同,如蘋果需要過秤之后再計價,而圖書不需要。使用訪問者模式來設計該購物過程。
- 獎勵審批系統
某高校獎勵審批系統可以實現教師獎勵和學生獎勵的審批(AwardCheck),如果教師發表論文數超過10篇或者學生論文超過2篇可以評選科研獎,如果教師教學反饋分大于等于90分或者學生平均成績大于等于90分可以評選成績優秀獎,使用訪問者模式設計該系統,以判斷候選人集合中的教師或學生是否符合某種獲獎要求。
設計結構
六、優缺點分析
優點 | 描述 |
---|---|
? 符合開閉原則 | 新增操作(訪問者)無需修改現有元素類 |
? 職責分離 | 將相關操作集中到訪問者類中,提高內聚性 |
? 便于擴展新操作 | 添加新功能只需新增一個訪問者類 |
缺點 | 描述 |
---|---|
? 增加新元素類困難 | 每新增一個元素類型,所有訪問者都要修改接口并實現新方法(違反開閉原則) |
? 破壞封裝性 | 訪問者可能需要訪問元素的內部狀態 |
? 代碼復雜度高 | 引入較多類和雙向調用,理解成本較高 |
? 性能開銷 | 多態調用和遞歸可能導致性能下降 |
七、與其它模式對比
模式 | 與訪問者模式的區別 |
---|---|
策略模式 | 策略是替換算法,訪問者是擴展操作 |
觀察者模式 | 觀察者是事件通知,訪問者是主動遍歷 |
迭代器模式 | 迭代器用于遍歷,訪問者用于操作+遍歷 |
八、最終小結
訪問者模式是一種強大但使用場景有限的設計模式,它特別適合那些數據結構相對穩定,但需要在其上執行多種不同操作的系統。
作為一名 Java 開發工程師,你可能會在以下場景中遇到它:
- 編譯器、解釋器等語言處理工具;
- 復雜的數據模型需要多種展示或處理方式;
- 報表、導出、統計等橫切功能較多的系統。
但也要注意:如果元素類型經常變化,訪問者模式會變得難以維護。因此,它更適合“操作多、結構穩”的場景。
📌 一句話總結:
訪問者模式就像“外聘專家團隊”,他們帶著各自的工具(操作),去訪問公司里的各個部門(元素),完成專業任務,而無需改變公司原有結構。
? 建議使用時機:
- 對象結構穩定,但操作頻繁擴展;
- 多種操作需要集中管理;
- 愿意接受一定的代碼復雜度換取靈活性。
以上內部部分由AI大模型生成,注意識別!