設計模式(二十四)行為型:訪問者模式詳解
訪問者模式(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
,無需修改Element
或ConcreteElement
。 - 符合單一職責原則:元素類只負責數據和
accept
,操作邏輯集中在訪問者中。 - 操作集中化:同一操作的邏輯集中在單個訪問者類中,易于理解、維護和復用。
- 支持新操作:可以輕松添加如打印、統計、轉換、驗證等新操作。
訪問者模式的關鍵挑戰(雙重分派):
- 第一重分派:在
ObjectStructure
中,調用element.accept(visitor)
。由于element
是多態的,accept()
的調用會根據element
的實際類型分派到ConcreteElementA.accept()
或ConcreteElementB.accept()
。 - 第二重分派:在
ConcreteElementX.accept()
中,調用visitor.visit(this)
。this
的靜態類型是ConcreteElementX
,因此編譯器會選擇visitor
上參數類型為ConcreteElementX
的visit
方法。即使visitor
是多態的,visit
方法的重載選擇在編譯時基于this
的靜態類型確定。
缺點與限制:
- 違反里氏替換原則:
accept()
方法暴露了具體類型。 - 元素類難以修改:新增元素類型需要修改所有
Visitor
接口及其所有實現類,違反開閉原則(結構維度)。 - 復雜性高:理解雙重分派和模式結構需要較高心智負擔。
- 過度設計:對于簡單操作或穩定結構,可能不必要。
訪問者模式適用于:
- 數據結構穩定,但操作頻繁變化(如編譯器 AST)。
- 需要對復雜對象結構執行多種不同的操作。
- 操作需要訪問元素的私有成員(訪問者可通過友元或公共方法訪問)。
- 需要避免在元素類中堆積大量無關操作。
二、訪問者模式的UML表示
以下是訪問者模式的標準 UML 類圖:
圖解說明:
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圖(簡化版)
運行說明:
DocumentElement
定義accept()
。Paragraph
,Image
,Table
實現accept()
,內部調用visitor.visit(this)
。DocumentElementVisitor
為每種元素聲明visit
重載。PrintVisitor
,WordCountVisitor
實現visit
方法,提供具體操作。Document
的accept()
遍歷所有元素,調用其accept()
。- 新增
TypeCheckVisitor
無需修改任何元素類,完美體現開閉原則。
四、總結
特性 | 說明 |
---|---|
核心目的 | 分離數據結構與操作,支持在不修改元素的情況下新增操作 |
實現機制 | 雙重分派:元素 accept 訪問者,訪問者 visit 元素 |
優點 | 符合開閉原則(操作維度)、操作集中化、支持新操作、符合單一職責 |
缺點 | 違反里氏替換、元素新增困難(違反開閉原則-結構維度)、復雜性高、依賴具體類型 |
適用場景 | 穩定數據結構(如AST)、多操作需求、編譯器、文檔處理、報表生成 |
不適用場景 | 結構頻繁變化、操作簡單、避免繼承/重載的語言 |
訪問者模式使用建議:
- 僅在數據結構