職責鏈模式

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

職責鏈模式的名字很是形象,一系列可能會處理請求的對象被鏈接成一條鏈,請求在這些對象之間依次傳遞,直到遇到一個能夠處理它的對象,咱們把這些對象稱爲鏈中的節點,如圖所示:程序員

1. 現實中的的職責鏈模式

職責鏈模式的例子在現實中並不難找到,如下就是兩個常見的跟職責鏈模式有關的場景。ajax

  • 若是早高峯能順利擠上公交車的話,那麼估計這一天都會過得很開心。由於公交車上人實在太多了,常常上車後卻找不到售票員在哪,因此只好把兩塊錢硬幣往前面遞。除非你運氣夠好,站在你前面的第一我的就是售票員,不然,你的硬幣一般要在 N 我的手上傳遞,才能最終到達售票員的手裏。
  • 中學時代的期末考試,若是你平時不太老實,考試時就會被安排在第一個位置。遇到不會答的題目,就把題目編號寫在小紙條上日後傳遞,坐在後面的同窗若是也不會答,他就會把這張小紙條繼續遞給他後面的人。

從這兩個例子中,咱們很容易找到職責鏈模式的最大優勢:請求發送者只須要知道鏈中的第一個節點,從而弱化了發送者和一組接收者之間的強聯繫。若是不使用職責鏈模式,那麼在公交車上,咱們就得先搞清楚誰是售票員,才能把硬幣遞給他。一樣,在期末考試中,也許我就要先了解同窗中有哪些能夠解答這道題。設計模式

2. 實際開發中的職責鏈模式

假設咱們負責一個售賣手機的電商網站,通過分別交納 500 元定金和 200 元定金的兩輪預訂後(訂單已在此時生成),如今已經到了正式購買的階段。app

公司針對支付過定金的用戶有必定的優惠政策。在正式購買後,已經支付過 500 元定金的用戶會收到 100 元的商城優惠券, 200 元定金的用戶能夠收到 50 元的優惠券,而以前沒有支付定金的用戶只能進入普通購買模式,也就是沒有優惠券,且在庫存有限的狀況下不必定保證能買到。框架

咱們的訂單頁面是 PHP 吐出的模板,在頁面加載之初, PHP 會傳遞給頁面幾個字段。異步

  • 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 優惠券

雖然咱們獲得了意料中的運行結果,但這遠遠算不上一段值得誇獎的代碼。 order 函數不只巨大到難以閱讀,並且須要常常進行修改。雖然目前的項目能正常運行,但接下來的維護工做無疑是個夢魘。恐怕只有最「新手」的程序員纔會寫出這樣的代碼。性能

3. 用職責鏈模式重構代碼

如今咱們採用職責鏈模式重構這段代碼,先把 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 元定金預購,獲得 50 優惠券
order500(3, false, 500)     //輸出:普通購買,無優惠券

能夠看到,執行結果和前面那個巨大的 order 函數徹底同樣,可是代碼的結構已經清晰了不少,咱們把一個大函數拆分了 3 個小函數,去掉了許多嵌套的條件分支語句。

目前已經有了不小的進步,但咱們不會知足於此,雖然已經把大函數拆分紅了互不影響的 3 個小函數,但能夠看到,請求在鏈條中的順序很是僵硬,傳遞請求的代碼被耦合在了業務函數之中:

var order500 = function (orderType, pay, stock) {
    if (orderType === 1 && pay === true) {
        console.log(' 500 元定金預購,獲得 100 優惠券');
    } else {
        order200(orderType, pay, stock);
        // order200 和 order 500 耦合在一塊兒
    }
};

這依然是違反開放——封閉原則的,若是有天咱們要增長 300 元預訂或者去掉 200 元預訂,意味着就必須改動這些業務函數內部。就像一根環環相扣打了死結的鏈條,若是要增長,拆除或者移動一個節點,就必須得先砸爛這根鏈條。

4. 靈活可拆分的職責鏈節點

本節咱們採用一種更靈活的方式,來改進上面的職責鏈模式,目標是讓鏈中的各個節點能夠靈活拆分和重組。

首先須要改寫一下分別表示 3 種購買模式的節點函數,咱們約定,若是某個節點不能處理請求,則返回一個特定的字符串 ‘nextSuccessor’ 來表示該請求須要繼續日後傳遞:

var order500 = function (orderType, pay, stock) {
    if (orderType === 1 && pay === true) {
        console.log(' 500 元定金預購,獲得 100 優惠券');
    } else {
        return 'nextSuccessor';     //我不知道下一個節點是誰,反正把請求日後面傳遞
    }
};
var order200 = function (orderType, pay, stock) {
    if (orderType === 2 && pay === true) {
        console.log(' 200 元定金預購,獲得 50 優惠券');
    } else {
        return 'nextSuccessor';     //我不知道下一個節點是誰,反正把請求日後面傳遞
    }
};
var orderNormal = function (orderType, pay, stock) {
    if (stock > 0) {
        console.log('普通購買,無優惠券');
    } else {
        console.log('手機庫存不足');
    }
}

