iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0

訪問者模式允許在不修改物件類別的情況下,對物件集合中的元素施加新的操作。

生活範例

麥當勞的餐點有很多種點法,比如說一個麥香雞漢堡,可以單點、搭配薯條和可樂做成套餐,或是與其他餐點組成 1+1 的組合餐。消費者的訂單就像各種餐點組合的集合,而金額的計算系統則如同訪問者,必須根據不同組合套用不同的計價規則,藉此得出正確的金額。

舉個例子

麥當勞的餐點有這麼多種組合方式,每一種組合的計價規則與明細的格式都不同。我們可以使用訪問者模式,將相同功能的操作集中在同一個訪問者中,統一操作介面,使程式碼更容易管理。

OrderItem 代表一個訂單項目,並提供一個接收訪問者的方法。

interface OrderItem {
  accept(visitor: OrderItemVisitor): void;
}

以下是菜單項目、套餐和 1+1 優惠組合的具體類別,這三種類別各自擁有不同的資料,但它們都實現了 accept 方法,可以透過訪問者來進行操作。

class MenuItem implements OrderItem {
  constructor(public name: string, public price: number) {}

  accept(visitor: OrderItemVisitor): void {
    visitor.visitMenuItem(this);
  }
}

class Meal implements OrderItem {
  constructor(
    public main: MenuItem,
    public side: MenuItem,
    public drink: MenuItem
  ) {}

  accept(visitor: OrderItemVisitor): void {
    visitor.visitMeal(this);
  }
}

class Combo implements OrderItem {
  constructor(public left: MenuItem, public right: MenuItem) {}

  accept(visitor: OrderItemVisitor): void {
    visitor.visitCombo(this);
  }
}

這是訂單項目的抽象訪問者,定義了各項具體訂單項目的訪問方法。

interface OrderItemVisitor {
  visitMenuItem(menuItem: MenuItem): void;
  visitMeal(meal: Meal): void;
  visitCombo(combo: Combo): void;
}

PricingVisitor 是一個具體訪問者,用於計算訂單總價。它定義了各項具體訂單項目的計算邏輯,可以根據不同餐點組合來累積價格。

class PricingVisitor implements OrderItemVisitor {
  private totalPrice: number = 0;

  visitMenuItem(menuItem: MenuItem) {
    this.totalPrice += menuItem.price;
  }

  visitMeal(meal: Meal) {
    this.totalPrice += meal.main.price;
    this.totalPrice += meal.side.price;
    this.totalPrice += meal.drink.price;
  }

  visitCombo(combo: Combo) {
    this.totalPrice += combo.left.price;
    this.totalPrice += combo.right.price;
  }

  getTotalPrice() {
    return this.totalPrice;
  }
}

OrderDetailVisitor 是另一個具體的訪問者類別,用於生成訂單明細。它會根據不同餐點組合生成對應格式的訂單細項。

class OrderDetailVisitor implements OrderItemVisitor {
  private details: string[] = [];

  visitMenuItem(item: MenuItem) {
    this.details.push(`- ${item.name}: $${item.price}`);
  }

  visitMeal(meal: Meal) {
    this.details.push("- Meal:");
    this.details.push(`  - ${meal.main.name}: $${meal.main.price}`);
    this.details.push(`  - ${meal.side.name}: $${meal.side.price}`);
    this.details.push(`  - ${meal.drink.name}: $${meal.drink.price}`);
  }

  visitCombo(combo: Combo) {
    this.details.push("- 1+1 Combo:");
    this.details.push(`  - ${combo.left.name}: $${combo.left.price}`);
    this.details.push(`  - ${combo.right.name}: $${combo.right.price}`);
  }

  getOrderDetails() {
    return this.details.join("\n");
  }
}

Order 類別代表一個訂單,擁有一個訂單項目清單,並透過 PricingVisitorOrderDetailVisitor 來計算總價和產生訂單明細。

class Order {
  private items: OrderItem[];

  constructor(items: OrderItem[] = []) {
    this.items = items;
  }

  calculateTotal() {
    const visitor = new PricingVisitor();
    this.items.forEach((item) => item.accept(visitor));
    return visitor.getTotalPrice();
  }

  getOrderDetails() {
    const visitor = new OrderDetailVisitor();
    this.items.forEach((item) => item.accept(visitor));
    return visitor.getOrderDetails();
  }
}

建立一些餐點和套餐,並建立兩筆訂單,再印出兩筆訂單的總金額與商品明細。

class OrderTestDrive {
  static main() {
    const mcChicken = new MenuItem("McChicken", 50);
    const filetOFish = new MenuItem("Filet-O-Fish", 45);
    const fries = new MenuItem("Fries", 30);
    const coke = new MenuItem("Coke", 25);
    const lemonTea = new MenuItem("Lemon Tea", 20);
    const iceCream = new MenuItem("Ice Cream", 35);

    const meal = new Meal(mcChicken, fries, coke);
    const combo = new Combo(fries, lemonTea);

    const order1 = new Order([filetOFish, meal, combo]);
    const order2 = new Order([filetOFish, iceCream]);

    console.log(`Total for Order1: $${order1.calculateTotal()}`);
    console.log(`Total for Order2: $${order2.calculateTotal()}`);

    console.log("\nOrder1 Details:");
    console.log(order1.getOrderDetails());

    console.log("\nOrder2 Details:");
    console.log(order2.getOrderDetails());
  }
}

OrderTestDrive.main();

執行結果:

Total for Order1: $200
Total for Order2: $80

Order1 Details:
- Filet-O-Fish: $45
- Meal:
  - McChicken: $50
  - Fries: $30
  - Coke: $25
- 1+1 Combo:
  - Fries: $30
  - Lemon Tea: $20

Order2 Details:
- Filet-O-Fish: $45
- Ice Cream: $35

定義

Visitor Pattern

  • 訪問者介面(Visitor): 定義對不同訪問對象的操作方法
  • 具體訪問者(ConcreteVisitor): 實現訪問者介面,定義每個訪問對象的實際行為
  • 訪問對象介面(Element): 定義接受訪問者的訪問介面
  • 具體訪問對象(ConcreteElement): 實作訪問者介面,允許訪問者進行操作

訪問者模式可以處理包含不同介面物件的集合,並根據每個物件的具體類別執行不同的操作。這種模式將操作與物件類別分離,使我們能在不修改物件類別的前提下新增功能。當需要新增功能時,只需定義新的訪問者來處理各類物件的行為,無需更改物件類別的原始程式碼。這不僅讓程式邏輯更加集中,也能通過替換訪問者輕鬆實現新的邏輯。

總結

  • 能夠對一個由不同介面或類型的物件組成的物件集合進行操作
  • 將操作與物件分離,在不修改物件類別的情況下實現新功能
  • 集中相同功能,避免程式邏輯分散在各個類別中

完整範例

https://github.com/chengen0612/design-patterns-typescript/blob/main/patterns/behavioral/visitor.ts


上一篇
Day 28 - Interpreter 解釋器
下一篇
Day 30 - 結語
系列文
前端也想學設計模式30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言