JavaScript 設(shè)計模式之組合模式

我們知道地球和一些其他行星圍繞著太陽旋轉(zhuǎn),也知道在一個原子中,有許多電子圍繞著原子核旋轉(zhuǎn)。我曾經(jīng)想象,我們的太陽系也許是一個更大世界里的一個原子,地球只是圍繞著太陽原子的一個電子。而我身上的每個原子又是一個星系,原子核就是這個星系中的恒星,電子是圍繞著恒星旋轉(zhuǎn)的行星。一個電子中也許還包含了另一個宇宙,雖然這個宇宙還不能被顯微鏡看到,但我相信它的存在。

也許這個想法有些異想天開,但在程序設(shè)計中,也有一些和“事物是由相似的子事物構(gòu)成”類似的思想。組合模式就是用小的子對象來構(gòu)建更大的對象,而這些小的子對象本身也許是由更小的“孫對象”構(gòu)成的。

回顧宏命令

宏命令對象包含了一組具體的子命令對象,不管是宏命令對象,還是子命令對象,都有
一個execute方法負責執(zhí)行命令。

現(xiàn)在我們來造一個“萬能遙控器”

// 新建一個關(guān)門的命令
var closeDoorCommand = {
    execute: function(){
        console.log( '關(guān)門' );
    }
};
// 新建一個開電腦的命令
var openPcCommand = {
    execute: function(){
        console.log( '開電腦' );
    }
};
// 登陸QQ的命令
var openQQCommand = {
    execute: function(){
        console.log( '登錄QQ' );
    }
};

// 創(chuàng)建一個宏命令
var MacroCommand = function(){
    return {
        // 宏命令的子命令列表
        commandsList: [],
        // 添加命令到子命令列表
        add: function( command ){
            this.commandsList.push( command );
        },
        // 依次執(zhí)行子命令列表里面的命令
        execute: function(){
            for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
                command.execute();
            }
        }
    }
};

var macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );
macroCommand.execute();

通過觀察這段代碼,我們很容易發(fā)現(xiàn),宏命令中包含了一組子命令,它們組成了一個樹形結(jié)構(gòu),這里是一棵結(jié)構(gòu)非常簡單的樹

image

其中,marcoCommand被稱為組合對象,closeDoorCommand、openPcCommand、openQQCommand都是葉對象。在macroCommand的execute方法里,并不執(zhí)行真正的操作,而是遍歷它所包含的葉對象,把真正的execute請求委托給這些葉對象。

macroCommand表現(xiàn)得像一個命令,但它實際上只是一組真正命令的“代理”。并非真正的代理,雖然結(jié)構(gòu)上相似,但macroCommand只負責傳遞請求給葉對象,它的目的不在于控制對葉對象的訪問。

組合模式的用途

組合模式將對象組合成樹形結(jié)構(gòu),以表示“部分-整體”的層次結(jié)構(gòu)。除了用來表示樹形結(jié)構(gòu)之外,組合模式的另一個好處是通過對象的多態(tài)性表現(xiàn),使得用戶對單個對象和組合對象的使用具有一致性,下面分別說明。

  • 表示樹形結(jié)構(gòu)。通過回顧上面的例子,我們很容易找到組合模式的一個優(yōu)點:提供了一種遍歷樹形結(jié)構(gòu)的方案,通過調(diào)用組合對象的execute方法,程序會遞歸調(diào)用組合對象下面的葉對象的execute方法,所以我們的萬能遙控器只需要一次操作,便能依次完成關(guān)
    門、打開電腦、登錄QQ這幾件事情。組合模式可以非常方便地描述對象部分-整體層次結(jié)構(gòu)。
  • 利用對象多態(tài)性統(tǒng)一對待組合對象和單個對象。利用對象的多態(tài)性表現(xiàn),可以使客戶端忽略組合對象和單個對象的不同。在組合模式中,客戶將統(tǒng)一地使用組合結(jié)構(gòu)中的所有對象,而不需要關(guān)心它究竟是組合對象還是單個對象。

這在實際開發(fā)中會給客戶帶來相當大的便利性,當我們往萬能遙控器里面添加一個命令的時候,并不關(guān)心這個命令是宏命令還是普通子命令。這點對于我們不重要,我們只需要確定它是一個命令,并且這個命令擁有可執(zhí)行的execute方法,那么這個命令就可以被添加進萬能遙控器。