接下來須要把函數包裝進職責鏈節點,咱們定義一個構造函數 Chain ,在 new Chain 的時候傳遞的參數即爲須要被包裝的函數,同時它還擁有一個實例屬性 this.successor ,表示在鏈中的下一個節點。

var Chain = function (fn) {
    this.fn = fn;
    this.successor = null;
};
//指定在鏈中的下一個節點
Chain.prototype.setNextSuccessor = function (successor) {
    return this.successor = successor;
}
//傳遞請求給某個節點
Chain.prototype.passRequest = function () {
    var ret = this.fn.apply(this, arguments);
    if (ret === 'nextSuccessor') {
        return this.successor && this.successor.passRequest.apply(this.successor, arguments);
    }
    return ret;
}

如今咱們把 3 個訂單函數分別包裝成職責鏈的節點:

var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);

而後指定節點在職責鏈中的順序:

chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);

最後把請求傳遞給第一個節點:

chainOrder500.passRequest(1, true, 500);    //輸出: 500 元定金預購,獲得 100 優惠券
chainOrder500.passRequest(1, false, 500);   //輸出:普通購買,無優惠券
chainOrder500.passRequest(2, true, 500);    //輸出: 200 元定金預購,獲得 50 優惠券
chainOrder500.passRequest(3, false, 0);     //輸出:手機庫存不足

經過改進,咱們能夠自由靈活地增長,移除和修改鏈中的節點順序,假如某天網站運營人員又想出了支持 300 元定金購買,那咱們就在該鏈中增長一個節點便可:

var order300 = function () {
    //具體功能略
}
chainOrder300 = new Chain(order300);
chainOrder500.setNextSuccessor(chainOrder300);
chainOrder300.setNextSuccessor(chainOrder200);

對於程序員來講,咱們老是喜歡去改動那些相對容易改動的地方,就像改動框架的配置文件遠比改動框架的源代碼簡單得多。在這裏徹底不用理會原來的訂單函數代碼,咱們要作的只是增長一個節點,而後從新設置鏈中相關節點的順序。

5. 異步的職責鏈

在上一節的職責鏈模式中,咱們讓每一個節點函數同步返回一個特定的值 「nextSuccessor」 ,來表示是否把請求傳遞給下一個節點。而在現實開發中,咱們常常會遇到一些異步的問題,好比咱們要在節點函數中發起一個 ajax 異步請求,異步請求返回的結果才能決定是否繼續在職責鏈中 passRequest。

這時候讓節點函數同步返回 「nextSuccessor」 已經沒有意義了,因此要給 Chain 類再增長一個原型方法 Chain.prototype.next ,表示手動傳遞請求給職責鏈中的下一個節點:

Chain.prototype.next = function () {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}

來看一個異步職責鏈的例子:

var fn1 = new Chain(function () {
    console.log('1');
    return 'nextSuccessor';
});
var fn2 = new Chain(function () {
    console.log('2');
    var self = this;
    setTimeout(function () {
        self.next();
    }, 1000);
});
var fn3 = new Chain(function () {
    console.log('3');
});

fn1.setNextSuccessor(fn2).setNextSuccessor(fn3);
fn1.passRequest();

如今咱們獲得了一個特殊的鏈條,請求在鏈中的節點裏傳遞,但節點有權力決定何時把請求交給下一個節點。能夠想象,異步的職責鏈加上命令模式(把 ajax 請求封裝成命令模式),咱們能夠很方便地建立一個異步 ajax 隊列庫。

6. 職責鏈模式的優缺點

前面已經說過,職責鏈模式最大優勢就是解耦了請求發送者和 N 個接收者之間的複雜關係,因爲不知道鏈中的哪一個節點能夠處理你發出的請求,因此你只須要傳遞給第一個節點便可。

在手機商城的例子中,原本咱們要被迫維護一個充斥着條件分支語句的巨大函數,在例子裏的購買過程當中只打印了一條 log 語句。其實在現實開發中,這裏要作更多事情,好比根據訂單種類彈出不一樣的浮層提示,渲染不一樣的 UI 節點,組合不一樣的參數發送給不一樣的 cgi 等。用了職責鏈模式以後,每種訂單都有各自的處理函數而互不影響。

