今天要來介紹一個比較特別、平常可能不太常見的模式。就讓我們直接進入問題吧
假設有間百貨公司週年慶,為了回饋會員,決定發送福袋給大家。在福袋當中,會放入價值不等的獎品,不過在準備福袋的時候,為了增加驚喜感,老闆改變規則,決定讓福袋當中可以放入各種不同的福袋。所以有可能會員收到福袋的時候,會像收到俄羅斯娃娃一樣,可以一層一層打開福袋,然後看到不同的獎品。
但是這時候經理就頭痛了,為了要掌握預算,本來只要清點每個福袋的獎品總額就行,但是當福袋當中還有福袋的時候,要計算獎品總額就變得相對困難了,這時候該怎麼辦呢?
這時候一位工程師就說話了:不如我們讓福袋自己可以回報獎品總額吧!但是該怎麼做呢?
首先,我們先建立一個抽象類別,他定義了兩個基本的方法:add
和 getPrize
。同樣的在抽象類別當中,我們沒有定義它們的實作細節
abstract class Gift {
add(gift: Gift): void {}
abstract getPrize(): number
}
接著,我們建立獎品類別 Prize
,他繼承了 Gift
類別,並定義 getPrize
的實作細節。另一方面,也建立了 prize
屬性以及 constructor
class Prize extends Gift {
private prize: number
constructor(prize: number) {
super()
this.prize = prize
}
getPrize(): number {
return this.prize
}
}
最後,來建立福袋類別 Package
,這裡同樣繼承了Gift
類別,並定義跟 Prize
不太一樣的 getPrize
實作細節,以及一個不同的屬性 children
class Package extends Gift {
private children: Gift[] = []
add(gift: Gift): void {
this.children.push(gift)
}
getPrize(): number {
return this.children.reduce((acc, item) => acc += item.getPrize(), 0)
}
}
接下來我們可以做什麼呢?我們可以建立福袋 package1,以及獎品 prizeA 和 prizeB
const package1 = new Package()
const prizeA = new Prize(100)
const prizeB = new Prize(150)
然後把 prizeA 和 prizeB 放入 package1
package1.add(prizeA)
package1.add(prizeB)
最後,只要呼叫 package1 的 getPrize
方法,就可以計算出獎品的總額
package1.getPrize() // 250
看到這裡,你可能會覺得這有什麼難的。到這裡的確沒有什麼難度,但如果是下面這樣包福袋的狀況,可能就會有點複雜了:
現在我們有三種福袋,其中
const package1 = new Package()
const package2 = new Package()
const package3 = new Package()
const prizeA = new Prize(100)
const prizeB = new Prize(150)
const prizeC = new Prize(200)
const prizeD = new Prize(250)
package1.add(prizeA)
package1.add(prizeB)
package2.add(prizeC)
package3.add(prizeD)
package2.add(package1)
package3.add(package2)
請問,三種福袋分別的價值是多少呢?
別擔心,這裡我們只要分別呼叫 getPrize
就可以得到結果囉
package1.getPrize() // 250
package2.getPrize() // 450
package3.getPrize() // 700
合成模式是一種較為特別的模式,他適用在類似樹狀的物件結構上面。什麼樣叫做樹狀呢?以樹來說,就是樹枝可以連接到其他小樹枝,小樹枝可以連到小小樹枝,而每一個樹枝、小樹枝、小小樹枝都可以連到樹葉。
以剛剛的例子來說,福袋當中可以包含小福袋、小福袋當中可以包含小小福袋,而每一個福袋、小福袋、小小福袋當中,可以放入獎品。
這裡會發現,福袋、小福袋、小小福袋有個共同的特徵,就是可以放入其他東西(福袋或獎品),以及計算總金額。而獎品本身雖然無法放入其他東西,但是同樣擁有計算總金額的方法。
這裡的 Package
類別,我們可以稱作 "Composite",而 Prize
可以稱作 "Leaf"。
"Composite" 的特點是,可以收集、連接其他不同的 "Composite" 或 "Leaf",但若要執行特定功能,譬如 getPrize
,就會將實際的執行交給底下的 "Composite" 或 "Leaf" 去執行。
以剛剛的例子來說,我們是怎麼計算 package3 的總金額呢?如果仔細來看,package3 當中的 getPrize
會呼叫所有 children 的 getPrize
方法,然後進行加總。所以這裡實際上我們呼叫了
以此類推,當我們呼叫了 package2.getPrize() 的時候,實際上也呼叫了
而當我們呼叫了 package1.getPrize() 的時候,實際上呼叫了
最後,所有結果就不斷回傳,最後計算出 package3 的總金額。
合成模式的優點在於,非常適合像是上面提到這樣的樹狀結構(自己可以組合/合成自己)的狀況,在樹的每一個解點上面,都可以執行期待中的操作。
在現實世界當中,類似的例子像是工作階層的分工,譬如一個企業的全球總部下面有各國的國家分部,每個國家分部當中又擁有不同的城市分部,而不管是哪一個階層、分部,都有同樣的財務、會計、法務、營運 ... 等功能,而且都要視情況回報給上一層。
合成模式的缺點在於,使用場景相對狹隘。如果剛剛提到企業的城市分部,和國家分部的運作行為非常不一樣的話,我們就無法建立一個共同使用的抽象類別或介面,最後導致其實這棵樹當中的每個節點都是不一樣類別,也就不是合成模式的行為了