當宏命令和普通子命令接收到執(zhí)行execute方法的請求時,宏命令和普通子命令都會做它們各自認為正確的事情。這些差異是隱藏在客戶背后的,在客戶看來,這種透明性可以讓我們非常自由地擴展這個萬能遙控器。

請求在樹中傳遞的過程

在組合模式中,請求在樹中傳遞的過程總是遵循一種邏輯。

以宏命令為例,請求從樹最頂端的對象往下傳遞,如果當前處理請求的對象是葉對象(普通子命令),葉對象自身會對請求作出相應的處理;如果當前處理請求的對象是組合對象(宏命令),組合對象則會遍歷它屬下的子節(jié)點,將請求繼續(xù)傳遞給這些子節(jié)點。

總而言之,如果子節(jié)點是葉對象,葉對象自身會處理這個請求,而如果子節(jié)點還是組合對象,請求會繼續(xù)往下傳遞。葉對象下面不會再有其他子節(jié)點,一個葉對象就是樹的這條枝葉的盡頭,組合對象下面可能還會有子節(jié)點

image

請求從上到下沿著樹進行傳遞,直到樹的盡頭。作為客戶,只需要關(guān)心樹最頂層的組合對象,客戶只需要請求這個組合對象,請求便會沿著樹往下傳遞,依次到達所有的葉對象。

由于上面這個宏命令和子命令組成的樹太過簡單,我們還不能清楚地看到組合模式帶來的好處,如果只是簡單地遍歷一組子節(jié)點,迭
代器便能解決所有的問題。接下來我們將創(chuàng)造一個更強大的宏命令,這個宏命令中又包含了另外一些宏命令和普通子命令,看起來是一棵相對較復雜的樹。

更強大的宏命令

目前的“萬能遙控器”,包含了關(guān)門、開電腦、登錄QQ這3個命令。現(xiàn)在我們需要一個“超級萬能遙控器”,可以控制家里所有的電器,這個遙控器擁有以下功能:

  • 打開空調(diào)
  • 打開電視和音響
  • 關(guān)門、開電腦、登錄QQ
// 創(chuàng)建一個宏命令
var MacroCommand = function(){
    return {
        // 宏命令的子命令列表
        commandsList: [],
        // 添加命令到子命令列表
        add: function( command ){
            this.commandsList.push( command );
        },
        // 依次執(zhí)行子命令列表里面的命令
        execute: function(){
            for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
                command.execute();
            }
        }
    }
};

<!--打開空調(diào)命令-->
var openAcCommand = {
    execute: function(){
        console.log( '打開空調(diào)' );
    }
};

<!--打開電視和音響-->
var openTvCommand = {
    execute: function(){
        console.log( '打開電視' );
    }
};
var openSoundCommand = {
    execute: function(){
        console.log( '打開音響' );
    }
};
//創(chuàng)建一個宏命令
var macroCommand1 = MacroCommand();
//把打開電視裝進這個宏命令里
macroCommand1.add(openTvCommand)
//把打開音響裝進這個宏命令里
macroCommand1.add(openSoundCommand)

<!--關(guān)門、打開電腦和打登錄QQ的命令-->
var closeDoorCommand = {
    execute: function(){
        console.log( '關(guān)門' );
    }
};
var openPcCommand = {
    execute: function(){
        console.log( '開電腦' );
    }
};
var openQQCommand = {
    execute: function(){
        console.log( '登錄QQ' );
    }
};
//創(chuàng)建一個宏命令
var macroCommand2 = MacroCommand();
//把關(guān)門命令裝進這個宏命令里
macroCommand2.add( closeDoorCommand );
//把開電腦命令裝進這個宏命令里
macroCommand2.add( openPcCommand );
//把登錄QQ命令裝進這個宏命令里
macroCommand2.add( openQQCommand );

<!--把各宏命令裝進一個超級命令中去-->
var macroCommand = MacroCommand();
macroCommand.add( openAcCommand );
macroCommand.add( macroCommand1 );
macroCommand.add( macroCommand2 );

