
你有一組資料結構(員工、銷售數據),已經穩定很久了。但老闆每個月都要新報表:這個月要薪資統計、下個月要業績排名、下下個月要部門分析。每次都要改資料的 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)
實戰:多種報表生成

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 就好,EmployeeData 和 SalesData 完全不用動。
Visitor 的致命弱點
優點很明顯,但缺點也很致命:新增資料類型的時候,所有 Visitor 都要改。
假設你加了一個 InventoryData,那 ReportVisitor、SalaryVisitor、其他所有 Visitor 都要加 visitInventory() 方法。如果你的資料類型經常變動,Visitor 反而會讓你更痛苦。
所以 Visitor 適合「資料結構穩定、操作經常變」的場景。反過來的話,用其他 pattern。
Visitor Pattern 就像博物館的導覽員——展品(資料)不會變,但每個導覽員(Visitor)可以講不同的故事。問題是博物館一加新展品,所有導覽員的講稿都要改。