訪問者模式將作用於某資料結構中各元素的操作封裝起來,在不改變資料結構的前提下定義作用於這些元素的新操作。屬於行為型模式,核心思想是分離資料結構與操作邏輯。
visit 方法accept(Visitor) 方法accept,調用 visitor.visit(this)public interface ItemElement {
int accept(ShoppingCartVisitor visitor);
}
public class Book implements ItemElement {
private int price;
private String isbn;
public Book(int price, String isbn) { this.price = price; this.isbn = isbn; }
public int getPrice() { return price; }
public String getIsbn() { return isbn; }
@Override
public int accept(ShoppingCartVisitor visitor) {
return visitor.visit(this); // 雙分派關鍵點
}
}
public class Fruit implements ItemElement {
private int pricePerKg, weight;
private String name;
public Fruit(int pricePerKg, int weight, String name) { ... }
public int getPricePerKg() { return pricePerKg; }
public int getWeight() { return weight; }
public String getName() { return name; }
@Override
public int accept(ShoppingCartVisitor visitor) {
return visitor.visit(this);
}
}
public interface ShoppingCartVisitor {
int visit(Book book);
int visit(Fruit fruit);
}
public class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
@Override
public int visit(Book book) {
int cost = book.getPrice();
if (cost > 50) cost -= 5; // 滿 50 減 5
System.out.println("Book ISBN:" + book.getIsbn() + " cost=" + cost);
return cost;
}
@Override
public int visit(Fruit fruit) {
int cost = fruit.getPricePerKg() * fruit.getWeight();
System.out.println(fruit.getName() + " cost=" + cost);
return cost;
}
}
public class ShoppingCartClient {
public static void main(String[] args) {
ItemElement[] items = {
new Book(20, "1234"), new Book(100, "5678"),
new Fruit(10, 2, "Banana"), new Fruit(5, 5, "Apple")
};
System.out.println("總價: " + calculatePrice(items));
}
private static int calculatePrice(ItemElement[] items) {
ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl();
int sum = 0;
for (ItemElement item : items) {
sum += item.accept(visitor); // 雙分派
}
return sum;
}
}
// 輸出:
// Book ISBN:1234 cost=20
// Book ISBN:5678 cost=95
// Banana cost=20
// Apple cost=25
// 總價: 160
訪問者模式本質是模擬雙分派:
element.accept(visitor) —— 第一次分派(根據元素類型)visitor.visit(this) —— 第二次分派(根據訪問者類型)Java 單分派語言中,通過重載 visit 方法實現編譯期綁定第二次分派。
visit 方法| 適用 ✓ | 不適用 ✗ |
|---|---|
| 元素類型穩定,操作頻繁變化/擴展 | 元素類型頻繁新增 |
| 需要對物件結構進行多種不相關操作 | 操作簡單且固定 |
| 操作邏輯複雜,需分離維護 | 物件結構本身極其簡單 |
| 編譯器/解析器 AST 遍歷 | 高效能熱點路徑(雙分派有開銷) |
// 預設空實現適配器
public class ShoppingCartVisitorAdapter implements ShoppingCartVisitor {
public int visit(Book book) { return 0; }
public int visit(Fruit fruit) { return 0; }
}
// 只關心書籍的訪問者
public class BookOnlyVisitor extends ShoppingCartVisitorAdapter {
@Override
public int visit(Book book) {
return book.getPrice(); // 忽略水果
}
}
public interface ShoppingCartVisitor {
default int visit(Book book) { return 0; }
default int visit(Fruit fruit) { return 0; }
// 新增元素時不破壞現有實現
}
public class ShoppingCart {
private List items = new ArrayList<>();
public void accept(ShoppingCartVisitor visitor) {
for (ItemElement item : items) {
item.accept(visitor);
}
}
}
public class PriceCalculator implements ShoppingCartVisitor {
private int total = 0;
private Map categorySum = new HashMap<>();
public int visit(Book book) {
total += book.getPrice();
categorySum.merge("Book", book.getPrice(), Integer::sum);
return book.getPrice();
}
// ... getter
}
「大多數情況下你不需要訪問者模式,但當你真正需要它時,你會知道的。」—— GoF