首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

訪問者模式深度解析:在不修改結構前提下擴展行為

     編輯:Java 程式設計入門

面對穩定的類階層結構,新需求頻繁要求在各類別中新增操作。若直接修改現有類別,違反開閉原則,且導致邏輯分散、維護困難。訪問者模式透過雙重分派機制,將操作從資料結構中分離,實現「資料結構不變、操作可擴展」的設計目標。本文深度剖析模式結構、核心機制、實戰範例與適用場景。

核心痛點:操作與結構的耦合

假設有 Document 抽象類別,衍生 Paragraph、Image、Table。需求依序出現:

  1. 導出 HTML → 各類別新增 toHtml()
  2. 字數統計 → 各類別新增 wordCount()
  3. 拼寫檢查 → 各類別新增 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("").append(img.getAlt()).append("\n"); } @Override public void visit(Table t) { html.append("\n"); for (List row : t.getRows()) { html.append(""); for (String cell : row) html.append(""); html.append("\n"); } html.append("
").append(cell).append("
\n"); } public String getHtml() { return html.toString(); } } // 字數統計 class WordCountVisitor implements DocumentVisitor { private int count = 0; @Override public void visit(Paragraph p) { count += p.getText().split("\\s+").length; } @Override public void visit(Image img) { /* 圖片不計字 */ } @Override public void visit(Table t) { for (List r : t.getRows()) for (String c : r) count += c.split("\\s+").length; } public int getCount() { return count; } }

物件結構與客戶端使用

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)
可跨類別聚合狀態(如統計全文字數)引入額外類別與間接呼叫,增加複雜度
支援多種遍歷策略(深度/廣度/條件)不適用於元素類別頻繁變動的場景

適用場景判斷清單

  • ✅ 物件結構穩定(元素類別很少新增)
  • ✅ 需頻繁新增操作(導出、驗證、分析、轉換...)
  • ✅ 操作需跨多種元素類型聚合狀態
  • ✅ 操作邏輯複雜,需獨立測試、維護
  • ❌ 元素類別頻繁變動 → 改用解釋器模式或直接在元素中實作
  • ❌ 簡單場景(1-2 操作、2-3 元素) → 直接多態或 switch 更簡潔

現代 Java 變體:函數式訪問者

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 的模式匹配與函數式特性為輕量級訪問提供了新選項,但核心設計哲學永不過時:識別變化維度,隔離變化點。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved