前端開發裏的設計模式

前端開發中的設計模式

設計模式的定義是,在面向對象軟件設計過程中針對特定問題的簡潔而優雅的解決方案。在不同的編程語言中,對設計模式的實現其實是可能會有區別的。比如java和javascript,在Java這種靜態編譯型語言中,無法動態地給已存在的對象添加職責,所以一般通過包裝類的方式來實現裝飾者模式。但在JavaScript這種動態解釋型語言中,給對象動態添加職責是再簡單不過的事情。這就造成了JavaScript語言的裝飾者模式不再關注於給對象動態添加職責,而是關注於給函數動態添加職責。本篇博文將介紹以下幾個比較常見的設計模式:

  • 單例模式
  • 觀察者模式
  • 命令模式
  • 職責鏈模式

單例模式

單例模式的定義是保證一個類只有一個實例,並且提供一個訪問它的全局訪問點。有些時候一些對象我們往往只需要一個,比如線程池、全局緩存、瀏覽器中的window對象等。單例模式的優點是:

  • 可以用來劃分命名空間,減少全局變量的數量
  • 使用單例模式可以使代碼組織的更爲一致,使代碼容易閱讀和維護
  • 可以被實例化,且實例化一次

要實現一個標準的單例模式並不複雜,無非是用一個變量標識當前是否已經爲某個類創建過對象,如果是,則在下一次獲取這個類的實例時,直接返回之前創建的對象。下面是單例模式的基本結構:

// 單例模式
var Singleton = function(name){
    this.name = name;
    this.instance = null;
};
Singleton.prototype.getName = function(){
    return this.name;
};
// 獲取實例對象
Singleton.getInstance = function(name) {
    if(!this.instance) {
        this.instance = new Singleton(name);
    }
    return this.instance;
};
// 測試單例模式的實例
var a = Singleton.getInstance("aa");
var b = Singleton.getInstance("bb");

實際上因爲單例模式是隻實例化一次,所以a和b其實是相等的。也即是說下面語句的值爲true。

console.log(a===b)

由於單例模式只實例化一次,因此第一次調用,返回的是a實例的對象,繼續調用的時候,b的實例也就是a的實例,因此下面打印的都是aa:

console.log(a.getName());// aa

console.log(b.getName());// aa  

觀察者模式

觀察者模式又叫做發佈-訂閱模式,它定義了對象間的一種一對多的依賴關係,當一個對象的狀態發生變化時,所有依賴於他的對象都將得到通知,在javascript的開發中,一般用事件模型來替代傳統的發佈 — 訂閱模式。

發佈 — 訂閱模式可以廣泛應用於異步編程中,這是一種替代傳遞迴調函數的方案。比如,我們可以訂閱 ajax請求的 error 、 succ 等事件。或者如果想在動畫的每一幀完成之後做一些事情,那我們可以訂閱一個事件,然後在動畫的每一幀完成之後發佈這個事件。在異步編程中使用發佈 — 訂閱模式,我們就無需過多關注對象在異步運行期間的內部狀態,而只需要訂閱感興趣的事件發生點。

發佈 — 訂閱模式還可以取代對象之間硬編碼的通知機制,一個對象不用再顯式地調用另外一個對象的某個接口。發佈 — 訂閱模式讓兩個對象鬆耦合地聯繫在一起,雖然不太清楚彼此的細節,但這不影響它們之間相互通信。當有新的訂閱者出現時,發佈者的代碼不需要任何修改;同樣發佈者需要改變時,也不會影響到之前的訂閱者。只要之前約定的事件名沒有變化,就可以自由地改變它們。

實際上,只要我們曾經在 DOM 節點上面綁定過事件函數,那我們就曾經使用過發佈 — 訂閱模式,以下代碼便是一個示例:

document.body.addEventListener( 'click', function(){
alert(2);}, false );
document.body.click(); // 模擬用戶點擊

在這裏需要監控用戶點擊 document.body 的動作,但是我們沒辦法預知用戶將在什麼時候點擊。所以我們訂閱 document.body 上的 click 事件,當 body 節點被點擊時, body 節點便會向訂閱者發佈這個消息。就像是樓房購買,購房者不知道房子什麼時候開售,於是他在訂閱消息後等待售樓處發佈消息。

除了 DOM 事件,我們還會經常實現一些自定義的事件,這種依靠自定義事件完成的發佈 —訂閱模式可以用於任何 JavaScript代碼中。實現發佈 — 訂閱模式的步驟如下:
1. 首先指定好誰充當發佈者;
2. 然後給發佈者添加一個緩存列表,用於存放回調函數以便通知訂閱者;
3. 最後發佈消息的時候,發佈者會遍歷這個緩存列表,依次觸發裏面存放的訂閱者回調函數。

