設計模式:訪客模式 (Visitor)

8 分鐘
軟體設計設計模式OOP

Visitor 把操作和它所作用的物件分離開來。你透過撰寫新的訪客來加入新行為,而不是去修改那些 element 類別。


意圖與用途

假設有一個檔案系統,包含 FolderTextFileImageFile 等節點。現在你需要實作多種匯出格式:PDF、HTML、Markdown。

如果把每一種匯出方法直接塞進各個節點類別,那每新增一種格式,就得修改每一個節點類 —— OCP 的角度來看,這是個問題。

Visitor 把操作抽離出來:每一種匯出格式都是一個 Visitor,它知道怎麼處理每一種節點。


結構與角色

  • Visitor:為每一種節點型別宣告一個 visit 方法的介面
  • ConcreteVisitor:實作具體操作 (PDF 匯出器、HTML 匯出器……)
  • Element:宣告 accept(visitor) 的介面
  • ConcreteElement:呼叫 `visitor.visitXxx(this) —— 自己傳進去

實作範例:文件匯出訪客

TypeScript
// Element 介面
interface DocumentElement {
  accept(visitor: ExportVisitor): string;
  name: string;
}

// Visitor 介面
interface ExportVisitor {
  visitFolder(folder: Folder): string;
  visitTextFile(file: TextFile): string;
  visitImageFile(file: ImageFile): string;
}

// ConcreteElement:資料夾
class Folder implements DocumentElement {
  name: string;
  children: DocumentElement[] = [];

  constructor(name: string) { this.name = name; }

  add(element: DocumentElement): void {
    this.children.push(element);
  }

  accept(visitor: ExportVisitor): string {
    return visitor.visitFolder(this);
  }
}

// ConcreteElement:文字檔
class TextFile implements DocumentElement {
  constructor(public name: string, public content: string) {}

  accept(visitor: ExportVisitor): string {
    return visitor.visitTextFile(this);
  }
}

// ConcreteElement:圖片檔
class ImageFile implements DocumentElement {
  constructor(public name: string, public width: number, public height: number) {}

  accept(visitor: ExportVisitor): string {
    return visitor.visitImageFile(this);
  }
}

// ConcreteVisitor:HTML 匯出
class HtmlExportVisitor implements ExportVisitor {
  visitFolder(folder: Folder): string {
    const children = folder.children.map(c => c.accept(this)).join('');
    return `
${children}
`; } visitTextFile(file: TextFile): string { return `

${file.name}

${file.content}

`; } visitImageFile(file: ImageFile): string { return ``; } } // ConcreteVisitor:Markdown 匯出 class MarkdownExportVisitor implements ExportVisitor { visitFolder(folder: Folder): string { const children = folder.children.map(c => c.accept(this)).join('\n'); return `## ${folder.name}\n${children}`; } visitTextFile(file: TextFile): string { return `### ${file.name}\n${file.content}`; } visitImageFile(file: ImageFile): string { return `![${file.name}](${file.name})`; } } // 使用 const root = new Folder('docs'); root.add(new TextFile('README.md', '歡迎使用')); root.add(new ImageFile('logo.png', 200, 100)); const htmlVisitor = new HtmlExportVisitor(); console.log(root.accept(htmlVisitor)); const mdVisitor = new MarkdownExportVisitor(); console.log(root.accept(mdVisitor));

適用情境

適用時機

  • 物件結構穩定,但你需要對這個結構不斷加入新的操作
  • 不希望每多一種操作,就往節點類別裡再塞一個方法

雙重分派 (Double Dispatch)

element.accept(visitor) 正是雙重分派的一種實作:第一次分派決定 element 的型別,第二次分派決定 visitor 的操作。


總結

Visitor 是在不修改物件的前提下,為一個穩定結構加入新操作的模式。編譯器中的 AST 遍歷、Linter 規則的套用,以及文件序列化,全都是 Visitor 的經典舞台。