組合模式和命令模式有點像,命令模式是一個個小的指令,而組合模式是一些小指令組合成的大指令
1,命令模式和組合模式的聯合應用
試想這麼一個場景:我們回家之後先關門,然後開電腦,最後打開QQ
關門,開電腦,開QQ是三個命令,現在我們用MacroCommand函數把他們組合起來,得到一個對象macroCommand ,通過macroCommand 來操作所有的命令。
- macroCommand 被稱作組合對象,它實際上是真正的命令數組commandList的“代理”。當然macroCommand不是代理,它只負責傳遞請求給真正的命令函數。
- 關門,開電腦,開QQ都是葉對象。
var closeDoorCommand = {
execute: function () {
console.log('關門')
}
}
var openPCCommand = {
execute: function () {
console.log('開電腦')
}
}
var openQQCommand = {
execute: function () {
console.log('開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()
//宏命令包含了一組子命令,形成了樹形結構
macroCommand.add(closeDoorCommand)
macroCommand.add(openPCCommand)
macroCommand.add(openQQCommand)
//macroCommand:它是一個組合對象,表現爲命令,但實際上只是一組真正命令的代理
macroCommand.execute()
2,分析一下組合模式
在上面的例子中macroCommand是 closeDoorCommand、openPCCommand、openQQCommand這三個命令的組合對象,它們有一個共同點:都有execute函數。這個函數代表了組合和單個命令的一致性。
組合模式將對象組合成樹形結構,以表示“部分-整體”的結構層次,加上execute實現的一致性,使得用戶在使用的時候,可以忽略組合和單個命令的不同,直接調用就完事了。
對於一個遙控器而言,當我們按下一個鍵時,只關注它帶來的結果,而不需要在意這個操作調用了多少命令,只要它有execute,那麼它就是好命令。
3,更強大的宏命令
現在我們的遙控器,包含了關門、開電腦、開QQ這三個功能。現在我們需要一個超級遙控器,能控制家裏所有的電器,包括:
- 打開空調
- 打開電視和音響
- 關門、開電腦、開QQ
這時候我們會發現,之前的macroCommand現在變成了一個組合對象的一部分。
/**
* 正題
* 組合模式就是組合了一堆命令,可以統一調用,而忽略單個命令
*/
//更強大的宏命令--只要有execute,你就是他的一員,進行深度遍歷
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('開空調')
}
}
var openTvCommand = {
execute: function () {
console.log('開電視')
}
}
var openSoundCommand = {
execute: function () {
console.log('開音響')
}
}
var macroCommand1 = MacroCommand()
macroCommand1.add(openTvCommand)
macroCommand1.add(openSoundCommand)
var closeDoorCommand = {
execute: function () {
console.log('關門')
}
}
var openPcCommand = {
execute: function () {
console.log('開電腦')
}
}
var openQQCommand = {
execute: function () {
console.log('開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()
錯誤處理:
//缺點:葉節點可能會使用add方法,需要錯誤處理
var openAcCommand = {
execute: function () {
console.log('開空調')
},
add: function () {
throw new Error('葉節點不能添加子對象')
}
}
openAcCommand.add()
4,組合模式的實例-掃描文件夾
文件夾和文件之間的聯繫,非常適合用組合模式來描述(個人覺得dom節點的關係也很適合)。文件夾裏既可以包含文件,又可以包含其他文件夾,最終形成了一棵樹。組合模式對於文件夾應用有以下兩個好處:
- 複製文件夾所有內容的時候,只需要複製最外層的文件夾就行了
- 用殺毒軟件掃描文件夾的時候,不需要關心文件夾裏面有多少文件夾或者文件,直接掃描最上層文件夾就可以了。
現在,我們先定義文件夾Folder和文件File這兩個類:
/*********** 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('開始掃描文件夾:' + 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('開始掃描文件:' + this.name)
}
然後,創建文件夾和文件,將其組合成一棵樹,這個結構就是我們硬盤裏的文件目錄結構:
var mainFolder = new Folder('學習資料')
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('設計模式')
folder1.add(file1)
folder1.add(file2)
folder2.add(file3)
mainFolder.add(folder1)
mainFolder.add(folder2)
mainFolder.add(file4)
mainFolder.scan()
通過這個例子我們可以看到,當我們需要遍歷整個文件夾時,只需要調用最上層文件夾的scen方法:mainFolder.scan()。在新增文件時,用戶也不需要關心它們具體是文件夾還是文件,直接添加進去就完事了。
5,一些需要注意的地方
- 組合模式是聚合關係,而不是父子關係,因爲葉對象(最開始的單個命令)不是組合對象的子類。組合對象可以把請求委託給它的所有葉對象,因爲它們有相同的接口(和dom樹很像)。
- 組合模式的應用場景:必須要每個節點都有相同的接口,以及操作一致性。比如之前的例子,現在每個文件節點都有scan方法,但是如果有的文件有刪除方法,有的沒有,那麼組合模式就不適用,要麼都有,那麼都沒有。
- 雙向映射關係:一個節點只能屬於一個組合對象,不能同時屬於兩個,比如一個文件,只能有一個直接的文件夾來包含,不可能同時有兩個。如果一個人屬於開發組,同時又屬於測試組,這種交叉情況就不適用於組合模式。
- 組合模式的對象關係和職責鏈模式很像。
6,葉對象引用父對象
之前的例子中,只能從父對象到葉對象,反過來是不行的。但是,當我們要刪除某個文件的時候,我們需要知道它具體屬於哪個文件夾,實際是從上層文件夾中刪除文件的。
首先改寫Folder類和File類,增加parent屬性,在add函數中設置parent:
var Folder = function (name) {
this.name = name
this.files = []
}
Folder.prototype.add = function (file) {
//添加父節點引用
file.parent = this
this.files.push(file)
}
Folder.prototype.scan = function () {
console.log('開始掃描文件夾:' + this.name)
for (var i = 0, file; file = this.files[i++];) {
file.scan()
}
}
給文件夾添加刪除功能。
如果this.parent === null,要麼它就是根節點,要麼是還沒有添加到樹中,這種情況先暫時return,不作處理。
否則的話,該文件夾有父節點,那麼就遍歷父節點的所有子節點,找個需要刪除的子節點,直接刪除。
//添加刪除功能。
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類的實現基本一致:
var File = function (name) {
this.name = name
this.parent = null
}
File.prototype.add = function () {
throw new Error('文件下面不能添加文件')
}
File.prototype.scan = function () {
console.log('開始掃描文件:' + 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)
}
}
}
最後,我們來try一try:
var folder = new Folder('學習資料')
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('設計模式')
folder1.add(file1)
folder2.add(file2)
folder.add(folder1)
folder.add(folder2)
folder.add(file3)
folder1.delete()
folder.scan()
7,小結
何時使用組合模式:
- 表示對象的“部分-整體”結構層次。組合模式構造了一棵樹,來表示對象的“部分-整體”結構,用戶不需要知道樹到底有多少層,只需要請求最頂層的節點,就可以對整棵樹做統一的操作。
- 樹中的所有對象都一致。客戶不需要關注一個節點到底是組合對象還是葉對象,因爲它們有相同的方法,能用就完事了。