組合模式和命令模式有點(diǎn)像,命令模式是一個(gè)個(gè)小的指令,而組合模式是一些小指令組合成的大指令
1,命令模式和組合模式的聯(lián)合應(yīng)用
試想這么一個(gè)場(chǎng)景:我們回家之后先關(guān)門,然后開(kāi)電腦,最后打開(kāi)QQ
關(guān)門,開(kāi)電腦,開(kāi)QQ是三個(gè)命令,現(xiàn)在我們用MacroCommand函數(shù)把他們組合起來(lái),得到一個(gè)對(duì)象macroCommand ,通過(guò)macroCommand 來(lái)操作所有的命令。
- macroCommand 被稱作組合對(duì)象,它實(shí)際上是真正的命令數(shù)組commandList的“代理”。當(dāng)然macroCommand不是代理,它只負(fù)責(zé)傳遞請(qǐng)求給真正的命令函數(shù)。
- 關(guān)門,開(kāi)電腦,開(kāi)QQ都是葉對(duì)象。
var closeDoorCommand = {
execute: function () {
console.log('關(guān)門')
}
}
var openPCCommand = {
execute: function () {
console.log('開(kāi)電腦')
}
}
var openQQCommand = {
execute: function () {
console.log('開(kāi)QQ')
}
}
var MacroCommand = function () {
return {
commandList: [],
add: function (command) {
this.commandList.push(command)
},
execute: function () {
for (var i = 0, command; command = this.commandList[i++];) {
command.execute()
}
}
}
}
var macroCommand = MacroCommand()
//宏命令包含了一組子命令,形成了樹(shù)形結(jié)構(gòu)
macroCommand.add(closeDoorCommand)
macroCommand.add(openPCCommand)
macroCommand.add(openQQCommand)
//macroCommand:它是一個(gè)組合對(duì)象,表現(xiàn)為命令,但實(shí)際上只是一組真正命令的代理
macroCommand.execute()
2,分析一下組合模式
在上面的例子中macroCommand是 closeDoorCommand、openPCCommand、openQQCommand這三個(gè)命令的組合對(duì)象,它們有一個(gè)共同點(diǎn):都有execute函數(shù)。這個(gè)函數(shù)代表了組合和單個(gè)命令的一致性。
組合模式將對(duì)象組合成樹(shù)形結(jié)構(gòu),以表示“部分-整體”的結(jié)構(gòu)層次,加上execute實(shí)現(xiàn)的一致性,使得用戶在使用的時(shí)候,可以忽略組合和單個(gè)命令的不同,直接調(diào)用就完事了。
對(duì)于一個(gè)遙控器而言,當(dāng)我們按下一個(gè)鍵時(shí),只關(guān)注它帶來(lái)的結(jié)果,而不需要在意這個(gè)操作調(diào)用了多少命令,只要它有execute,那么它就是好命令。
3,更強(qiáng)大的宏命令
現(xiàn)在我們的遙控器,包含了關(guān)門、開(kāi)電腦、開(kāi)QQ這三個(gè)功能?,F(xiàn)在我們需要一個(gè)超級(jí)遙控器,能控制家里所有的電器,包括:
- 打開(kāi)空調(diào)
- 打開(kāi)電視和音響
- 關(guān)門、開(kāi)電腦、開(kāi)QQ
這時(shí)候我們會(huì)發(fā)現(xiàn),之前的macroCommand現(xiàn)在變成了一個(gè)組合對(duì)象的一部分。
/**
* 正題
* 組合模式就是組合了一堆命令,可以統(tǒng)一調(diào)用,而忽略單個(gè)命令
*/
//更強(qiáng)大的宏命令--只要有execute,你就是他的一員,進(jìn)行深度遍歷
var MacroCommand = function () {
return {
commandList: [],
add: function (command) {
this.commandList.push(command)
},
execute: function () {
for (var i = 0, command; command = this.commandList[i++];) {
command.execute()
}
}
}
}
var openAcCommand = {
execute: function () {
console.log('開(kāi)空調(diào)')
}
}
var openTvCommand = {
execute: function () {
console.log('開(kāi)電視')
}
}
var openSoundCommand = {
execute: function () {
console.log('開(kāi)音響')
}
}
var macroCommand1 = MacroCommand()
macroCommand1.add(openTvCommand)
macroCommand1.add(openSoundCommand)
var closeDoorCommand = {
execute: function () {
console.log('關(guān)門')
}
}
var openPcCommand = {
execute: function () {
console.log('開(kāi)電腦')
}
}
var openQQCommand = {
execute: function () {
console.log('開(kāi)QQ')
}
}
var macroCommand2 = MacroCommand()
macroCommand2.add(closeDoorCommand)
macroCommand2.add(openPcCommand)
macroCommand2.add(openQQCommand)
var macroCommand = MacroCommand()
macroCommand.add(openAcCommand)
macroCommand.add(macroCommand1)
macroCommand.add(macroCommand2)
macroCommand.execute()
錯(cuò)誤處理:
//缺點(diǎn):葉節(jié)點(diǎn)可能會(huì)使用add方法,需要錯(cuò)誤處理
var openAcCommand = {
execute: function () {
console.log('開(kāi)空調(diào)')
},
add: function () {
throw new Error('葉節(jié)點(diǎn)不能添加子對(duì)象')
}
}
openAcCommand.add()
4,組合模式的實(shí)例-掃描文件夾
文件夾和文件之間的聯(lián)系,非常適合用組合模式來(lái)描述(個(gè)人覺(jué)得dom節(jié)點(diǎn)的關(guān)系也很適合)。文件夾里既可以包含文件,又可以包含其他文件夾,最終形成了一棵樹(shù)。組合模式對(duì)于文件夾應(yīng)用有以下兩個(gè)好處:
- 復(fù)制文件夾所有內(nèi)容的時(shí)候,只需要復(fù)制最外層的文件夾就行了
- 用殺毒軟件掃描文件夾的時(shí)候,不需要關(guān)心文件夾里面有多少文件夾或者文件,直接掃描最上層文件夾就可以了。
現(xiàn)在,我們先定義文件夾Folder和文件File這兩個(gè)類:
/*********** Folder ***********/
var Folder = function (name) {
this.name = name
this.files = []
}
Folder.prototype.add = function (file) {
this.files.push(file)
}
Folder.prototype.scan = function () {
console.log('開(kāi)始掃描文件夾:' + this.name)
for (var i = 0, file; file = this.files[i++];) {
file.scan()
}
}
/*********** Folder ***********/
var File = function (name) {
this.name = name
}
File.prototype.add = function () {
throw new Error('文件下面不能添加文件')
}
File.prototype.scan = function () {
console.log('開(kāi)始掃描文件:' + this.name)
}
然后,創(chuàng)建文件夾和文件,將其組合成一棵樹(shù),這個(gè)結(jié)構(gòu)就是我們硬盤里的文件目錄結(jié)構(gòu):
var mainFolder = new Folder('學(xué)習(xí)資料')
var folder1 = new Folder('vue資料')
var folder2 = new Folder('react資料')
var file1 = new File('vue api')
var file2 = new File('vue 生命周期')
var file3 = new File('react-router')
var file4 = new File('設(shè)計(jì)模式')
folder1.add(file1)
folder1.add(file2)
folder2.add(file3)
mainFolder.add(folder1)
mainFolder.add(folder2)
mainFolder.add(file4)
mainFolder.scan()
通過(guò)這個(gè)例子我們可以看到,當(dāng)我們需要遍歷整個(gè)文件夾時(shí),只需要調(diào)用最上層文件夾的scen方法:mainFolder.scan()。在新增文件時(shí),用戶也不需要關(guān)心它們具體是文件夾還是文件,直接添加進(jìn)去就完事了。
5,一些需要注意的地方
- 組合模式是聚合關(guān)系,而不是父子關(guān)系,因?yàn)槿~對(duì)象(最開(kāi)始的單個(gè)命令)不是組合對(duì)象的子類。組合對(duì)象可以把請(qǐng)求委托給它的所有葉對(duì)象,因?yàn)樗鼈冇邢嗤慕涌冢ê蚫om樹(shù)很像)。
- 組合模式的應(yīng)用場(chǎng)景:必須要每個(gè)節(jié)點(diǎn)都有相同的接口,以及操作一致性。比如之前的例子,現(xiàn)在每個(gè)文件節(jié)點(diǎn)都有scan方法,但是如果有的文件有刪除方法,有的沒(méi)有,那么組合模式就不適用,要么都有,那么都沒(méi)有。
- 雙向映射關(guān)系:一個(gè)節(jié)點(diǎn)只能屬于一個(gè)組合對(duì)象,不能同時(shí)屬于兩個(gè),比如一個(gè)文件,只能有一個(gè)直接的文件夾來(lái)包含,不可能同時(shí)有兩個(gè)。如果一個(gè)人屬于開(kāi)發(fā)組,同時(shí)又屬于測(cè)試組,這種交叉情況就不適用于組合模式。
- 組合模式的對(duì)象關(guān)系和職責(zé)鏈模式很像。
6,葉對(duì)象引用父對(duì)象
之前的例子中,只能從父對(duì)象到葉對(duì)象,反過(guò)來(lái)是不行的。但是,當(dāng)我們要?jiǎng)h除某個(gè)文件的時(shí)候,我們需要知道它具體屬于哪個(gè)文件夾,實(shí)際是從上層文件夾中刪除文件的。
首先改寫Folder類和File類,增加parent屬性,在add函數(shù)中設(shè)置parent:
var Folder = function (name) {
this.name = name
this.files = []
}
Folder.prototype.add = function (file) {
//添加父節(jié)點(diǎn)引用
file.parent = this
this.files.push(file)
}
Folder.prototype.scan = function () {
console.log('開(kāi)始掃描文件夾:' + this.name)
for (var i = 0, file; file = this.files[i++];) {
file.scan()
}
}
給文件夾添加刪除功能。
如果this.parent === null,要么它就是根節(jié)點(diǎn),要么是還沒(méi)有添加到樹(shù)中,這種情況先暫時(shí)return,不作處理。
否則的話,該文件夾有父節(jié)點(diǎn),那么就遍歷父節(jié)點(diǎn)的所有子節(jié)點(diǎn),找個(gè)需要?jiǎng)h除的子節(jié)點(diǎn),直接刪除。
//添加刪除功能。
Folder.prototype.delete = function () {
if (this.parent === null) {
return
}
for (var i = 0, files = this.parent.files; file = files[i]; i++) {
if (file === this) {
files.splice(i, 1)
}
}
}
File類的實(shí)現(xiàn)基本一致:
var File = function (name) {
this.name = name
this.parent = null
}
File.prototype.add = function () {
throw new Error('文件下面不能添加文件')
}
File.prototype.scan = function () {
console.log('開(kāi)始掃描文件:' + this.name)
}
File.prototype.delete = function () {
if (this.parent === null) {
return
}
for (var i = 0, files = this.parent.files; file = files[i]; i++) {
if (file === this) {
files.splice(i, 1)
}
}
}
最后,我們來(lái)try一try:
var folder = new Folder('學(xué)習(xí)資料')
var folder1 = new Folder('vue')
var folder2 = new Folder('react')
var file1 = new File('vue api')
var file2 = new File('react api')
var file3 = new File('設(shè)計(jì)模式')
folder1.add(file1)
folder2.add(file2)
folder.add(folder1)
folder.add(folder2)
folder.add(file3)
folder1.delete()
folder.scan()
7,小結(jié)
何時(shí)使用組合模式:
- 表示對(duì)象的“部分-整體”結(jié)構(gòu)層次。組合模式構(gòu)造了一棵樹(shù),來(lái)表示對(duì)象的“部分-整體”結(jié)構(gòu),用戶不需要知道樹(shù)到底有多少層,只需要請(qǐng)求最頂層的節(jié)點(diǎn),就可以對(duì)整棵樹(shù)做統(tǒng)一的操作。
- 樹(shù)中的所有對(duì)象都一致??蛻舨恍枰P(guān)注一個(gè)節(jié)點(diǎn)到底是組合對(duì)象還是葉對(duì)象,因?yàn)樗鼈冇邢嗤姆椒?,能用就完事了?/li>