摘自《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》
職責(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ì)象稱為鏈中的節(jié)點(diǎn)睦霎。
現(xiàn)實(shí)中的職責(zé)鏈模式
如果早高峰能順利擠上公交車的話,那么估計(jì)這一天都會(huì)過得很開心走诞。因?yàn)楣卉嚿先藢?shí)在太多了副女,經(jīng)常上車后卻找不到售票員在哪,所以只好把兩塊錢硬幣往前面遞蚣旱。除非你運(yùn)氣夠好碑幅,站在你前面的第一個(gè)人就是售票員,否則塞绿,你的硬幣通常要在 N 個(gè)人手上傳遞沟涨,才能最終到達(dá)售票員的手里。
實(shí)際開發(fā)中的職責(zé)鏈模式
假設(shè)我們負(fù)責(zé)一個(gè)售賣手機(jī)的電商網(wǎng)站异吻,經(jīng)過分別交納 500元定金和 200元定金的兩輪預(yù)定后(訂單已在此時(shí)生成)裹赴,現(xiàn)在已經(jīng)到了正式購(gòu)買的階段。
公司針對(duì)支付過定金的用戶有一定的優(yōu)惠政策诀浪。在正式購(gòu)買后棋返,已經(jīng)支付過 500元定金的用戶會(huì)收到 100元的商城優(yōu)惠券,200元定金的用戶可以收到 50元的優(yōu)惠券雷猪,而之前沒有支付定金的用戶只能進(jìn)入普通購(gòu)買模式睛竣,也就是沒有優(yōu)惠券,且在庫(kù)存有限的情況下不一定保證能買到求摇。
- orderType :表示訂單類型(定金用戶或者普通購(gòu)買用戶)酵颁, code 的值為 1的時(shí)候是 500元定金用戶,為 2的時(shí)候是 200元定金用戶月帝,為 3的時(shí)候是普通購(gòu)買用戶躏惋。
- pay :表示用戶是否已經(jīng)支付定金,值為 true 或者 false , 雖然用戶已經(jīng)下過 500元定金的訂單嚷辅,但如果他一直沒有支付定金簿姨,現(xiàn)在只能降級(jí)進(jìn)入普通購(gòu)買模式。
- stock :表示當(dāng)前用于普通購(gòu)買的手機(jī)庫(kù)存數(shù)量,已經(jīng)支付過 500 元或者 200 元定金的用戶不受此限制扁位。
const order = function (orderType, pay, stock) {
if (orderType === 1) { // 500 元定金購(gòu)買模式
if (pay === true) { // 已支付定金
console.log('500 元定金預(yù)購(gòu), 得到 100 優(yōu)惠券')
} else { // 未支付定金准潭,降級(jí)到普通購(gòu)買模式
if (stock > 0) { // 用于普通購(gòu)買的手機(jī)還有庫(kù)存
console.log('普通購(gòu)買, 無優(yōu)惠券')
} else {
console.log('手機(jī)庫(kù)存不足')
}
}
}
else if (orderType === 2) { // 200 元定金購(gòu)買模式
if (pay === true) {
console.log('200 元定金預(yù)購(gòu), 得到 50 優(yōu)惠券')
} else {
if (stock > 0) {
console.log('普通購(gòu)買, 無優(yōu)惠券')
} else {
console.log('手機(jī)庫(kù)存不足')
}
}
}
else if (orderType === 3) {
if (stock > 0) {
console.log('普通購(gòu)買, 無優(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)行暇务,但接下來的維護(hù)工作無疑是個(gè)夢(mèng)魘泼掠。
用職責(zé)鏈模式重構(gòu)代碼
現(xiàn)在我們采用職責(zé)鏈模式重構(gòu)這段代碼,先把 500 元訂單垦细、200 元訂單以及普通購(gòu)買分成 3個(gè)函數(shù)择镇。接下來把 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)買函數(shù),代碼如下:
// 500元訂單
const 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元訂單
const 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)買訂單
const orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log('普通購(gòu)買, 無優(yōu)惠券')
} else {
console.log('手機(jī)庫(kù)存不足')
}
}
order500(1, true, 500) // 輸出:500 元定金預(yù)購(gòu), 得到 100 優(yōu)惠券
order500(1, false, 500) // 輸出:普通購(gòu)買, 無優(yōu)惠券
order500(2, true, 500) // 輸出:200 元定金預(yù)購(gòu), 得到 500 優(yōu)惠券
order500(3, false, 500) // 輸出:普通購(gòu)買, 無優(yōu)惠券
order500(3, false, 0) // 輸出:手機(jī)庫(kù)存不足
可以看到惹骂,執(zhí)行結(jié)果和前面那個(gè)巨大的 order 函數(shù)完全一樣苏携,但是代碼的結(jié)構(gòu)已經(jīng)清晰了很多,我們把一個(gè)大函數(shù)拆分了 3個(gè)小函數(shù)析苫,去掉了許多嵌套的條件分支語句。
目前已經(jīng)有了不小的進(jìn)步穿扳,但我們不會(huì)滿足于此衩侥,雖然已經(jīng)把大函數(shù)拆分成了互不影響的 3個(gè)小函數(shù),但可以看到矛物,請(qǐng)求在鏈條傳遞中的順序非常僵硬茫死,傳遞請(qǐng)求的代碼被耦合在了業(yè)務(wù)函數(shù)之中:
const 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 元訂單
}
}
這依然是違反開放?封閉原則的,如果有天我們要增加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é)我們采用一種更靈活的方式糙及,來改進(jìn)上面的職責(zé)鏈模式详幽,目標(biāo)是讓鏈中的各個(gè)節(jié)點(diǎn)可以靈活拆分和重組。
首先需要改寫一下分別表示 3種購(gòu)買模式的節(jié)點(diǎn)函數(shù),我們約定唇聘,如果某個(gè)節(jié)點(diǎn)不能處理請(qǐng)求版姑,則返回一個(gè)特定的字符串 'nextSuccessor' 來表示該請(qǐng)求需要繼續(xù)往后面?zhèn)鬟f:
// 我們約定,如果某個(gè)節(jié)點(diǎn)不能處理請(qǐng)求迟郎,則返回一個(gè)特定的字符串 'nextSuccessor' 來表示該請(qǐng)求需要繼續(xù)往后面?zhèn)鬟f
const 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)是誰,反正把請(qǐng)求往后面?zhèn)鬟f
}
}
const 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)是誰表制,反正把請(qǐng)求往后面?zhèn)鬟f
}
}
const orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log('普通購(gòu)買,無優(yōu)惠券')
} else {
console.log('手機(jī)庫(kù)存不足')
}
}
接下來需要把函數(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)
const Chain = function (fn) {
this.fn = fn
this.successor = null
}
Chain.prototype.setNextSuccessor = function (successor) {
return this.successor = successor
}
Chain.prototype.passRequest = function () {
const 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):
const chainOrder500 = new Chain(order500)
const chainOrder200 = new Chain(order200)
const 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)買,無優(yōu)惠券
chainOrder500.passRequest(1, false, 0) // 輸出:手機(jī)庫(kù)存不足
異步的職責(zé)鏈
<script>
function Fn1() {
console.log(1)
return "nextSuccessor"
}
function Fn2() {
console.log(2)
const self = this
setTimeout(function () {
self.next()
}, 1000)
}
function Fn3() {
console.log(3)
}
// 下面需要編寫職責(zé)鏈模式的封裝構(gòu)造函數(shù)方法
const Chain = function (fn) {
this.fn = fn
this.successor = null
}
Chain.prototype.setNextSuccessor = function (successor) {
return this.successor = successor
}
// 把請(qǐng)求往下傳遞
Chain.prototype.passRequest = function () {
const ret = this.fn.apply(this, arguments)
if (ret === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments)
}
return ret
}
Chain.prototype.next = function () {
return this.successor && this.successor.passRequest.apply(this.successor, arguments)
}
//現(xiàn)在我們把3個(gè)函數(shù)分別包裝成職責(zé)鏈節(jié)點(diǎn):
const chainFn1 = new Chain(Fn1)
const chainFn2 = new Chain(Fn2)
const chainFn3 = new Chain(Fn3)
// 然后指定節(jié)點(diǎn)在職責(zé)鏈中的順序
chainFn1.setNextSuccessor(chainFn2)
chainFn2.setNextSuccessor(chainFn3)
chainFn1.passRequest() // 打印出1镶骗,2 過1秒后 會(huì)打印出3
</script>
調(diào)用函數(shù) chainFn1.passRequest() 后桶现,會(huì)先執(zhí)行發(fā)送者Fn1這個(gè)函數(shù) 打印出 1,然后返回字符串 nextSuccessor 接著就執(zhí)行return this.successor && this.successor.passRequest.apply(this.successor,arguments) 這個(gè)函數(shù)到 Fn2鼎姊,打印 2骡和,接著里面有一個(gè)setTimeout 定時(shí)器異步函數(shù),需要把請(qǐng)求給職責(zé)鏈中的下一個(gè)節(jié)點(diǎn)相寇,因此過一秒后會(huì)打印出 3慰于。
職責(zé)鏈的優(yōu)缺點(diǎn)
職責(zé)鏈模式的優(yōu)點(diǎn)是:
- 解耦了請(qǐng)求發(fā)送者和N個(gè)接收者之間的復(fù)雜關(guān)系,不需要知道鏈中那個(gè)節(jié)點(diǎn)能處理你的請(qǐng)求唤衫,所以你只需要把請(qǐng)求傳遞到第一個(gè)節(jié)點(diǎn)即可婆赠。
- 鏈中的節(jié)點(diǎn)對(duì)象可以靈活地拆分重組,增加或刪除一個(gè)節(jié)點(diǎn)佳励,或者改變節(jié)點(diǎn)的位置都是很簡(jiǎn)單的事情休里。
- 我們還可以手動(dòng)指定節(jié)點(diǎn)的起始位置,并不是說非得要從其實(shí)節(jié)點(diǎn)開始傳遞的赃承。
職責(zé)鏈模式的缺點(diǎn)是:
- 職責(zé)鏈模式中多了一點(diǎn)節(jié)點(diǎn)對(duì)象妙黍,可能在某一次請(qǐng)求過程中,大部分節(jié)點(diǎn)沒有起到實(shí)質(zhì)性作用瞧剖,他們的作用只是讓請(qǐng)求傳遞下去废境,從性能方面考慮,避免過長(zhǎng)的職責(zé)鏈提高性能。