什么是職責(zé)鏈模式毅往?
職責(zé)鏈模式的定義是:使多個(gè)對(duì)象都有機(jī)會(huì)處理請(qǐng)求瞬场,從而避免請(qǐng)求的發(fā)送者和接收者之間的耦合關(guān)系野瘦,將這些對(duì)象連成一條鏈躲庄,并沿著這條鏈傳遞該請(qǐng)求查剖,直到有一個(gè)對(duì)象處理它為止。
職責(zé)鏈模式的名字非常形象噪窘,一系列可能會(huì)處理請(qǐng)求的對(duì)象被連接成一條鏈笋庄,請(qǐng)求在這些對(duì)象之間依次傳遞,直到遇到一個(gè)可以處理它的對(duì)象倔监,我們把這些對(duì)象稱(chēng)為鏈中的節(jié)點(diǎn)
現(xiàn)實(shí)中的職責(zé)鏈模式
職責(zé)鏈模式的例子在現(xiàn)實(shí)中并不難找到直砂,以下就是兩個(gè)常見(jiàn)的跟職責(zé)鏈模式有關(guān)的場(chǎng)景。
如果早高峰能順利擠上公交車(chē)的話浩习,那么估計(jì)這一天都會(huì)過(guò)得很開(kāi)心静暂。因?yàn)楣卉?chē)上人實(shí)在太多了,經(jīng)常上車(chē)后卻找不到售票員在哪谱秽,所以只好把兩塊錢(qián)硬幣往前面遞洽蛀。除非你運(yùn)氣夠好,站在你前面的第一個(gè)人就是售票員疟赊,否則郊供,你的硬幣通常要在N個(gè)人手上傳遞,才能最終到達(dá)售票員的手里近哟。
中學(xué)時(shí)代的期末考試驮审,如果你平時(shí)不太老實(shí),考試時(shí)就會(huì)被安排在第一個(gè)位置。遇到不會(huì)答的題目疯淫,就把題目編號(hào)寫(xiě)在小紙條上往后傳遞地来,坐在后面的同學(xué)如果也不會(huì)答,他就會(huì)把這張小紙條繼續(xù)遞給他后面的人熙掺。
從這兩個(gè)例子中未斑,我們很容易找到職責(zé)鏈模式的最大優(yōu)點(diǎn):請(qǐng)求發(fā)送者只需要知道鏈中的第一個(gè)節(jié)點(diǎn),從而弱化了發(fā)送者和一組接收者之間的強(qiáng)聯(lián)系适掰。如果不使用職責(zé)鏈模式颂碧,那么在公交車(chē)上,我就得先搞清楚誰(shuí)
是售票員类浪,才能把硬幣遞給他载城。同樣,在期末考試中费就,也許我就要先了解同學(xué)中有哪些可以解答這道題诉瓦。
實(shí)際開(kāi)發(fā)中的職責(zé)鏈模式
假設(shè)我們負(fù)責(zé)一個(gè)售賣(mài)手機(jī)的電商網(wǎng)站,經(jīng)過(guò)分別交納500元定金和200元定金的兩輪預(yù)定后(訂單已在此時(shí)生成)力细,現(xiàn)在已經(jīng)到了正式購(gòu)買(mǎi)的階段睬澡。
公司針對(duì)支付過(guò)定金的用戶有一定的優(yōu)惠政策。在正式購(gòu)買(mǎi)后眠蚂,已經(jīng)支付過(guò)500元定金的用戶會(huì)收到100元的商城優(yōu)惠券煞聪,200元定金的用戶可以收到50元的優(yōu)惠券,而之前沒(méi)有支付定金的用戶只能進(jìn)入普通購(gòu)買(mǎi)模式逝慧,也就是沒(méi)有優(yōu)惠券昔脯,且在庫(kù)存有限的情況下不一定保證能買(mǎi)到。
我們的訂單頁(yè)面是PHP吐出的模板笛臣,在頁(yè)面加載之初云稚,PHP會(huì)傳遞給頁(yè)面幾個(gè)字段。
orderType:表示訂單類(lèi)型(定金用戶或者普通購(gòu)買(mǎi)用戶)沈堡,code的值為1的時(shí)候是500元定金用戶静陈,為2的時(shí)候是200元定金用戶,為3的時(shí)候是普通購(gòu)買(mǎi)用戶诞丽。
pay:表示用戶是否已經(jīng)支付定金鲸拥,值為true或者false,雖然用戶已經(jīng)下過(guò)500元定金的訂單率拒,但如果他一直沒(méi)有支付定金崩泡,現(xiàn)在只能降級(jí)進(jìn)入普通購(gòu)買(mǎi)模式。
stock:表示當(dāng)前用于普通購(gòu)買(mǎi)的手機(jī)庫(kù)存數(shù)量猬膨,已經(jīng)支付過(guò)500元或者200元定金的用戶不受此限制。
下面我們把這個(gè)流程寫(xiě)成代碼:
var order = function( orderType, pay, stock ){
if ( orderType === 1 ){ // 500元定金購(gòu)買(mǎi)模式
if ( pay === true ){ // 已支付定金
console.log( '500元定金預(yù)購(gòu), 得到100優(yōu)惠券' );
}else{ // 未支付定金,降級(jí)到普通購(gòu)買(mǎi)模式
if ( stock > 0 ){ // 用于普通購(gòu)買(mǎi)的手機(jī)還有庫(kù)存
console.log( '普通購(gòu)買(mǎi), 無(wú)優(yōu)惠券' );
}else{
console.log( '手機(jī)庫(kù)存不足' );
}
}
}
else if ( orderType === 2 ){ // 200元定金購(gòu)買(mǎi)模式
if ( pay === true ){
console.log( '200元定金預(yù)購(gòu), 得到50優(yōu)惠券' );
}else{
if ( stock > 0 ){
console.log( '普通購(gòu)買(mǎi), 無(wú)優(yōu)惠券' );
}else{
console.log( '手機(jī)庫(kù)存不足' );
}
}
}
else if ( orderType === 3 ){
if ( stock > 0 ){
console.log( '普通購(gòu)買(mǎi), 無(wú)優(yōu)惠券' );
}else{
console.log( '手機(jī)庫(kù)存不足' );
}
}
};
order( 1 , true, 500); // 輸出: 500元定金預(yù)購(gòu), 得到100優(yōu)惠券
雖然我們得到了意料中的運(yùn)行結(jié)果勃痴,但這遠(yuǎn)遠(yuǎn)算不上一段值得夸獎(jiǎng)的代碼谒所。order函數(shù)不僅巨大到難以閱讀,而且需要經(jīng)常進(jìn)行修改沛申。雖然目前項(xiàng)目能正常運(yùn)行劣领,但接下來(lái)的維護(hù)工作無(wú)疑是個(gè)夢(mèng)魘。
用職責(zé)鏈模式重構(gòu)代碼
現(xiàn)在我們采用職責(zé)鏈模式重構(gòu)這段代碼铁材,先把500元訂單尖淘、200元訂單以及普通購(gòu)買(mǎi)分成3個(gè)函數(shù)。
接下來(lái)把orderType著觉、pay村生、stock這3個(gè)字段當(dāng)作參數(shù)傳遞給500元訂單函數(shù),如果該函數(shù)不符合處理?xiàng)l件饼丘,則把這個(gè)請(qǐng)求傳遞給后面的200元訂單函數(shù)趁桃,如果200元訂單函數(shù)依然不能處理該請(qǐng)求,則繼續(xù)傳遞請(qǐng)求給普通購(gòu)買(mǎi)函數(shù)肄鸽,代碼如下:
// 500元訂單
var order500 = function( orderType, pay, stock ){
if ( orderType === 1 && pay === true ){
console.log( '500元定金預(yù)購(gòu), 得到100優(yōu)惠券' );
}else{
order200( orderType, pay, stock ); // 將請(qǐng)求傳遞給200元訂單
}
};
// 200元訂單
var order200 = function( orderType, pay, stock ){
if ( orderType === 2 && pay === true ){
console.log( '200元定金預(yù)購(gòu), 得到50優(yōu)惠券' );
}else{
orderNormal( orderType, pay, stock ); // 將請(qǐng)求傳遞給普通訂單
}
};
// 普通購(gòu)買(mǎi)訂單
var orderNormal = function( orderType, pay, stock ){
if ( stock > 0 ){
console.log( '普通購(gòu)買(mǎi), 無(wú)優(yōu)惠券' );
}else{
console.log( '手機(jī)庫(kù)存不足' );
}
};
// 測(cè)試結(jié)果:
order500( 1 , true, 500); // 輸出:500元定金預(yù)購(gòu), 得到100優(yōu)惠券
order500( 1, false, 500 ); // 輸出:普通購(gòu)買(mǎi), 無(wú)優(yōu)惠券
order500( 2, true, 500 ); // 輸出:200元定金預(yù)購(gòu), 得到50優(yōu)惠券
order500( 3, false, 500 ); // 輸出:普通購(gòu)買(mǎi), 無(wú)優(yōu)惠券
order500( 3, false, 0 ); // 輸出:手機(jī)庫(kù)存不足
可以看到卫病,執(zhí)行結(jié)果和前面那個(gè)巨大的order函數(shù)完全一樣,但是代碼的結(jié)構(gòu)已經(jīng)清晰了很多典徘,我們把一個(gè)大函數(shù)拆分了3個(gè)小函數(shù)蟀苛,去掉了許多嵌套的條件分支語(yǔ)句。
目前已經(jīng)有了不小的進(jìn)步逮诲,但我們不會(huì)滿足于此帜平,雖然已經(jīng)把大函數(shù)拆分成了互不影響的3個(gè)小函數(shù),但可以看到汛骂,請(qǐng)求在鏈條傳遞中的順序非常僵硬罕模,傳遞請(qǐng)求的代碼被耦合在了業(yè)務(wù)函數(shù)之中:
var order500 = function( orderType, pay, stock ){
if ( orderType === 1 && pay === true ){
console.log( '500元定金預(yù)購(gòu), 得到100優(yōu)惠券' );
}else{
order200( orderType, pay, stock );
// order200和order500耦合在一起
}
};
這依然是違反開(kāi)放-封閉原則的,如果有天我們要增加300元預(yù)訂或者去掉200元預(yù)訂帘瞭,意味著就必須改動(dòng)這些業(yè)務(wù)函數(shù)內(nèi)部淑掌。就像一根環(huán)環(huán)相扣打了死結(jié)的鏈條,如果要增加蝶念、拆除或者移動(dòng)一個(gè)節(jié)點(diǎn)抛腕,就必須得先
砸爛這根鏈條。
靈活可拆分的職責(zé)鏈節(jié)點(diǎn)
本節(jié)我們采用一種更靈活的方式媒殉,來(lái)改進(jìn)上面的職責(zé)鏈模式担敌,目標(biāo)是讓鏈中的各個(gè)節(jié)點(diǎn)可以靈活拆分和重組。
首先需要改寫(xiě)一下分別表示3種購(gòu)買(mǎi)模式的節(jié)點(diǎn)函數(shù)廷蓉,我們約定全封,如果某個(gè)節(jié)點(diǎn)不能處理請(qǐng)求,則返回一個(gè)特定的字符串'nextSuccessor'來(lái)表示該請(qǐng)求需要繼續(xù)往后面?zhèn)鬟f:
var order500 = function( orderType, pay, stock ){
if ( orderType === 1 && pay === true ){
console.log( '500元定金預(yù)購(gòu),得到100優(yōu)惠券' );
}else{
return 'nextSuccessor'; // 我不知道下一個(gè)節(jié)點(diǎn)是誰(shuí)刹悴,反正把請(qǐng)求往后面?zhèn)鬟f
}
};
var order200 = function( orderType, pay, stock ){
if ( orderType === 2 && pay === true ){
console.log( '200元定金預(yù)購(gòu)行楞,得到50優(yōu)惠券' );
}else{
return 'nextSuccessor'; // 我不知道下一個(gè)節(jié)點(diǎn)是誰(shuí),反正把請(qǐng)求往后面?zhèn)鬟f
}
};
var orderNormal = function( orderType, pay, stock ){
if ( stock > 0 ){
console.log( '普通購(gòu)買(mǎi)土匀,無(wú)優(yōu)惠券' );
}else{
console.log( '手機(jī)庫(kù)存不足' );
}
};
接下來(lái)需要把函數(shù)包裝進(jìn)職責(zé)鏈節(jié)點(diǎn)子房,我們定義一個(gè)構(gòu)造函數(shù)Chain,在new Chain的時(shí)候傳遞的參數(shù)即為需要被包裝的函數(shù)就轧, 同時(shí)它還擁有一個(gè)實(shí)例屬性this.successor证杭,表示在鏈中的下一個(gè)節(jié)點(diǎn)。
此外Chain的prototype中還有兩個(gè)函數(shù)妒御,它們的作用如下所示:
// Chain.prototype.setNextSuccessor 指定在鏈中的下一個(gè)節(jié)點(diǎn)
// Chain.prototype.passRequest 傳遞請(qǐng)求給某個(gè)節(jié)點(diǎn)
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;
};
現(xiàn)在我們把3個(gè)訂單函數(shù)分別包裝成職責(zé)鏈的節(jié)點(diǎn):
var chainOrder500 = new Chain( order500 );
var chainOrder200 = new Chain( order200 );
var chainOrderNormal = new Chain( orderNormal )
然后指定節(jié)點(diǎn)在職責(zé)鏈中的順序:
chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );
最后把請(qǐng)求傳遞給第一個(gè)節(jié)點(diǎn):
chainOrder500.passRequest( 1, true, 500 ); // 輸出:500元定金預(yù)購(gòu)解愤,得到100優(yōu)惠券
chainOrder500.passRequest( 2, true, 500 ); // 輸出:200元定金預(yù)購(gòu),得到50優(yōu)惠券
chainOrder500.passRequest( 3, true, 500 ); // 輸出:普通購(gòu)買(mǎi)携丁,無(wú)優(yōu)惠券
chainOrder500.passRequest( 1, false, 0 ); // 輸出:手機(jī)庫(kù)存不足
通過(guò)改進(jìn)琢歇,我們可以自由靈活地增加、移除和修改鏈中的節(jié)點(diǎn)順序梦鉴,假如某天網(wǎng)站運(yùn)營(yíng)人員又想出了支持300元定金購(gòu)買(mǎi)李茫,那我們就在該鏈中增加一個(gè)節(jié)點(diǎn)即可:
var order300 = function(){
// 具體實(shí)現(xiàn)略
};
chainOrder300= new Chain( order300 );
chainOrder500.setNextSuccessor( chainOrder300);
chainOrder300.setNextSuccessor( chainOrder200);
對(duì)于程序員來(lái)說(shuō),我們總是喜歡去改動(dòng)那些相對(duì)容易改動(dòng)的地方肥橙,就像改動(dòng)框架的配置文件遠(yuǎn)比改動(dòng)框架的源代碼簡(jiǎn)單得多魄宏。在這里完全不用理會(huì)原來(lái)的訂單函數(shù)代碼,我們要做的只是增加一個(gè)節(jié)點(diǎn)存筏,然后重新設(shè)
置鏈中相關(guān)節(jié)點(diǎn)的順序宠互。
異步的職責(zé)鏈
在上一節(jié)的職責(zé)鏈模式中,我們讓每個(gè)節(jié)點(diǎn)函數(shù)同步返回一個(gè)特定的值"nextSuccessor"椭坚,來(lái)表示是否把請(qǐng)求傳遞給下一個(gè)節(jié)點(diǎn)予跌。而在現(xiàn)實(shí)開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到一些異步的問(wèn)題善茎,比如我們要在節(jié)點(diǎn)函數(shù)中
發(fā)起一個(gè)ajax異步請(qǐng)求券册,異步請(qǐng)求返回的結(jié)果才能決定是否繼續(xù)在職責(zé)鏈中passRequest。
這時(shí)候讓節(jié)點(diǎn)函數(shù)同步返回"nextSuccessor"已經(jīng)沒(méi)有意義了垂涯,所以要給Chain類(lèi)再增加一個(gè)原型方法Chain.prototype.next烁焙,表示手動(dòng)傳遞請(qǐng)求給職責(zé)鏈中的下一個(gè)節(jié)點(diǎn):
Chain.prototype.next= function(){
return this.successor && this.successor.passRequest.apply( this.successor, arguments );
};
來(lái)看一個(gè)異步職責(zé)鏈的例子:
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();
職責(zé)鏈模式的優(yōu)缺點(diǎn)
前面已經(jīng)說(shuō)過(guò),職責(zé)鏈模式的最大優(yōu)點(diǎn)就是解耦了請(qǐng)求發(fā)送者和N個(gè)接收者之間的復(fù)雜關(guān)系耕赘,由于不知道鏈中的哪個(gè)節(jié)點(diǎn)可以處理你發(fā)出的請(qǐng)求骄蝇,所以你只需把請(qǐng)求傳遞給第一個(gè)節(jié)點(diǎn)即可
職責(zé)鏈模式還有一個(gè)優(yōu)點(diǎn),那就是可以手動(dòng)指定起始節(jié)點(diǎn)操骡,請(qǐng)求并不是非得從鏈中的第一個(gè)節(jié)點(diǎn)開(kāi)始傳遞九火。比如在公交車(chē)的例子中赚窃,如果我明確在我前面的第一個(gè)人不是售票員,那我當(dāng)然可以越過(guò)他把公交卡遞給他前面的人吃既,這樣可以減少請(qǐng)求在鏈中的傳遞次數(shù)考榨,更快地找到合適的請(qǐng)求接受者跨细。這在普通的條件分支語(yǔ)句下是做不到的鹦倚,我們沒(méi)有辦法讓請(qǐng)求越過(guò)某一個(gè)if判斷。
用AOP實(shí)現(xiàn)職責(zé)鏈
在之前的職責(zé)鏈實(shí)現(xiàn)中冀惭,我們利用了一個(gè)Chain類(lèi)來(lái)把普通函數(shù)包裝成職責(zé)鏈的節(jié)點(diǎn)震叙。其實(shí)利用JavaScript的函數(shù)式特性,有一種更加方便的方法來(lái)創(chuàng)建職責(zé)鏈散休。
下面我們改寫(xiě)一下Function.prototype.after函數(shù)媒楼,使得第一個(gè)函數(shù)返回'nextSuccessor'時(shí),將請(qǐng)求繼續(xù)傳遞給下一個(gè)函數(shù)戚丸,無(wú)論是返回字符串'nextSuccessor'或者false都只是一個(gè)約定划址,當(dāng)然在這里我們也可以讓函數(shù)返回false表示傳遞請(qǐng)求,選擇'nextSuccessor'字符串是因?yàn)樗雌饋?lái)更能表達(dá)我們的目的限府,代碼如下:
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 = order500yuan.after( order200yuan ).after( orderNormal );
order( 1, true, 500 ); // 輸出:500元定金預(yù)購(gòu)夺颤,得到100優(yōu)惠券
order( 2, true, 500 ); // 輸出:200元定金預(yù)購(gòu),得到50優(yōu)惠券
order( 1, false, 500 ); // 輸出:普通購(gòu)買(mǎi)胁勺,無(wú)優(yōu)惠券
用AOP來(lái)實(shí)現(xiàn)職責(zé)鏈既簡(jiǎn)單又巧妙世澜,但這種把函數(shù)疊在一起的方式,同時(shí)也疊加了函數(shù)的作用域署穗,如果鏈條太長(zhǎng)的話寥裂,也會(huì)對(duì)性能有較大的影響。
小結(jié)
在JavaScript開(kāi)發(fā)中案疲,職責(zé)鏈模式是最容易被忽視的模式之一封恰。實(shí)際上只要運(yùn)用得當(dāng),職責(zé)鏈模式可以很好地幫助我們管理代碼褐啡,降低發(fā)起請(qǐng)的對(duì)象和處理請(qǐng)求的對(duì)象之間的耦合性诺舔。職責(zé)鏈中的節(jié)點(diǎn)數(shù)量和順序是可以自由變化的,我們可以在運(yùn)行時(shí)決定鏈中包含哪些節(jié)點(diǎn)春贸。
無(wú)論是作用域鏈混萝、原型鏈,還是DOM節(jié)點(diǎn)中的事件冒泡萍恕,我們都能從中找到職責(zé)鏈模式的影子逸嘀。職責(zé)鏈模式還可以和組合模式結(jié)合在一起,用來(lái)連接部件和父部件允粤,或是提高組合對(duì)象的效率崭倘。學(xué)會(huì)使用職責(zé)鏈模
式翼岁,相信在以后的代碼編寫(xiě)中,將會(huì)對(duì)你大有裨益司光。