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 的总金额。

优点与缺点

合成模式的优点在於,非常适合像是上面提到这样的树状结构(自己可以组合/合成自己)的状况,在树的每一个解点上面,都可以执行期待中的操作。

在现实世界当中,类似的例子像是工作阶层的分工,譬如一个企业的全球总部下面有各国的国家分部,每个国家分部当中又拥有不同的城市分部,而不管是哪一个阶层、分部,都有同样的财务、会计、法务、营运 ... 等功能,而且都要视情况回报给上一层。

合成模式的缺点在於,使用场景相对狭隘。如果刚刚提到企业的城市分部,和国家分部的运作行为非常不一样的话,我们就无法建立一个共同使用的抽象类别或介面,最後导致其实这棵树当中的每个节点都是不一样类别,也就不是合成模式的行为了


<<:  Day24-你的资料安全吗(二)

>>:  找LeetCode上简单的题目来撑过30天啦(DAY24)

【演算法】L1 演算法评估

演算法评估 ### 演算法衡量 效率 渐进符号 EX:O(n) 最差案例 平均案例 平摊分析 问题衡...

Day 11 - 自订事件

昨天我们介绍了props的用法,要注意的是,props是单向数据流,所以只能从上传到下,要由下传到上...

Day30 - this&Object Prototypes Ch3 Objects - Review

Iteration forEach()、every()、some() 三者的差异在於:他们会对我们...

第 28 天 - RxGesture

今天讲GitHub - RxSwiftCommunity/RxGesture,RxGesture是封...

[DAY8]k8s必学的设定档-yaml (上)

YAML(/ˈjæməl/,尾音类似camel骆驼)是一个可读性高,用来表达资料序列化的格式。YA...