iT邦幫忙

2024 iThome 鐵人賽

DAY 19
0

合成模式藉由統一樹狀結構中所有節點的介面,使不同類型的節點擁有相同的操作介面。

生活範例

一間企業由不同部門組成,部門下有組別,而每個組別還可能有不同的團隊。企業中的每個團隊就像一個組合節點,而團隊中的每個員工則是企業的葉節點。當老闆交辦任務時,會先指派給部門主管,再由部門主管協調團隊與員工來完成。老闆不會直接指派任務給每位員工,正如同客戶端透過組合節點來管理節點,而不需逐一操作每個葉節點。

舉個例子

想像一個訂單系統,每筆訂單可以包含商品或子訂單,子訂單中可以再包含更多子訂單。我們可以利用合成模式統一商品和訂單的介面,讓使用者通過統一的操作來檢視訂單資訊。

首先,定義訂單元件,並在其中定義所有商品和訂單的操作行為。在這些方法中,有些是通用方法,有些是商品或訂單的專屬方法。我們預設所有方法都拋出例外錯誤,以確保使用者不會向錯誤的對象進行不恰當的操作。

abstract class OrderComponent {
  getName(): string {
    throw new Error("Unsupported Operation");
  }
  getPrice(): number {
    throw new Error("Unsupported Operation");
  }
  print(): void {
    throw new Error("Unsupported Operation");
  }
  addComponent(component: OrderComponent): void {
    throw new Error("Unsupported Operation");
  }
  removeComponent(component: OrderComponent): void {
    throw new Error("Unsupported Operation");
  }
}

訂單項目用來表示一項商品,例如茶或薯片。這個類別實現了 getName()getPrice()print() 方法,用來取得商品的名稱、價格並列印出其資訊。

class OrderItem extends OrderComponent {
  private name: string;
  private price: number;

  constructor(name: string, price: number) {
    super();
    this.name = name;
    this.price = price;
  }

  getName() {
    return this.name;
  }

  getPrice() {
    return this.price;
  }

  print() {
    console.log(`  ${this.name}, $${this.price}`);
  }
}

訂單類別代表一個可以包含多個商品或子訂單的訂單。訂單類別可以透過 addComponentremoveComponent 方法來新增或移除成員,此外,也可以透過其他方法取得訂單資訊。

class Order extends OrderComponent {
  private name: string;
  private components: OrderComponent[];

  constructor(name: string) {
    super();
    this.name = name;
    this.components = [];
  }

  addComponent(component: OrderComponent) {
    this.components.push(component);
  }

  removeComponent(component: OrderComponent) {
    const index = this.components.indexOf(component);
    if (index !== -1) {
      this.components.splice(index, 1);
    }
  }

  getName() {
    return this.name;
  }

  getPrice() {
    return this.components.reduce(
      (total, component) => total + component.getPrice(),
      0
    );
  }

  print() {
    console.log(this.name);
    console.log("-----------------");
    this.components.forEach((component) => {
      if (component instanceof Order) console.log("");
      component.print();
    });
  }
}

定義一個檢視器類別來查看訂單資訊。

class OrderInspector {
  private order: OrderComponent;

  constructor(order: OrderComponent) {
    this.order = order;
  }

  printDetails() {
    this.order.print();
  }

  printTotalPrice() {
    console.log("\nTotal Price:", this.order.getPrice());
  }
}

測試訂單系統。

class OrderInspectorTestDrive {
  static main() {
    const chips = new OrderItem("Potato Chips", 50);
    const tea = new OrderItem("Tea", 30);
    const shirt = new OrderItem("T-shirt", 500);
    const jeans = new OrderItem("Jeans", 1200);
    const jacket = new OrderItem("Jacket", 2500);
    const headphones = new OrderItem("Headphones", 15000);

    const mainOrder = new Order("Main Order");
    const groceryOrder = new Order("Grocery Order");
    const clothingOrder = new Order("Clothing Order");

    groceryOrder.addComponent(chips);
    groceryOrder.addComponent(tea);

    clothingOrder.addComponent(shirt);
    clothingOrder.addComponent(jeans);
    clothingOrder.addComponent(jacket);

    mainOrder.addComponent(headphones);
    mainOrder.addComponent(groceryOrder);
    mainOrder.addComponent(clothingOrder);

    const inspector = new OrderInspector(mainOrder);
    inspector.printDetails();
    inspector.printTotalPrice();
  }
}

OrderInspectorTestDrive.main();

執行結果。

Main Order
-----------------
  Headphones, $15000

Grocery Order
-----------------
  Potato Chips, $50
  Tea, $30

Clothing Order
-----------------
  T-shirt, $500
  Jeans, $1200
  Jacket, $2500

Total Price: 19280

定義

Composite Pattern

  • 元件(Component): 葉節點和組合節點的通用介面
  • 葉節點(Leaf): 負責實現元件介面的具體行為
  • 組合節點(Composite): 葉節點的集合,負責管理與協調葉節點

合成模式將樹狀結構中的節點設計為統一的介面,使葉節點和組合節點擁有相同的操作介面。客戶端可以透過相同的介面操作節點,無需區分葉節點和組合節點,這使樹狀資料變得容易操作,也讓客戶端程式變得更為簡潔。

總結

  • 統一樹狀結構中所有節點的介面,使葉節點和組合節點擁有相同的操作介面
  • 客戶端可以利用相同的介面操作節點,而無需區分葉節點和組合節點
  • 提升樹狀資料的易用性,進而使客戶端程式變得簡潔

完整範例

https://github.com/chengen0612/design-patterns-typescript/blob/main/patterns/structural/composite.ts


上一篇
Day 18 - Iterator 反覆器
下一篇
Day 20 - Null Object 空物件
系列文
前端也想學設計模式30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言