javascript設計模式 之 7組合模式

zhaoyezi發表於2018-06-01

1 組合模式的定義

組合模式:用小的子物件來構成更大的物件,而這些小的物件本身也許是由更小的孫物件構成的。
我理解的例如:一個人,是由肌肉+肥肉+骨頭+器官構成。而肌肉,肥肉等又是又化學物質構成,化學物質裡面還分為很多細小的鈣鐵鋅硒等元素組成....。由小到大最終組成了一個人。

2 巨集命令例子

巨集命令是一組命令的集合,通過執行巨集命令的方式,可以一次執行一批命令。家裡有一個萬能遙控器,每天回家的時候,只要按一個特別的按鈕,它就會幫我們關上房間門,順便開啟電視,開始煮飯。

var closeDoorCommand = {
    execute: function() {
        console.log('close door');
    }
};
var openTVCommand = {
    execute: function() {
        console.log('open TV');
    };
};
var cookCommand = {
    execute: function() {
        console.log('cook');
    }
};

var MacroCommand = function() {
    var cache = [];
    return {
        add: function(command) {
            cache.push(command);
        },
        execute: function() {
            cache.map(function(command) {
                command.execute()
            });
        }
    };
}

var macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openTVCommand );
macroCommand.add( cookCommand );
macroCommand.execute();
複製程式碼

通過觀察這段程式碼,我們很容易發現,巨集命令中包含了一組子命令。看起來像是在執行macroCommand的execute方法,表現的像一個命令,但是實際只是一組命令的‘代理’,並非正真的代理,雖然結構相似,但是macroCommand只負責傳遞請求給葉子物件,不低不在於控制對葉子物件的訪問。

3 組合模式傳遞

  • 組合模式將物件組合成為樹形結構,以表示部分-整體的層次結構。通過呼叫組合物件的execute方法,程式便會遞迴呼叫組合物件下面的葉物件的execute方法。
  • 組合物件與葉物件有物件多型性,客戶端可以忽略組合物件與單個物件的不同。在組合模式中,客戶將統一使用組合物件結構中的所有物件,而不需要關係它究竟是組合物件還是單個物件。 實際開發中,我們往組合物件中新增一個命令時,並不會關心這個命令時巨集命令還是普通名,只要它是一個命令並擁有execute方法,那麼就能被新增到組合物件(例如上面的萬能遙控器)中。當巨集命令和子命令接收到執行請求時,它們就會做自己認為正確的事。這些差異隱藏在客戶背後。對於客戶來說,只看到了組合物件(萬能遙控器)

組合模式的樹形結構,讓它的呼叫遵循一種自上而下的請求順序規則。例如上面的巨集命令物件呼叫後,就會將它下面的子命令一個一個呼叫。如果子命令中還有子命令,也會繼續往下傳遞,直到不再有其他子節點。

javascript設計模式 之 7組合模式

4 更強大的巨集命令

  • 基本物件可以被組合成為複雜的組合物件,組合物件又可以被組合,這樣不斷地遞迴下去,這棵樹可以支援任意多的複雜度。而想讓這棵樹執行起來很簡單,只需要執行execute方法就可以了。
  • 組合模式的透明性讓客戶不用顧忌樹中組合物件與葉子物件的區別,但是實際葉子物件不能再新增子節點。因此為了保證一致性,可以為葉子節點也新增add方法並丟擲不能新增的提示資訊。
    javascript設計模式 之 7組合模式

    上面的萬能遙控器只有一層子節點。現在我們來實現一個多層節點的遙控器。
// 定義一個組合命令的功能函式
var MachroCommand = function() {
    var cache = [];
    return {
        add: function(command) {
            cache.push(command);
        },
        execute: function() {
            cache.map(function(command) {
                command.execute()
            });
        }
    };
};

var closeDoorCommand = {
    execute: function() {
        console.log('close door');
    },
    add: function() {
        throw Error('can\'t add child node');
    }
};
var openTVCommand = {
    execute: function() {
        console.log('open TV');
    },
     add: function() {
        throw Error('can\'t add child node');
    }
};

var openAirConditionCommand = {
    execute: function() {
        console.log('open 冰箱');
    },
     add: function() {
        throw Error('can\'t add child node');
    }
};

var openComputerCommand = {
    execute: function() {
        console.log('open computer');
    },
     add: function() {
        throw Error('can\'t add child node');
    }
};

// 組合命令(開啟家電)
var openApplianceCommand = MachroCommand();
openApplianceCommand.add(openTVCommand);
openApplianceCommand.add(openAirConditionCommand);

// 組合命令(萬能遙控器)
var machroCommand = MachroCommand();
machroCommand.add(openApplianceCommand);
machroCommand.add(openComputerCommand);
machroCommand.add(closeDoorCommand);

// 呼叫萬能遙控器
machroCommand.execute();
複製程式碼