var salesOffices = {}; // 定義售樓處
salesOffices.clientList = []; // 緩存列表,存放訂閱者的回調函數
salesOffices.listen = function( fn ){ // 增加訂閱者
    this.clientList.push( fn ); // 訂閱的消息添加進緩存列表
};
salesOffices.trigger = function(){ // 發佈消息
    for( var i = 0, fn; fn = this.clientList[ i++ ]; ){
        fn.apply( this, arguments ); // arguments 是發佈消息時帶上的參數
    }
};
//調用
salesOffices.listen( function( price, squareMeter ){//訂閱消息
    console.log( '價格= ' + price );
    console.log( 'squareMeter= ' + squareMeter );
});
salesOffices.trigger( 2000000, 88 ); // 輸出:200 萬,88 平方米

至此,實現了最簡單的發佈-訂閱模式。比起在Java中實現觀察者模式,還是有不同的,在Java裏面實現,通常會把訂閱者對象當成引用傳入發佈者對象中,同時訂閱者對象還需提供一個名爲諸如 update的方法,供發佈者對象在適合的時候調用。而在 JavaScript中,我們用註冊回調函數的形式來代替傳統的發佈 — 訂閱模式。

命令模式

命令模式中的命令(command)指的是一個執行某些特定事情的指令。

命令模式的應用場景是:有時候需要向某些對象發送請求,但是並不知道請求的接收者是誰,也不知道被請求的操作是什麼,此時希望用一種鬆耦合的方式來設計軟件,使得請求發送者和請求接收者能夠消除彼此之間的耦合關係。

傳統的面向對象的模式設計代碼的方式是,假設html結構如下:

<button id="button1">刷新菜單目錄</button>
<button id="button2">增加子菜單</button>
<button id="button3">刪除子菜單</button>

JavaScript的代碼如下:

var b1 = document.getElementById("button1"),
    b2 = document.getElementById("button2"),
    b3 = document.getElementById("button3");

 // 定義setCommand 函數,該函數負責往按鈕上面安裝命令。點擊按鈕後會執行command對象的execute()方法。
 var setCommand = function(button,command){
    button.onclick = function(){
        command.execute();
    }
 };
 // 下面我們自己來定義各個對象來完成自己的業務操作
 var MenuBar = {
    refersh: function(){
        alert("刷新菜單目錄");
    }
 };
 var SubMenu = {
    add: function(){
        alert("增加子菜單");
    },
    del: function(){
        alert("刪除子菜單");
    }
 };
 // 下面是編寫命令類
 var RefreshMenuBarCommand = function(receiver){
    this.receiver = receiver;
 };
 RefreshMenuBarCommand.prototype.execute = function(){
    this.receiver.refersh();
 }
 // 增加命令操作
 var AddSubMenuCommand = function(receiver) {
    this.receiver = receiver;
 };
 AddSubMenuCommand.prototype.execute = function() {
    this.receiver.add();
 }
 // 刪除命令操作
 var DelSubMenuCommand = function(receiver) {
    this.receiver = receiver;
 };
 DelSubMenuCommand.prototype.execute = function(){
    this.receiver.del();
 }
 // 最後把命令接收者傳入到command對象中,並且把command對象安裝到button上面
 var refershBtn = new RefreshMenuBarCommand(MenuBar);
 var addBtn = new AddSubMenuCommand(SubMenu);
 var delBtn = new DelSubMenuCommand(SubMenu);

 setCommand(b1,refershBtn);
 setCommand(b2,addBtn);
 setCommand(b3,delBtn);

不過上述代碼太過繁瑣,用javascript的回調函數,接收者被封閉在回調函數產生的環境中,執行操作將會更加簡單,僅僅執行回調函數即可。

var setCommand = function(button,func) {
    button.onclick = function(){
        func();
    }
 }; 
 var MenuBar = {
    refersh: function(){
        alert("刷新菜單界面");
    }
 };
 var SubMenu = {
    add: function(){
        alert("增加菜單");
    }
 };
 // 刷新菜單
 var RefreshMenuBarCommand = function(receiver) {
    return function(){
        receiver.refersh();    
    };
 };
 // 增加菜單
 var AddSubMenuCommand = function(receiver) {
    return function(){
        receiver.add();    
    };
 };
 var refershMenuBarCommand = RefreshMenuBarCommand(MenuBar);
 // 增加菜單
 var addSubMenuCommand = AddSubMenuCommand(SubMenu);
 setCommand(b1,refershMenuBarCommand);

 setCommand(b2,addSubMenuCommand);

