iT邦幫忙

2021 iThome 鐵人賽

DAY 24
0
Software Development

幫自己搞懂物件導向和設計模式系列 第 24

Composite 合成模式

今天要來介紹一個比較特別、平常可能不太常見的模式。就讓我們直接進入問題吧

問題

假設有間百貨公司週年慶,為了回饋會員,決定發送福袋給大家。在福袋當中,會放入價值不等的獎品,不過在準備福袋的時候,為了增加驚喜感,老闆改變規則,決定讓福袋當中可以放入各種不同的福袋。所以有可能會員收到福袋的時候,會像收到俄羅斯娃娃一樣,可以一層一層打開福袋,然後看到不同的獎品。

但是這時候經理就頭痛了,為了要掌握預算,本來只要清點每個福袋的獎品總額就行,但是當福袋當中還有福袋的時候,要計算獎品總額就變得相對困難了,這時候該怎麼辦呢?

這時候一位工程師就說話了:不如我們讓福袋自己可以回報獎品總額吧!但是該怎麼做呢?

實作

首先,我們先建立一個抽象類別,他定義了兩個基本的方法:addgetPrize。同樣的在抽象類別當中,我們沒有定義它們的實作細節

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

看到這裡,你可能會覺得這有什麼難的。到這裡的確沒有什麼難度,但如果是下面這樣包福袋的狀況,可能就會有點複雜了:

現在我們有三種福袋,其中

  • package1 會放入 prizeA 和 prizeB
  • package2 會放入 package1 和 prizeC
  • package3 會放入 package2 和 prizeD
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

合成模式

合成模式是一種較為特別的模式,他適用在類似樹狀的物件結構上面。什麼樣叫做樹狀呢?以樹來說,就是樹枝可以連接到其他小樹枝,小樹枝可以連到小小樹枝,而每一個樹枝、小樹枝、小小樹枝都可以連到樹葉。

以剛剛的例子來說,福袋當中可以包含小福袋、小福袋當中可以包含小小福袋,而每一個福袋、小福袋、小小福袋當中,可以放入獎品。

這裡會發現,福袋、小福袋、小小福袋有個共同的特徵,就是可以放入其他東西(福袋或獎品),以及計算總金額。而獎品本身雖然無法放入其他東西,但是同樣擁有計算總金額的方法。

Delegate

這裡的 Package 類別,我們可以稱作 "Composite",而 Prize 可以稱作 "Leaf"。

"Composite" 的特點是,可以收集、連接其他不同的 "Composite" 或 "Leaf",但若要執行特定功能,譬如 getPrize,就會將實際的執行交給底下的 "Composite" 或 "Leaf" 去執行。

以剛剛的例子來說,我們是怎麼計算 package3 的總金額呢?如果仔細來看,package3 當中的 getPrize 會呼叫所有 children 的 getPrize 方法,然後進行加總。所以這裡實際上我們呼叫了

  • package2.getPrize()
  • prizeD.getPrize()

以此類推,當我們呼叫了 package2.getPrize() 的時候,實際上也呼叫了

  • package1.getPrize()
  • prizeC.getPrize()

而當我們呼叫了 package1.getPrize() 的時候,實際上呼叫了

  • prizeB.getPrize()
  • prizeC.getPrize()

最後,所有結果就不斷回傳,最後計算出 package3 的總金額。

優點與缺點

合成模式的優點在於,非常適合像是上面提到這樣的樹狀結構(自己可以組合/合成自己)的狀況,在樹的每一個解點上面,都可以執行期待中的操作。

在現實世界當中,類似的例子像是工作階層的分工,譬如一個企業的全球總部下面有各國的國家分部,每個國家分部當中又擁有不同的城市分部,而不管是哪一個階層、分部,都有同樣的財務、會計、法務、營運 ... 等功能,而且都要視情況回報給上一層。

合成模式的缺點在於,使用場景相對狹隘。如果剛剛提到企業的城市分部,和國家分部的運作行為非常不一樣的話,我們就無法建立一個共同使用的抽象類別或介面,最後導致其實這棵樹當中的每個節點都是不一樣類別,也就不是合成模式的行為了


上一篇
Bridge 橋接器模式
下一篇
Decorator 裝飾器模式
系列文
幫自己搞懂物件導向和設計模式30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言