5 組合模式掃描資料夾

當我們將資料夾從A處複製到B處, 只需要使用Ctrl + C, Ctrl + v 命令就可以了。而這裡來的複製和貼上就是組合命令,檔案的層級關係是:

  • 資料夾(組合物件)可包含資料夾(組合物件)和檔案(子節點)
  • 檔案不能包含檔案或資料夾
// 檔案
Var File = function(name) {
    return {
        add: function() {
            console.log('can not add child file');
        },
        execute: function() {
            console.log('複製檔案: ' + name);
        },
        scan: function() {
            console.log('掃描檔案: ' + name);
        }
    }
};

// 資料夾
var Folder =function(name) {
    var cache = [];
    return {
        add: function(file) {
            cache.push(file);
        },
        execute: function() {
            console.log('複製資料夾: ' + name);
        },
        scan: function() {
            console.log('掃描資料夾: ' + name);
        }
    }
};

// 建立資料夾和檔案物件,讓他們組成一棵樹
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 );
複製程式碼

此時在我們的目錄中已經有了一顆完整的樹了,那麼我希望將folder資料夾複製到另一個位置,會有掃描檔案,和複製檔案的過程:

// 將鍵盤按下CTRL + C的時候,註冊事件執行
foler.scan();
// 將鍵盤按下CTRL + V的時候,註冊事件執行
folder.execute();
複製程式碼

當我們希望新增新的節點的時候,我們可以直接增加新增件節點到folder中,我們改變了樹的結構,增加了新的資料,卻不用修改任何一句原有的程式碼,這是符合開放-封閉原則的。

var folder3 = new Folder( 'Nodejs' );
var file4 = new File( '深入淺出 Node.js' );
folder3.add( file4 );
var file5 = new File( 'JavaScript 語言精髓與程式設計實踐' );
// 新增到folder結構樹中,修改了整體結構
folder.add( folder3 );
folder.add( file5 );
複製程式碼

6 組合模式小結

  • 組合模式不是父子關係(繼承)
    組合模式是一種HAS-A(聚合)關係,不是IS-A。組合物件包含一組葉物件,但是葉物件並不是Composite(組合)的子類。組合物件最終會將請求委託給所有的葉子物件,它們能夠合作關鍵按是擁有相同的介面。
  • 對葉物件的操作一致
    組合物件與葉物件擁有相同的介面外,對同一組葉子物件的操作必須是一致的。例如上面的我要掃描檔案,不能在組合物件中出現呼叫複製檔案的介面。
  • 雙向對映關係
    例如公司給員工發放福利卡。公司給各個部分發放,部分給員工發放。組合模式就很適合,但是如果員工A是一個架構師,既屬於A部門也屬於B部門,那麼很有可能被髮放兩次。就不適合了。
  • 用職責鏈模式提高組合模式效能
    組合模式中,如果樹結構比較複雜,節點較多,那麼遍歷過程中效能就不好了。我們可以藉助職責鏈:手動設定鏈條,將組合物件與子節點之間效能職責鏈。讓請求從組合象到子物件,或從子物件到父物件都能傳遞。
  • 組合模式缺點:因為組合的內容都是程式碼中已存在的,會讓系統中的物件看起來和其他的物件差不多,只有在執行的時候才能顯示出來。如果組合模式建立太多物件,可能讓系統負擔不起。
    利用職責鏈思想在子節點保持對父節點的引用。可以從自己子節點往父節點冒泡。
var File = function(name) {
    this.parent = undefined;
	this.name = name;
};
File.prototype.add = function(file) {
    console.log('can not add child file');
}
File.prototype.remove = function() {
    // 根節點或則遊離無組織的節點
   if (!this.parent) {
       return;
   }
	var self = this;
   this.parent.cache = this.parent.cache.filter(function(f) {
       return f != self;
   });

}

// 資料夾
var Folder = function(name) {
    this.parent = undefined;
    this.cache = [];
	this.name = name;
};
Folder.prototype.add = function(file) {
    file.parent = this; // 設定父物件
    this.cache.push(file);
}

Folder.prototype.remove = function() {
    // 根節點或則遊離無組織的節點
   if (!this.parent) {
       return;
   }
      console.log(' 移除資料夾' + this.name,    this.parent.cache );
   var self = this;
   this.parent.cache = this.parent.cache.filter(function(f) {
       return f != self;
   });
   console.log(' 移除資料夾' + this.name,    this.parent.cache );
}

// 移除資料夾
var folder = new Folder( '學習資料' );
var folder1 = new Folder( 'JavaScript' );
var file1 = new File ( '深入淺出 Node.js' );
folder1.add( new File( 'JavaScript 設計模式與開發實踐' ) );
folder.add( folder1 );
folder.add( file1 );
folder1.remove(); //移除資料夾
複製程式碼

相關文章