從以上代碼可以看出基本對象可以被組合成更復雜的組合對象,合對象又可以被組合,這樣不斷遞歸下去,這棵樹的結(jié)構(gòu)可以支持任意多的復雜度。在樹最終被構(gòu)造完成之后,讓整顆樹最終運轉(zhuǎn)起來的步驟非常簡單,只需要調(diào)用最上層對象的execute方法。每當對最上層的對象進行一次請求時,實際上是在對整個樹進行深度優(yōu)先的搜索,而創(chuàng)建組合對象的程序員并不關(guān)心這些內(nèi)在的細節(jié),往這棵樹里面添加一些新的節(jié)點對象是非常容易的事情。

透明性帶來的安全問題

組合模式的透明性使得發(fā)起請求的客戶不用去顧忌樹中組合對象和葉對象的區(qū)別,但它們在本質(zhì)上有是區(qū)別的。

組合對象可以擁有子節(jié)點,葉對象下面就沒有子節(jié)點,所以我們也許會發(fā)生一些誤操作,比如試圖往葉對象中添加子節(jié)點。解決方案通常是給葉對象也增加add方法,并且在調(diào)用這個方法時,拋出一個異常來及時提醒客戶,

一些值得注意的地方

在使用組合模式的時候,還有以下幾個值得我們注意的地方。

1.組合模式不是父子關(guān)系

組合模式的樹型結(jié)構(gòu)容易讓人誤以為組合對象和葉對象是父子關(guān)系,這是不正確的。

2.對葉對象操作的一致性

組合模式除了要求組合對象和葉對象擁有相同的接口之外,還有一個必要條件,就是對一組葉對象的操作必須具有一致性。

比如公司要給全體員工發(fā)放元旦的過節(jié)費1000塊,這個場景可以運用組合模式,但如果公司給今天過生日的員工發(fā)送一封生日祝福的郵件,組合模式在這里就沒有用武之地了,除非先把今天過生日的員工挑選出來。只有用一致的方式對待列表中的每個葉對象的時候,才適合使用組合模式。

3.雙向映射關(guān)系

發(fā)放過節(jié)費的通知步驟是從公司到各個部門,再到各個小組,最后到每個員工的郵箱里。這本身是一個組合模式的好例子,但要考慮的一種情況是,也許某些員工屬于多個組織架構(gòu)。比如某位架構(gòu)師既隸屬于開發(fā)組,又隸屬于架構(gòu)組,對象之間的關(guān)系并不是嚴格意義上的層次結(jié)構(gòu),在這種情況下,是不適合使用組合模式的,該架構(gòu)師很可能會收到兩份過節(jié)費。

4.用職責鏈模式提高組合模式性能

在組合模式中,如果樹的結(jié)構(gòu)比較復雜,節(jié)點數(shù)量很多,在遍歷樹的過程中,性能方面也許表現(xiàn)得不夠理想。有時候我們確實可以借助一些技巧,在實際操作中避免遍歷整棵樹,有一種現(xiàn)成的方案是借助職責鏈模式。職責鏈模式一般需要我們手動去設(shè)置鏈條,但在組合模式中,父對象和子對象之間實際上形成了天然的職責鏈。讓請求順著鏈條從父對象往子對象傳遞,或者是反過來從子對象往父對象傳遞,直到遇到可以處理該請求的對象為止,這也是職責鏈模式的經(jīng)典運用場景之一。

何時使用組合模式

組合模式如果運用得當,可以大大簡化客戶的代碼。一般來說,組合模式適用于以下這兩種情況。

  • 表示對象的部分-整體層次結(jié)構(gòu)。組合模式可以方便地構(gòu)造一棵樹來表示對象的部分-整體結(jié)構(gòu)。特別是我們在開發(fā)期間不確定這棵
    樹到底存在多少層次的時候。在樹的構(gòu)造最終完成之后,只需要通過請求樹的最頂層對象,便能對整棵樹做統(tǒng)一的操作。在組合模式
    中增加和刪除樹的節(jié)點非常方便,并且符合開放-封閉原則。

  • 客戶希望統(tǒng)一對待樹中的所有對象。組合模式使客戶可以忽略組合對象和葉對象的區(qū)別,客戶在面對這棵樹的時候,不用關(guān)心當前正在處理的對象是組合對象還是葉對象,也就不用寫一堆if、else語句來分別處理它們。組合對象和葉對象會各自做自己正確的事
    情,這是組合模式最重要的能力。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容