合成模式藉由統一樹狀結構中所有節點的介面,使不同類型的節點擁有相同的操作介面。
一間企業由不同部門組成,部門下有組別,而每個組別還可能有不同的團隊。企業中的每個團隊就像一個組合節點,而團隊中的每個員工則是企業的葉節點。當老闆交辦任務時,會先指派給部門主管,再由部門主管協調團隊與員工來完成。老闆不會直接指派任務給每位員工,正如同客戶端透過組合節點來管理節點,而不需逐一操作每個葉節點。
想像一個訂單系統,每筆訂單可以包含商品或子訂單,子訂單中可以再包含更多子訂單。我們可以利用合成模式統一商品和訂單的介面,讓使用者通過統一的操作來檢視訂單資訊。
首先,定義訂單元件,並在其中定義所有商品和訂單的操作行為。在這些方法中,有些是通用方法,有些是商品或訂單的專屬方法。我們預設所有方法都拋出例外錯誤,以確保使用者不會向錯誤的對象進行不恰當的操作。
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}`);
}
}
訂單類別代表一個可以包含多個商品或子訂單的訂單。訂單類別可以透過 addComponent
和 removeComponent
方法來新增或移除成員,此外,也可以透過其他方法取得訂單資訊。
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
合成模式將樹狀結構中的節點設計為統一的介面,使葉節點和組合節點擁有相同的操作介面。客戶端可以透過相同的介面操作節點,無需區分葉節點和組合節點,這使樹狀資料變得容易操作,也讓客戶端程式變得更為簡潔。
https://github.com/chengen0612/design-patterns-typescript/blob/main/patterns/structural/composite.ts