我又双叒叕来更新设计模式啦!,这一次我们将更新的名字叫做组合模式。
组合
我们废话不多说先来看一段代码:
const wash = {
excute: function () {
console.log('洗衣')
}
}
const dehydrated = {
excute: function () {
console.log('脱水')
}
}
const dry = {
excute: function () {
console.log('烘干')
}
}
const Auot_Washer = function () {
const ret = {
commandList: [],
add: function (command) {
this.commandList.push(command)
return ret
},
excute: function () {
this.commandList.forEach(command => command.excute())
return ret
}
}
return ret
}
const myAutoWasher = Auot_Washer()
myAutoWasher.add(wash).add(dehydrated).add(dry)
myAutoWasher.excute()
通过上面的代码,我们很容易的发现,宏命令中和包含了一组子命令,他们组成了一个树形结构,尽管他是一个很简单的树,即myAutoWasher是为根节点,wash、dehydrated和dry则为子节点,他们通过组合实现了一键洗衣的功能的。在myAutoWasher的excute方法里,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的execute请求委托给这些也对象。
尽管myAutoWasher表现得像一个命令,但它实际上只是一组真正命令的“代理”。之所以它不是真正的代理,是因为它的目的并不是控制对于叶对象的访问。
用途
组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对对象的多态性表现,使得用户对单个对象和组合对象使用具有一致性,下面分别说明:
- 表示树形结构。通过上面的例子我们很容易找到组合模式的一个特点: 提供了一种遍历树形结构的方案,通过调用组合对象的excute方法,程序会递归调用组合对象下面的也对象的execute方法,所以我们只需要通过全自动洗衣机的一次操作,就能依次完成洗衣、脱水和烘干几件事情。组合模式可以非常方便地描述对象部分-整体层次结构。看到这里有的读者可能就会觉得有一种莫名的熟悉感,没错使用过React的读者应该都写过JSX吧,React就是通过组合来实现整个dom树的,我们通过将各个功能的组件组合在一起形成了一个大的组件树,最后我们渲染的时候只需要渲染根节点,然后React内部会根据整个结构去深度优先的渲染整个dom。
- 利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。对于上面说的React而言,我们去渲染一个节点,我们并不需要关注他是否是根节点还是叶节点。
这在我们日常开发中是非常方便的,当我们往全自动洗衣机里面添加一个命令的时候例如新的消毒功能,我们并不需要知道这个命令是宏命令还是普通命令。这点对于我们并不重要,我们只需要确定它是一个命令即可,并且这个命令拥有可以执行的excute方法,那么这个命令就可以被添加进万能遥控器。
注意点
在使用组合模式的时候,还有以下几个值得我们注意的地方。
-
组合模式不是父子关系
组合模式的树形结构容易让人误以为组合对象和叶对象是父子关系,这是不正确的。
组合模式是一种HAS-A(聚合)的关系,而不是IS-A。组合对象包含一组对象,但是Leaf并不是Composite的子类。组合对象把请求委托给它所包含的所有叶对象,它们能够合作的关键是拥有相同的接口。
-
叶对象操作的一致性
组合模式除了要求组合对象和叶对象拥有相同的接口之外,还有一个必要条件,就是对一组叶对象的操作必须具有一致性。
比如过节的时候公司要给全体员工发放过节费1000元,这个场景可以运用组合模式,但是如果公司要给这个月过生日的员工发送一封生日祝福的邮件,组合模式在这里就没有用武之地了,除非先把今天过生日的员工挑选出来。只有用一致的方式对待列表中的每个叶对象的时候,才适合使用组合模式。 3.双向映射关系
发放过节费的通知步骤是从公司到各个部门,再到各个小组,最后再到每个员工的手中。这本身就是一个组合模式的例子,但是要考虑的一点是,比如我们前端的架构师,他可能既是前端组的也可能是架构组的,对象与之间的关系并不是严格意义上的层次结构,在这种情况下,是不适合用组合模式的,该架构师很可能会收到两个过节费,但是如果我是这个架构师那就当我没说O.o。
-
用职责链模式提高组合模式性能
在组合模式中,如果树的结构比较复杂,节点数量很多,在遍历树的过程中,性能方面也许表现得不够理想。有时候我们确实可以借助一些技巧,在实际操作中避免遍历整棵树,有一种现成的方案是借助职责链模式。职责链模式一般需要我们手动去设置链条,但是在组合模式中,父对象和子对象之间实际上形成了天然的职责链。让请求顺着链条从父对象往子对象传递,或者是反过来从子对象往父对象传递,直到遇到可以处理该请求的对象为止,这也是职责链模式的经典运用场景之一。
引用父对象
在react中,每个dom节点都会形成一个Fiber节点,那么当我们更新渲染的时候有时候会删掉一部分的节点,当React删除某个节点的时候实际上是从这个Fiber节点的父Fiber节点上删除该节点,所以当前要被删除的Fiber节点要保留对于父节点的引用。
正确使用组合模式
组合模式如果运用得当,可以大大简化我们的代码。一般来说,组合模式适用于以下两种情况。
- 表示对象的部分-整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放-封闭原则。
- 我们想统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,我们在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就是不需要写一堆if、else语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。