Javascript-設計模式_組合模式
簡介
我們平時開發過程中,一定會遇到這種情況:同時處理簡單物件和由簡單物件組成的複雜物件,這些簡單物件和複雜物件會組合成樹形結構,在客戶端對其處理的時候要保持一致性。比如電商網站中的產品訂單,每一張產品訂單可能有多個子訂單組合,比如作業系統的資料夾,每個資料夾有多個子資料夾或檔案,我們作為使用者對其進行復制,刪除等操作時,不管是資料夾還是檔案,對我們操作者來說是一樣的。在這種場景下,就非常適合使用組合模式來實現
組合模式
定義
是用小的子物件來構建更大的 物件,而這些小的子物件本身也許是由更小 的“孫物件”構成的
基本知識
組合模式也是結構型設計模式的一種,它主要體現了整體與部分的誒關係,其典型的應用就是樹形結構。組合是一組物件,其中的物件可能包含一個其他物件,也可能包含一組其他物件
組合模式:將物件組合成樹形結構以表示“部分-整體”的層次結構,組合模式使得使用者對單個物件和組合物件的使用具有一致性
在使用組合模式的使用要注意以下兩點
-
組合中既要能包含個體,也要能包含其他組合
-
要抽象出物件和組合的公共特性
組合模式主要有三個角色
-
抽象元件(Component):抽象類,主要定義了參與組合的物件的公共介面
-
子物件(Leaf):組成組合物件的最基本物件
-
組合物件(Composite):由子物件組合起來的複雜物件
理解組合模式的關鍵是要理解組合模式對單個物件和組合物件使用的一致性,如下解析組合模式的實現加深理解
核心
可以用樹形結構來表示這種“部分- 整體”的層次結構
呼叫組合物件的execute方法,程式會遞迴呼叫組合物件 下面的葉物件的execute方法,在後續的實現中體現
但要注意的是,組合模式不是父子關係,它是一種HAS-A(聚合)的關係,將請求委託給 它所包含的所有葉物件,基於這種委託,就需要保證組合物件和葉物件擁有相同的介面
此外,也要保證用一致的方式對待 列表中的每個葉物件,即葉物件屬於同一類,不需要過多特殊的額外操作
實現
巨集命令物件包含了一組具體的子命令物件,不管是巨集命令物件,還是子命令物件,都有 一個execute方法負責執行命令
現在我們來造一個“萬能遙控器”
// 新建一個關門的命令 const closeDoorCommand = { execute: function(){ console.log( '關門' ) } } // 新建一個開電腦的命令 const openPcCommand = { execute: function(){ console.log( '開電腦' ) } }; // 登陸QQ的命令 const openQQCommand = { execute: function(){ console.log( '登入QQ' ) } }; // 建立一個巨集命令 const MacroCommand = function(){ return { // 巨集命令的子命令列表 commandsList: [], // 新增命令到子命令列表 add: function( command ){ this.commandsList.push( command ) }, // 依次執行子命令列表裡面的命令 execute: function(){ for ( var i = 0, command; command = this.commandsList[ i++ ]; ){ command.execute() } } } } const macroCommand = MacroCommand() macroCommand.add( closeDoorCommand ) macroCommand.add( openPcCommand ) macroCommand.add( openQQCommand ) macroCommand.execute()
其中,marcoCommand被稱為組合物件,closeDoorCommand、openPcCommand、openQQCommand都是葉物件。在macroCommand的execute方法裡,並不執行真正的操作,而是遍歷它所包含的葉物件,把真正的execute請求委託給這些葉物件
macroCommand表現得像一個命令,但它實際上只是一組真正命令的“代理”。並非真正的代理,雖然結構上相似,但macroCommand只負責傳遞請求給葉物件,它的目的不在於控制對葉物件的訪問
組合模式的用途
組合模式將物件組合成樹形結構,以表示“部分-整體”的層次結構。除了用來表示樹形結構之外,組合模式的另一個好處是通過物件的多型性表現,使得使用者對單個物件和組合物件的使用具有一致性
-
表示樹形結構。組合模式有一個優點:提供了一種遍歷樹形結構的方案,通過呼叫組合物件的execute方法,程式會遞迴呼叫組合物件下面的葉物件的execute方法,所以我們的萬能遙控器只需要一次操作,便能依次完成關 門、開啟電腦、登入QQ這幾件事情。組合模式可以非常方便地描述物件部分-整體層次結構
-
利用物件多型性統一對待組合物件和單個物件。利用物件的多型性表現,可以使客戶端忽略組合物件和單個物件的不同。在組合模式中,客戶將統一地使用組合結構中的所有物件,而不需要關心它究竟是組合物件還是單個物件
這在實際開發中會給客戶帶來相當大的便利性,當我們往萬能遙控器裡面新增一個命令的時候,並不關心這個命令是巨集命令還是普通子命令。這點對於我們不重要,我們只需要確定它是一個命令,並且這個命令擁有可執行的execute方法,那麼這個命令就可以被新增進萬能遙控器
當巨集命令和普通子命令接收到執行execute方法的請求時,巨集命令和普通子命令都會做它們各自認為正確的事情。這些差異是隱藏在客戶背後的,在客戶看來,這種透明性可以讓我們非常自由地擴充套件這個萬能遙控器
更強大的巨集命令
目前的“萬能遙控器”,包含了關門、開電腦、登入QQ這3個命令。現在我們需要一個“超級萬能遙控器”,可以控制家裡所有的電器,這個遙控器擁有以下功能
-
開啟空調
-
開啟電視和音響
-
關門、開電腦、登入QQ
// 建立一個巨集命令 const MacroCommand = function(){ return { // 巨集命令的子命令列表 commandsList: [], // 新增命令到子命令列表 add: function( command ){ this.commandsList.push( command ) }, // 依次執行子命令列表裡面的命令 execute: function(){ for ( var i = 0, command; command = this.commandsList[ i++ ]; ){ command.execute() } } } } <!--開啟空調命令--> const openAcCommand = { execute: function(){ console.log( '開啟空調' ) } } <!--開啟電視和音響--> const openTvCommand = { execute: function(){ console.log( '開啟電視' ) } } var openSoundCommand = { execute: function(){ console.log( '開啟音響' ) } } // 建立一個巨集命令 const macroCommand1 = MacroCommand() // 把開啟電視裝進這個巨集命令裡 macroCommand1.add(openTvCommand) // 把開啟音響裝進這個巨集命令裡 macroCommand1.add(openSoundCommand) <!--關門、開啟電腦和打登入QQ的命令--> const closeDoorCommand = { execute: function(){ console.log( '關門' ) } } const openPcCommand = { execute: function(){ console.log( '開電腦' ) } } const openQQCommand = { execute: function(){ console.log( '登入QQ' ) } }; //建立一個巨集命令 const macroCommand2 = MacroCommand() //把關門命令裝進這個巨集命令裡 macroCommand2.add( closeDoorCommand ) //把開電腦命令裝進這個巨集命令裡 macroCommand2.add( openPcCommand ) //把登入QQ命令裝進這個巨集命令裡 macroCommand2.add( openQQCommand ) <!--把各巨集命令裝進一個超級命令中去--> const macroCommand = MacroCommand() macroCommand.add( openAcCommand ) macroCommand.add( macroCommand1 ) macroCommand.add( macroCommand2 )
透明性帶來的安全問題
組合模式的透明性使得發起請求的客戶不用去顧忌樹中組合物件和葉物件的區別,但它們在本質上有是區別的
組合物件可以擁有子節點,葉物件下面就沒有子節點,所以我們也許會發生一些誤操作,比如試圖往葉物件中新增子節點。解決方案通常是給葉物件也增加add方法,並且在呼叫這個方法時,丟擲一個異常來及時提醒客戶
後續
優缺點
-
優點:可以方便地構造一棵樹來表示物件的部分-整體 結構。在樹的構造最終 完成之後,只需要通過請求樹的最頂層對 象,便能對整棵樹做統一一致的操作
-
缺點:創建出來的物件長得都差不多,可能會使程式碼不好理解,建立太多的物件對效能也會有一些影響
一些值得注意的地方
-
組合模式不是父子關係
組合模式的樹型結構容易讓人誤以為組合物件和葉物件是父子關係,這是不正確的
-
對葉物件操作的一致性
組合模式除了要求組合物件和葉物件擁有相同的介面之外,還有一個必要條件,就是對一組葉物件的操作必須具有一致性。
比如公司要給全體員工發放元旦的過節費1000塊,這個場景可以運用組合模式,但如果公司給今天過生日的員工傳送一封生日祝福的郵件,組合模式在這裡就沒有用武之地了,除非先把今天過生日的員工挑選出來。只有用一致的方式對待列表中的每個葉物件的時候,才適合使用組合模式
-
雙向對映關係
發放過節費的通知步驟是從公司到各個部門,再到各個小組,最後到每個員工的郵箱裡。這本身是一個組合模式的好例子,但要考慮的一種情況是,也許某些員工屬於多個組織架構。比如某位架構師既隸屬於開發組,又隸屬於架構組,物件之間的關係並不是嚴格意義上的層次結構,在這種情況下,是不適合使用組合模式的,該架構師很可能會收到兩份過節費
-
用職責鏈模式提高組合模式效能
在組合模式中,如果樹的結構比較複雜,節點數量很多,在遍歷樹的過程中,效能方面也許表現得不夠理想。有時候我們確實可以藉助一些技巧,在實際操作中避免遍歷整棵樹,有一種現成的方案是藉助職責鏈模式。職責鏈模式一般需要我們手動去設定鏈條,但在組合模式中,父物件和子物件之間實際上形成了天然的職責鏈。讓請求順著鏈條從父物件往子物件傳遞,或者是反過來從子物件往父物件傳遞,直到遇到可以處理該請求的物件為止,這也是職責鏈模式的經典運用場景之一
何時使用組合模式
組合模式如果運用得當,可以大大簡化客戶的程式碼。一般來說,組合模式適用於以下這兩種情況
-
表示物件的部分整體層次結構。組合模式可以方便地構造一棵樹來表示物件的部分整體結構。特別是我們在開發期間不確定這棵樹到底存在多少層次的時候。在樹的構造最終完成之後,只需要通過請求樹的最頂層物件,便能對整棵樹做統一的操作。在組合模式中增加和刪除樹的節點非常方便,並且符合開放封閉原則
-
客戶希望統一對待樹中的所有物件。組合模式使客戶可以忽略組合物件和葉物件的區別,客戶在面對這棵樹的時候,不用關心當前正在處理的物件是組合物件還是葉物件,也就不用寫一堆 if 、 else 語句來分別處理它們。組合物件和葉物件會各自做自己正確的事情,這是組合模式最重要的能力
總結
組合模式並不難理解,它主要解決的是單一物件和組合物件在使用方式上的一致性問題。如果物件具有明顯的層次結構並且想要統一地使用它們,這就非常適合使用組合模式。在Web開發中,這種層次結構非常常見,很適合使用組合模式,尤其是對於JS來說,不用拘泥於傳統面嚮物件語言的形式,靈活地利用JS語言的特性,達到對部分和整體使用的一致性