組合模式
組合模式,將對象組合成樹形結構以表示“部分-整體”的層次結構,組合模式使得用戶對單個對象和組合對象的使用具有一致性。掌握組合模式的重點是要理解清楚 “部分/整體” 還有 ”單個對象“ 與 "組合對象" 的含義。
組合模式可以讓客戶端像修改配置文件一樣簡單的完成本來需要流程控制語句來完成的功能。
經典案例:系統目錄結構,網站導航結構等。
模式作用:
- 你想表示對象的部分-整體層次結構時
- 你希望用戶忽略組合對象和單個對象的不同,用戶將統一地使用組合結構中的所有對象(方法)
注意事項:
- 該模式經常和裝飾者模式一起使用,它們通常有一個公共的父類(也就是原型),因此裝飾必須支持具有add,remove,getChild操作的component接口
例子:
文件夾和文件之間的關系,非常適合用組合模式來描述.文件夾里既可以包含文件,又可以包含其他文件夾,最終可能組合成一棵樹,組合模式在文件夾的應用中有一下兩層好處.
例如,我在同事的移動硬盤里找到了一些電子書,想把它們復制到F盤中的學習資料文件夾.在復制這些電子書的時候,我并不需要考慮這批文件的類型,不管它們是單獨的電子書還是被放在了文件夾中.組合模式讓Ctrl+V,Ctrl+C成為了一個統一的操作.
當我用殺毒軟件掃描該文件夾時,往往不會關心里面有多少文件和子文件夾,組合模式使得我們只需要操作最外層的文件夾進行掃描
現在我們來編寫代碼,首先分別定義好文件夾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=this.files[i];i++){
file.scan();
}
}
/***********File***********/
var File=function(name){
this.name=name;
}
File.prototype.add=function(){
throw new Error("文件下面不能添加文件");
}
File.prototype.scan=function(){
console.log("開始掃描文件:"+this.name);
}
接下來創建一些文件夾和文件對象,并且讓它們組合成一棵樹,這棵樹就是我們F盤里的現有文件目錄結構:
var folder=new Folder("學習資料");
var folder1=new Folder("JavaScript");
var folder2=new Folder("JQuery");
var file1=new File("JavaScript設計模式與開發實踐")
var file2=new File("精通JQuery");
var file3=new File("重構與模式");
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);
現在的需求是把移動硬盤里的文件和文件夾都復制到這棵樹中,假設我們已經得到了這些文件對象:
var folder3=new Folder("Nodejs");
var file4=new File("深入淺出Node.js");
folder3.add(file4);
var file5=new File("JavaScript語言精髓與編程實踐");
folder.add(folder3);
folder.add(file5);
通過這個例子,我們再次看到客戶是如何同等對待組合對象和葉對象.在添加一批文件的操作過程中,客戶不用分辨它們到底是文件還是文件夾.新增加的文件和文件夾能夠很容易地添加到原來的樹結構中,和樹里已有的對象一起工作
我們改變了樹的結構,添加了新的數據,卻不用修改任何一句原有的代碼,這是符合開放-封閉原則的.
運用了組合模式之后,掃描整個文件夾的操作也是輕而易舉的,我們只需要操作樹的最頂端對象
folder.scan()
執行結果如下如所示:
一些值得注意的地方
- 組合模式不是父子關系
組合模式的樹形結構容易讓人誤以為組合對象和葉對象是父子關系,這是不正確的.
組合模式是一種HAS-A(聚合)的關系,而不是IS-A.組合對象包含一組葉對象,但Leaf并不是composite的子類.組合對象把請求委托給它所包含的所有葉對象,它們能夠合作的關鍵是擁有相同的接口. - 和葉對象操作的一致性
組合模式除了要求組合對象和葉對象擁有相同的接口之外,還有一個必要條件,就是對一組葉對象的操作必須具有一致性.
比如公司要給全體員工發放元旦的過節費1000塊,這個場景可以運用組合模式,但如果公司給今天過生日的員工發送一封生日祝福的郵件,組合模式在這里就沒有用武之地了,除非先把今天過生日的員工挑選出來.只有用一致的方式對待列表中的每個葉對象的時候,才適合使用組合模式 - 雙向映射關系
發放過節費的通知步驟是從公司到各個部門,再到各個小組,最后到每個員工的郵箱里.這本身是個組合模式的好例子,但要考慮的一種情況是,也許某些員工屬于多個組織架構.比如某位架構師既隸屬于開發組,又隸屬于架構組,對象之間的關系并不是嚴格意義上的層次結構,在這種情況下,是不適合使用組合模式的額,該架構師很可能會收到兩份過節費.
這種復合情況下我們必須給父節點和子節點建立雙向映射關系,一個簡單的方法是給小組和員工對象都增加集合來保存對方的引用.但是這種相互間的引用相當復雜,而且對象之間產生了過多的耦合性,修改或者刪除一個對象都變得困難,此時我們可以引入中介者模式來管理這些對象 - 用職責鏈模式來提高組合模式性能
在組合模式中,如果樹的結構比較復雜,節點數量很多,在遍歷樹的過程中,性能方面也許表現得不夠理想.有時候我們確實可以借助一些技巧,在實際操作中避免遍歷整棵樹,有一種現成的方案是借助職責鏈模式.職責鏈模式一般需要我們手動去設置鏈條,但在組合模式中,父對象和自對象之間實際上形成了天然的職責鏈.讓請求順著鏈條從父對象往子對象傳遞,或者是反過來從子對象往父對象傳遞,直到遇到可以處理該請求的對象為止,這也是職責鏈模式的經典運用場景之一.