面對穩定的類階層結構,新需求頻繁要求在各類別中新增操作。若直接修改現有類別,違反開閉原則,且導致邏輯分散、維護困難。訪問者模式透過雙重分派機制,將操作從資料結構中分離,實現「資料結構不變、操作可擴展」的設計目標。本文深度剖析模式結構、核心機制、實戰範例與適用場景。
假設有 Document 抽象類別,衍生 Paragraph、Image、Table。需求依序出現:
toHtml()wordCount()spellCheck()結果:每個新操作都要修改所有類別,違反單一職責、開閉原則,且操作邏輯散落在各類別中,難以復用與測試。
| 角色 | 職責 | 關鍵方法 |
|---|---|---|
| Visitor (介面) | 宣告針對每種 ConcreteElement 的訪問操作 | visit(ConcreteElementA), visit(ConcreteElementB)... |
| ConcreteVisitor | 實作具體操作邏輯(如 HTML導出、字數統計) | 實作各 visit() 方法 |
| Element (介面) | 定義 accept(Visitor),讓訪問者「拜訪」自己 | accept(Visitor v) |
| ConcreteElement | 實作 accept,呼叫 v.visit(this) | v.visit(this) — 關鍵雙重分派點 |
| ObjectStructure | 管理元素集合,提供遍歷入口 | elements() 返回 Iterator/Stream |
// 客戶端代碼
Element element = structure.getElement(); // 靜態類型 Element,動態類型 Paragraph
Visitor visitor = new HtmlExportVisitor(); // 靜態/動態類型均為 HtmlExportVisitor
element.accept(visitor); // 第一次分派:動態綁定到 Paragraph.accept()
// Paragraph.accept(Visitor v) 實作:
public void accept(Visitor v) {
v.visit(this); // 第二次分派:動態綁定到 HtmlExportVisitor.visit(Paragraph)
}
兩次動態分派鎖定「具體元素類型 × 具體訪問者類型」的唯一組合,實現靜態類型系統無法直接表達的多態。
interface DocumentElement {
void accept(DocumentVisitor visitor);
}
class Paragraph implements DocumentElement {
private final String text;
public Paragraph(String text) { this.text = text; }
public String getText() { return text; }
@Override public void accept(DocumentVisitor v) { v.visit(this); }
}
class Image implements DocumentElement {
private final String src, alt;
public Image(String src, String alt) { this.src = src; this.alt = alt; }
public String getSrc() { return src; }
public String getAlt() { return alt; }
@Override public void accept(DocumentVisitor v) { v.visit(this); }
}
class Table implements DocumentElement {
private final List> rows;
public Table(List> rows) { this.rows = rows; }
public List> getRows() { return rows; }
@Override public void accept(DocumentVisitor v) { v.visit(this); }
}
interface DocumentVisitor {
void visit(Paragraph p);
void visit(Image img);
void visit(Table table);
}
// 導出 HTML
class HtmlExportVisitor implements DocumentVisitor {
private final StringBuilder html = new StringBuilder();
@Override public void visit(Paragraph p) { html.append("").append(p.getText()).append("
\n"); }
@Override public void visit(Image img) { html.append("
\n"); }
@Override public void visit(Table t) {
html.append("| ").append(cell).append(" | "); html.append("
class Document {
private final List elements = new ArrayList<>();
public void add(DocumentElement e) { elements.add(e); }
public void accept(DocumentVisitor v) { elements.forEach(e -> e.accept(v)); }
}
// 使用
Document doc = new Document();
doc.add(new Paragraph("Hello Visitor Pattern"));
doc.add(new Image("chart.png", "Q3 Sales"));
doc.add(new Table(List.of(List.of("A","B"), List.of("1","2"))));
HtmlExportVisitor htmlVisitor = new HtmlExportVisitor();
doc.accept(htmlVisitor);
System.out.println(htmlVisitor.getHtml());
WordCountVisitor countVisitor = new WordCountVisitor();
doc.accept(countVisitor);
System.out.println("Word count: " + countVisitor.getCount());
| 優點 | 缺點 |
|---|---|
| 新增操作不修改元素類別(開閉原則) | 新增元素類別需修改 Visitor 介面及所有實作(破壞元素擴展性) |
| 同類操作集中在一個 Visitor 中,內聚性高 | 訪問者需存取元素內部狀態,可能破壞封裝(需提供 getter) |
| 可跨類別聚合狀態(如統計全文字數) | 引入額外類別與間接呼叫,增加複雜度 |
| 支援多種遍歷策略(深度/廣度/條件) | 不適用於元素類別頻繁變動的場景 |
Java 8+ 可用 Function + instanceof 模式匹配(JDK 17+)簡化:
// 函數式訪問者(適用於簡單場景) interface DocVisitor{ R visit(Paragraph p); R visit(Image img); R visit(Table t); } // 使用 lambda DocVisitor htmlVisitor = new DocVisitor<>() { public String visit(Paragraph p) { return " " + p.getText() + "
"; } public String visit(Image img) { return ""; } public String visit(Table t) { /* ... */ } };
訪問者模式是「操作與資料結構分離」的經典答案。透過雙重分派,它在靜態類型系統中實現了動態多態的擴展性。核心取捨:犧牲元素擴展性,換取操作擴展性。適用於編譯器 AST 處理、文檔模型操作、圖形結構分析等「結構穩定、操作多變」領域。現代 Java 的模式匹配與函數式特性為輕量級訪問提供了新選項,但核心設計哲學永不過時:識別變化維度,隔離變化點。