[TOC]
# 組合模式
組合模式是將對象組合成樹形結構,以表示“部分-整體”的層次結構。
在大多數情況下,我們都可以忽略掉組合對象和單個對象之間的差別,從而用一致的方式來處理它們。
- 表示樹形結構:提供了一種遍歷樹形結構的方案,通過調用組合對象的execute方法,程序會遞歸調用組合對象下面的葉對象的execute方法。組合模式可以非常方便地描述對象部分-整體層次結構。
- 利用對象多態性統一對待組合對象和單個對象
## 請求在樹中傳遞的過程
請求從樹最頂端的對象往下傳遞,當前處理請求的對象是葉對象的話,葉對象自身會對請求做出相應的處理;如果當前請求的對象是組合對象,組合對象則會遍歷它屬下的子節點,將請求繼續傳遞給這些子節點。
總而言之,子節點是葉對象,葉對象自身會處理這個請求,而如果子節點還是組合對象,請求會繼續往下傳遞。
## 更強大的宏命令
```javascript
var MacroCommand = function(){
return {
commandList: [],
add: function(command){
this.commandsList.push(command);
},
execute: function(){
for(var i=0, command; command = this.commandList[i++];){
command.execute();
}
}
}
};
var openAcCommand = {
execute: function(){
...
}
};
// Tv、Sound 是相連的
var openTvCommand = {
execute: function(){
...
}
};
var openSoundCommand = {
execute: function(){
...
}
};
var macroCommand1 = MacroCommand();
macroCommand1.add(openTvCommand);
macroCommand1.add(openSoundCommand);
// closeDoor、openPC、QQ 是相連的
var closeDoorCommand = {
execute: function(){
...
}
};
var openPCCommand = {
execute: function(){
...
}
};
var openQQCommand = {
execute: function(){
...
}
};
var macroCommand2 = MacroCommand();
macroCommand2.add(closeDoorCommand);
macroCommand2.add(openPCCommand);
macroCommand1.add(openQQCommand);
var macroCommand = MacroCommand();
macroCommand.add(openAcCommand);
macroCommand.add(macroCommand1);
macroCommand.add(macroCommand2);
macroCommand.execute();
```
基本對象可以被組合成更復雜的組合對象,組合對象又可以被組合,這樣不斷遞歸下去,這棵樹的結構可以支持任意多的復雜度。實際上是對整個樹進行深度優先的搜索。
## 抽象類在組合模式中的作用
在`JavaScript`這種動態語言中,對象的多態性是與生俱來的。`JavaScript`中實現組合模式的難點在于要保證組合對象和葉對象擁有同樣的方法,通常需要用鴨子類型的思想對它們進行接口檢查。
## 透明性帶來的安全問題
組合對象可以擁有子節點,葉對象下面就沒有子節點,所以會發生一些誤操作。解決方案通常從是也給葉對象增加對應的方法,并且在調用的時候拋出異常。
```javascript
var MacroCommand = function(){
return {
commandList: [],
add: function(command){
this.commandsList.push(command);
},
execute: function(){
for(var i=0, command; command = this.commandList[i++];){
command.execute();
}
}
}
};
var openAcCommand = {
execute: function(){
...
},
add: function(){ // 增加相同的方法并拋出異常
throw new Error('葉對象不能添加子節點');
}
};
```
## 一些值得注意的地方
- 組合模式不是父子關系:組合模式是一種`HAS-A 聚合`的關系,不是`IS-A`。
- 對葉對象操作的一致性:對一組葉對象的操作必須具有一致性,只有用一致的方法對待列表中的每個葉對象的時候,才適合使用組合模式。
- 雙向映射關系:引入中介者模式來管理。
- 用職責鏈模式提高組合模式性能:組合模式中,父對象和子對象之間實際上形成了天然的職責鏈。讓請求順著鏈條從父對象往子對象傳遞,或者是反過來從子對象往父對象傳遞,直到遇到可以處理該請求的對象為止。
## 引用父對象
有時需要在子節點上保持對父節點的引用,實際上是從子節點的父節點中查找該節點。
```javascript
var Folder = function(name){
this.name = name;
this.parent = null;
this.files = [];
};
Folder.prototype.add = function(file){
file.parent = this; // 設置父對象
this.files.push(file);
};
Folder.prototype.remove = function(){
if(!this.parent){ // 根節點或者樹外的游離節點
return;
}
for(var files = this.parent.files, l=files.length - 1; l >=0; l--){
var file = files[l];
if(file === this){
files.splice(l,1);
}
}
};
```
首先會判斷`this.parent`,其次再遍歷父節點中保存的子節點列表,找到先要刪除的子節點。
## 何時使用組合模式
- 表示對象的部分-整體層次結構:在開發期間不確定這棵樹到底存在多說層次的時候,在樹的構造最終完成之后,只需要通過請求樹的最頂層對象,便能對整棵樹做統一的操作。
- 客戶希望統一對待樹中的所有對象。組合模式使客戶可以忽略組合對象和葉對象的區別,不用關心當前正在處理的對象是組合對象或者葉對象,不需要寫一堆`if`、`else`語句來分別處理它們。
在組合模式中可能會產生這樣一個系統:系統中的每個對象看起來都與其它對象差不多。它們的區別只有在運行的時候才會顯現出來,這會使代碼難以理解。此外,如果通過組合模式創建了太多的對象,那么這些對象可能會讓系統負擔不起。