cover

你有一組資料結構(員工、銷售數據),已經穩定很久了。但老闆每個月都要新報表:這個月要薪資統計、下個月要業績排名、下下個月要部門分析。每次都要改資料的 class 來加新 method 嗎?

不用。Visitor Pattern 讓你把「對資料做什麼」抽出來,資料結構完全不動。

先講結論

資料物件只需要提供一個 accept(visitor) 方法,把自己交給 Visitor 處理。要加新操作?寫一個新的 Visitor,資料 class 一行都不用改。

classDiagram
    class Element {
        <<abstract>>
        +accept(visitor)* void
    }
    class EmployeeData {
        +accept(visitor) void
    }
    class SalesData {
        +accept(visitor) void
    }
    class Visitor {
        <<interface>>
        +visitEmployee(employee)* void
        +visitSales(sales)* void
    }
    class ReportVisitor {
        +visitEmployee(employee) void
        +visitSales(sales) void
        +getReport() String
    }
    Element <|-- EmployeeData
    Element <|-- SalesData
    Visitor <|.. ReportVisitor
    Element ..> Visitor : accept(visitor)

實戰:多種報表生成

Visitor 模式:訪問者巡迴操作不同展品

class EmployeeData {
    constructor(name, salary, department) {
        this.name = name;
        this.salary = salary;
        this.department = department;
    }
    accept(visitor) { visitor.visitEmployee(this); }
}
 
class SalesData {
    constructor(product, amount, region) {
        this.product = product;
        this.amount = amount;
        this.region = region;
    }
    accept(visitor) { visitor.visitSales(this); }
}
 
// 報表 Visitor
class ReportVisitor {
    constructor() { this.report = []; }
 
    visitEmployee(emp) {
        this.report.push(`Employee: ${emp.name}, Dept: ${emp.department}`);
    }
 
    visitSales(sale) {
        this.report.push(`Sales: ${sale.product}, $${sale.amount}`);
    }
 
    getReport() { return this.report.join('\n'); }
}
 
// 資料不變,Visitor 隨便換
const data = [
    new EmployeeData('John', 50000, 'Engineering'),
    new EmployeeData('Jane', 60000, 'Marketing'),
    new SalesData('Product A', 10000, 'Asia'),
];
 
const reporter = new ReportVisitor();
data.forEach(item => item.accept(reporter));
console.log(reporter.getReport());

想加一個「薪資加總 Visitor」?寫一個新 class 就好,EmployeeDataSalesData 完全不用動。

Visitor 的致命弱點

優點很明顯,但缺點也很致命:新增資料類型的時候,所有 Visitor 都要改

假設你加了一個 InventoryData,那 ReportVisitorSalaryVisitor、其他所有 Visitor 都要加 visitInventory() 方法。如果你的資料類型經常變動,Visitor 反而會讓你更痛苦。

所以 Visitor 適合「資料結構穩定、操作經常變」的場景。反過來的話,用其他 pattern。


Visitor Pattern 就像博物館的導覽員——展品(資料)不會變,但每個導覽員(Visitor)可以講不同的故事。問題是博物館一加新展品,所有導覽員的講稿都要改


延伸閱讀