Visitor Pattern(訪問者模式)
是什麼?
Visitor Pattern 讓你在不修改既有物件結構的情況下定義新的操作。透過「雙重分派(Double Dispatch)」技巧,把操作邏輯從物件中抽離到獨立的 Visitor 類別中。
ℹ️GoF 分類
Visitor 屬於行為型模式(Behavioral Pattern),重點在於將演算法和物件結構分離,方便新增操作而不修改現有類別。
什麼時候用?
- 你需要對一組不同型別的物件執行多種不同的操作
- 物件結構穩定(很少新增型別),但操作經常變動(常新增新操作)
- 你想避免在每個物件類別中堆積不相關的操作方法
- 你在處理 AST(抽象語法樹)、文件結構等複合結構
什麼時候不該用?
⚠️過度設計警告
如果物件的型別經常新增,每新增一種型別就要修改所有 Visitor——這正好違反了開放封閉原則。Visitor 適合「型別穩定、操作多變」的場景。如果反過來,用普通的多型(Polymorphism)更合適。
執行流程
定義 Visitor 介面
為每種元素型別宣告一個 Visit 方法
定義 Element 介面
宣告 Accept(visitor) 方法
實作具體 Element
Accept 方法中呼叫 visitor.Visit(this)
實作具體 Visitor
在每個 Visit 方法中定義對該型別的操作
Client 走訪結構
遍歷所有元素,對每個呼叫 Accept(visitor)
流程解讀:Visitor 的核心技巧是 Double Dispatch。第一次分派:Client 呼叫 element.Accept(visitor),根據 element 的具體型別分派到正確的 Accept 實作。第二次分派:Accept 內部呼叫 visitor.VisitCircle(this) 或 visitor.VisitRectangle(this),根據 visitor 的具體型別分派到正確的 Visit 方法。兩次分派確保了「正確的 Visitor 方法」作用在「正確的 Element 型別」上。
程式碼範例
C# 版本
// 1. Element 介面
public interface IShape
{
void Accept(IShapeVisitor visitor);
}
// 2. 具體 Element
public class Circle : IShape
{
public double Radius { get; }
public Circle(double radius) => Radius = radius;
public void Accept(IShapeVisitor visitor) => visitor.VisitCircle(this);
}
public class Rectangle : IShape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double w, double h) { Width = w; Height = h; }
public void Accept(IShapeVisitor visitor) => visitor.VisitRectangle(this);
}
public class Triangle : IShape
{
public double Base { get; }
public double Height { get; }
public Triangle(double b, double h) { Base = b; Height = h; }
public void Accept(IShapeVisitor visitor) => visitor.VisitTriangle(this);
}
// 3. Visitor 介面
public interface IShapeVisitor
{
void VisitCircle(Circle circle);
void VisitRectangle(Rectangle rectangle);
void VisitTriangle(Triangle triangle);
}
// 4. 具體 Visitor:計算面積
public class AreaCalculator : IShapeVisitor
{
public double TotalArea { get; private set; }
public void VisitCircle(Circle c)
{
var area = Math.PI * c.Radius * c.Radius;
TotalArea += area;
Console.WriteLine($" 圓形面積:{area:F2}");
}
public void VisitRectangle(Rectangle r)
{
var area = r.Width * r.Height;
TotalArea += area;
Console.WriteLine($" 矩形面積:{area:F2}");
}
public void VisitTriangle(Triangle t)
{
var area = t.Base * t.Height / 2;
TotalArea += area;
Console.WriteLine($" 三角形面積:{area:F2}");
}
}
// 5. 具體 Visitor:繪製描述
public class ShapeDescriber : IShapeVisitor
{
public void VisitCircle(Circle c)
=> Console.WriteLine($" [描述] 半徑 {c.Radius} 的圓");
public void VisitRectangle(Rectangle r)
=> Console.WriteLine($" [描述] {r.Width}x{r.Height} 的矩形");
public void VisitTriangle(Triangle t)
=> Console.WriteLine($" [描述] 底 {t.Base} 高 {t.Height} 的三角形");
}
// 6. 使用
IShape[] shapes = { new Circle(5), new Rectangle(4, 6), new Triangle(3, 8) };
var calculator = new AreaCalculator();
var describer = new ShapeDescriber();
Console.WriteLine("計算面積:");
foreach (var shape in shapes) shape.Accept(calculator);
Console.WriteLine($"總面積:{calculator.TotalArea:F2}");
Console.WriteLine("描述形狀:");
foreach (var shape in shapes) shape.Accept(describer);TypeScript 版本
// 1. 介面
interface ShapeVisitor {
visitCircle(circle: Circle): void;
visitRectangle(rect: Rectangle): void;
visitTriangle(tri: Triangle): void;
}
interface Shape {
accept(visitor: ShapeVisitor): void;
}
// 2. 具體 Element
class Circle implements Shape {
constructor(public radius: number) {}
accept(visitor: ShapeVisitor) { visitor.visitCircle(this); }
}
class Rectangle implements Shape {
constructor(public width: number, public height: number) {}
accept(visitor: ShapeVisitor) { visitor.visitRectangle(this); }
}
class Triangle implements Shape {
constructor(public base: number, public height: number) {}
accept(visitor: ShapeVisitor) { visitor.visitTriangle(this); }
}
// 3. 具體 Visitor
class AreaCalculator implements ShapeVisitor {
totalArea = 0;
visitCircle(c: Circle) {
const area = Math.PI * c.radius ** 2;
this.totalArea += area;
console.log(` 圓形面積:${area.toFixed(2)}`);
}
visitRectangle(r: Rectangle) {
const area = r.width * r.height;
this.totalArea += area;
console.log(` 矩形面積:${area.toFixed(2)}`);
}
visitTriangle(t: Triangle) {
const area = (t.base * t.height) / 2;
this.totalArea += area;
console.log(` 三角形面積:${area.toFixed(2)}`);
}
}
// 4. 使用
const shapes: Shape[] = [new Circle(5), new Rectangle(4, 6), new Triangle(3, 8)];
const calc = new AreaCalculator();
shapes.forEach(s => s.accept(calc));
console.log(`總面積:${calc.totalArea.toFixed(2)}`);Python 版本
from abc import ABC, abstractmethod
import math
# 1. Visitor 介面
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle: "Circle") -> None:
pass
@abstractmethod
def visit_rectangle(self, rect: "Rectangle") -> None:
pass
@abstractmethod
def visit_triangle(self, tri: "Triangle") -> None:
pass
# 2. Element 介面
class Shape(ABC):
@abstractmethod
def accept(self, visitor: ShapeVisitor) -> None:
pass
# 3. 具體 Element
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def accept(self, visitor: ShapeVisitor) -> None:
visitor.visit_circle(self)
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def accept(self, visitor: ShapeVisitor) -> None:
visitor.visit_rectangle(self)
class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def accept(self, visitor: ShapeVisitor) -> None:
visitor.visit_triangle(self)
# 4. 具體 Visitor
class AreaCalculator(ShapeVisitor):
def __init__(self):
self.total_area = 0.0
def visit_circle(self, c: Circle) -> None:
area = math.pi * c.radius ** 2
self.total_area += area
print(f" 圓形面積:{area:.2f}")
def visit_rectangle(self, r: Rectangle) -> None:
area = r.width * r.height
self.total_area += area
print(f" 矩形面積:{area:.2f}")
def visit_triangle(self, t: Triangle) -> None:
area = t.base * t.height / 2
self.total_area += area
print(f" 三角形面積:{area:.2f}")
# 5. 使用
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]
calc = AreaCalculator()
for shape in shapes:
shape.accept(calc)
print(f"總面積:{calc.total_area:.2f}")Java 版本
// 1. 介面
public interface ShapeVisitor {
void visitCircle(Circle circle);
void visitRectangle(Rectangle rect);
void visitTriangle(Triangle tri);
}
public interface Shape {
void accept(ShapeVisitor visitor);
}
// 2. 具體 Element
public class Circle implements Shape {
public final double radius;
public Circle(double radius) { this.radius = radius; }
public void accept(ShapeVisitor v) { v.visitCircle(this); }
}
public class Rectangle implements Shape {
public final double width, height;
public Rectangle(double w, double h) { width = w; height = h; }
public void accept(ShapeVisitor v) { v.visitRectangle(this); }
}
public class Triangle implements Shape {
public final double base, height;
public Triangle(double b, double h) { base = b; height = h; }
public void accept(ShapeVisitor v) { v.visitTriangle(this); }
}
// 3. 具體 Visitor
public class AreaCalculator implements ShapeVisitor {
private double totalArea = 0;
public void visitCircle(Circle c) {
double area = Math.PI * c.radius * c.radius;
totalArea += area;
System.out.printf(" 圓形面積:%.2f%n", area);
}
public void visitRectangle(Rectangle r) {
double area = r.width * r.height;
totalArea += area;
System.out.printf(" 矩形面積:%.2f%n", area);
}
public void visitTriangle(Triangle t) {
double area = t.base * t.height / 2;
totalArea += area;
System.out.printf(" 三角形面積:%.2f%n", area);
}
public double getTotalArea() { return totalArea; }
}
// 4. 使用
Shape[] shapes = { new Circle(5), new Rectangle(4, 6), new Triangle(3, 8) };
AreaCalculator calc = new AreaCalculator();
for (Shape s : shapes) s.accept(calc);
System.out.printf("總面積:%.2f%n", calc.getTotalArea());結構圖
結構解讀:Client 遍歷 Element 集合,對每個 Element 呼叫 accept(visitor)。Element 的 accept 方法內部呼叫 visitor.visitXxx(this),這就是 Double Dispatch(雙重分派,指方法的最終執行取決於兩個物件的型別而非一個)。新增操作只需新增 Visitor(如 ShapeDescriber),Element 類別完全不用改。但新增 Element 型別需要修改所有 Visitor。
實戰補充
💡資深開發者經驗
AST 遍歷:編譯器和程式碼分析工具(如 Roslyn Analyzer)廣泛使用 Visitor Pattern。語法樹的節點型別固定(IfStatement、ForLoop、Assignment...),但你需要定義各種不同的分析操作。
報表生成:同一組資料結構需要產出 PDF、Excel、HTML 等不同格式的報表。每種格式是一個 Visitor,資料結構不用改。
Double Dispatch:Visitor 的核心技巧是 Double Dispatch——element.Accept(visitor) 先分派到正確的元素型別,再分派到 visitor.Visit(element) 的正確多載。這繞過了大部分語言單一分派的限制。
C# 的 Pattern Matching:C# 8+ 的 switch expression + Pattern Matching 在某些情況下可以取代 Visitor,寫起來更簡潔。但當操作需要跨元素累積狀態時,Visitor 仍然更適合。
理解測驗
🤔 Visitor Pattern 最適合什麼樣的場景?
🤔 Visitor Pattern 中的 Double Dispatch 是什麼意思?
🤔 以下哪個是 Visitor Pattern 的缺點?
面試常見問題
Q: Visitor Pattern 什麼時候不該用?
A: 當 Element 型別經常新增時不該用。因為每新增一種 Element,所有 Visitor 都要新增對應的 Visit 方法,違反開放封閉原則。判斷標準:如果型別穩定(如 AST 節點型別、幾何圖形種類)且操作經常新增 → 適合 Visitor。如果型別經常變(如電商的商品種類)→ 用普通的多型更適合。
Q: C# 的 Pattern Matching 能取代 Visitor 嗎?
A: 部分場景可以。C# 8+ 的 switch expression + is 模式在簡單場景下比 Visitor 更簡潔:shape switch { Circle c => ..., Rectangle r => ... }。但 Visitor 在以下場景仍有優勢:(1) 操作需要跨元素累積狀態(如 AreaCalculator 的 totalArea);(2) 需要編譯時型別安全(新增 Element 時 Visitor 介面會強制你處理);(3) 需要對整個結構做遞迴走訪。
相關模式
| 模式 | 關係 | |------|------| | Iterator | Iterator 控制走訪順序,Visitor 控制對每個元素做什麼操作。常搭配使用 | | Composite | Visitor 常用來走訪 Composite 結構,對 Leaf 和 Composite 節點做不同操作 | | Strategy | 如果只需要替換單一操作 → Strategy。如果需要對多種型別的元素各自定義操作 → Visitor | | Interpreter | Interpreter 的語法樹常用 Visitor 來走訪和求值 |
重點整理
💡一句話記住
Visitor Pattern = 外掛操作:結構不動,操作外掛。 口訣:「型別穩定加操作,雙重分派是核心」
| 概念 | 說明 | |------|------| | Visitor(訪問者介面) | 為每種元素型別定義一個 Visit 方法 | | ConcreteVisitor | 實作對每種元素的具體操作 | | Element(元素介面) | 定義 Accept(visitor) 方法 | | 核心好處 | 新增操作不用改元素類別 | | 代價 | 新增元素型別要改所有 Visitor |