其次,使用了職責鏈模式以後,鏈中的節點對象能夠靈活地拆分重組。增長或者刪除一個節點,或者改變節點在鏈中的位置都是垂手可得的事情。這一點咱們也已經看到,在上面的例子中,增長一種訂單徹底不須要改動其餘訂單函數中的代碼。

職責鏈模式還有一個優勢,那就是能夠手動指定起始節點,請求並非非得從鏈中的第一個節點開始傳遞。好比在公交車的例子中,若是我明確在我前面的第一我的不是售票員,那我固然能夠越過他把公交卡遞給他前面的人,這樣就能夠減小請求在鏈中的傳遞次數,更快的找到合適的請求接受者。這在普通的條件分支語句是作不到的,咱們沒有辦法讓請求越過某一個 if 判斷。

拿代碼來證實這一點,假設某一天網站中支付過定金的訂單已經所有結束購買流程,咱們接下來的時間裏只須要處理普通購買訂單,因此咱們能夠直接把請求交給普通購買訂單節點:

orderNormal.passRequest(1, false, 500); //普通購買,無優惠券

若是運用得當,職責鏈模式能夠很好地幫助咱們組織代碼,但這種模式也並不是沒有弊端,首先咱們不能保證某個請求必定會被鏈中的節點處理。好比在期末考試的例子中,小紙條上的題目也許沒有任何一個同窗知道如何解答,此時的請求就得不到答覆,而是徑直從鏈尾離開,或者拋出一個錯誤異常。在這種狀況下,咱們能夠在鏈尾增長一個保底的接受者來處理這種即將離開鏈尾的請求。

另外,職責鏈模式使得程序中多了一些節點對象,可能在某一次的請求傳遞過程當中,大部分節點並無起到實質性的做用,它們的做用僅僅是讓請求傳遞下去,從性能方面考慮,咱們要避免過長的職責鏈帶來的性能損耗。

7. 用 AOP 實現職責鏈

在以前的職責鏈實現中,咱們利用了一個 Chain 類來把普通函數包裝成職責鏈的節點。其實利用 JavaScript 的函數式特性,有一種更加方便的方法來建立職責鏈。

下面咱們建立一個 Function.prototype.after 函數,使得第一個函數返回 ‘nextSuccessor’ 時,將請求繼續傳遞給下一個函數,不管是返回字符串 ‘nextSuccessor’ 或者 false 都只是一個約定,固然在這裏咱們也可讓函數返回 false 表示傳遞請求,選擇 ‘nextSuccessor’ 字符串是由於它看起來更能表達咱們的目的,代碼以下:

var order500 = function (orderType, pay, stock) {
    if (orderType === 1 && pay === true) {
        console.log(' 500 元定金預購,獲得 100 優惠券');
    } else {
        return 'nextSuccessor';
    }
};
var order200 = function (orderType, pay, stock) {
    if (orderType === 2 && pay === true) {
        console.log(' 200 元定金預購,獲得 50 優惠券');
    } else {
        return 'nextSuccessor';
    }
};
var orderNormal = function (orderType, pay, stock) {
    if (stock > 0) {
        console.log('普通購買,無優惠券');
    } else {
        console.log('手機庫存不足');
    }
}

Function.prototype.after = function (fn) {
    var self = this;
    return function () {
        var ret = self.apply(this, arguments);
        if (ret === 'nextSuccessor') {
            return fn.apply(this, arguments);
        }
        return ret;
    };
};

var order = order500.after(order200).after(orderNormal);
order(1, true, 500);    //輸出: 500 元定金預購,獲得 100 優惠券
order(1, false, 500);   //輸出:普通購買,無優惠券
order(2, true, 500);    //輸出: 200 元定金預購,獲得 50 優惠券
order(3, false, 0); //輸出:手機庫存不足

用 AOP 來實現職責鏈既簡單又巧妙,但這種把函數疊在一塊兒的方式,同時也疊加了函數的做用域,若是鏈條太長的話,也會對性能有較大的影響。

8. 小結

在 JavaScript 開發中,職責鏈模式是最容易被忽視的模式之一。實際上只要運用的當,職責鏈模式能夠很好地幫助咱們管理代碼,下降發起請求的對象和處理請求的對象之間的耦合性。職責鏈模式的節點數量和順序是能夠自由變化的,咱們能夠在運行時決定鏈中包含哪些節點。

不管是做用域鏈,原型鏈,仍是 DOM 節點中的事件冒泡,咱們都能從中找到職責鏈模式的影子。職責鏈模式還能夠和組合模式結合在一塊兒,用來鏈接部件和父部件,或是提升組合對象的效率。學會使用職責鏈模式,相信在之後的代碼編寫中,將會對你大有裨益。


參考書目:《JavaScript設計模式與開發實踐》

相關文章
相關標籤/搜索