職責鏈模式

職責鏈模式的定義是:使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關係,將這些對象連成一條鏈,並沿着這條鏈傳遞該請求,直到有一個對象處理它爲止。

假設有這樣的場景,我們負責一個售賣手機的電商網站,經過分別交納 500元定金和 200元定金的兩輪預定後(訂單已在此時生成),現在已經到了正式購買的階段。公司針對支付過定金的用戶有一定的優惠政策。在正式購買後,已經支付過 500元定金的用戶會收到 100元的商城優惠券,200元定金的用戶可以收到 50元的優惠券,而之前沒有支付定金的用戶只能進入普通購買模式,也就是沒有優惠券,且在庫存有限的情況下不一定保證能買到。

  • orderType :表示訂單類型(定金用戶或者普通購買用戶), code 的值爲 1的時候是 500元定金用戶,爲 2的時候是 200元定金用戶,爲 3的時候是普通購買用戶。
  • pay :表示用戶是否已經支付定金,值爲 true 或者 false , 雖然用戶已經下過 500元定金的訂單,但如果他一直沒有支付定金,現在只能降級進入普通購買模式
  • stock :表示當前用於普通購買的手機庫存數量,已經支付過 500 元或者 200 元定金的用戶不受此限制

把這個流程代碼化:

var order = function( orderType, pay, stock ){
if ( orderType === 1 ){ // 500 元定金購買模式
    if ( pay === true ){ // 已支付定金
        console.log( '500 元定金預購, 得到 100 優惠券' );
    }else{ // 未支付定金,降級到普通購買模式
        if ( stock > 0 ){ // 用於普通購買的手機還有庫存
            console.log( '普通購買, 無優惠券' );
            }else{
                console.log( '手機庫存不足' );
                }
            }
        }
        else if ( orderType === 2 ){ // 200 元定金購買模式
            if ( pay === true ){
                console.log( '200 元定金預購, 得到 50 優惠券' );
            }else{
                if ( stock > 0 ){
                    console.log( '普通購買, 無優惠券' );
                }else{
                    console.log( '手機庫存不足' );
                }
            }
        }       else if ( orderType === 3 ){
            if ( stock > 0 ){
                console.log( '普通購買, 無優惠券' );
            }else{
                console.log( '手機庫存不足' );
            }
        }
    };
    order( 1 , true, 500); // 輸出: 500 元定金預購, 得到 100 優惠券

現在我們採用職責鏈模式重構這段代碼,先把 500 元訂單、200 元訂單以及普通購買分成 3個函數。
接下來把 orderType 、 pay 、 stock 這 3個字段當作參數傳遞給 500元訂單函數,如果該函數不符合處理條件,則把這個請求傳遞給後面的 200元訂單函數,如果 200元訂單函數依然不能處理該請求,則繼續傳遞請求給普通購買函數,代碼如下:

// 500 元訂單
var order500 = function( orderType, pay, stock ){
    if ( orderType === 1 && pay === true ){
        console.log( '500 元定金預購, 得到 100 優惠券' );
    }else{
        order200( orderType, pay, stock ); // 將請求傳遞給 200 元訂單
    }
};
// 200 元訂單
var order200 = function( orderType, pay, stock ){
    if ( orderType === 2 && pay === true ){
        console.log( '200 元定金預購, 得到 50 優惠券' );
    }else{
        orderNormal( orderType, pay, stock ); // 將請求傳遞給普通訂單
    }
};
// 普通購買訂單
var orderNormal = function( orderType, pay, stock ){
    if ( stock > 0 ){
        console.log( '普通購買, 無優惠券' );
    }else{
        console.log( '手機庫存不足' );
    }
};
// 測試結果:
order500( 1 , true, 500); // 輸出:500 元定金預購, 得到 100 優惠券
order500( 1, false, 500 ); // 輸出:普通購買, 無優惠券
order500( 2, true, 500 ); // 輸出:200 元定金預購, 得到 500 優惠券
order500( 3, false, 500 ); // 輸出:普通購買, 無優惠券
order500( 3, false, 0 ); // 輸出:手機庫存不足

可以看到,執行結果和前面那個巨大的 order 函數完全一樣,但是代碼的結構已經清晰了很多,我們把一個大函數拆分了 3個小函數,去掉了許多嵌套的條件分支語句。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章