1. 程式人生 > >前端開發裡的設計模式

前端開發裡的設計模式

前端開發中的設計模式

設計模式的定義是,在面向物件軟體設計過程中針對特定問題的簡潔而優雅的解決方案。在不同的程式語言中,對設計模式的實現其實是可能會有區別的。比如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個小函式,去掉了許多巢狀的條件分支語句。