職責(zé)鏈模式

摘自《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é)鏈提高性能。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末噩凹,一起剝皮案震驚了整個(gè)濱河市巴元,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驮宴,老刑警劉巖逮刨,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異堵泽,居然都是意外死亡修己,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門迎罗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來睬愤,“玉大人,你說我怎么就攤上這事纹安∮热瑁” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵厢岂,是天一觀的道長(zhǎng)光督。 經(jīng)常有香客問我,道長(zhǎng)塔粒,這世上最難降的妖魔是什么结借? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮卒茬,結(jié)果婚禮上船老,老公的妹妹穿的比我還像新娘。我一直安慰自己圃酵,他們只是感情好柳畔,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著辜昵,像睡著了一般荸镊。 火紅的嫁衣襯著肌膚如雪咽斧。 梳的紋絲不亂的頭發(fā)上堪置,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音张惹,去河邊找鬼舀锨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛宛逗,可吹牛的內(nèi)容都是我干的坎匿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼替蔬!你這毒婦竟也來了告私?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤承桥,失蹤者是張志新(化名)和其女友劉穎驻粟,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凶异,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜀撑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年剩彬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了喉恋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瀑晒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轩褐,到底是詐尸還是另有隱情把介,我是刑警寧澤拗踢,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站君纫,受9級(jí)特大地震影響蓄髓,放射性物質(zhì)發(fā)生泄漏会喝。R本人自食惡果不足惜枉阵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望反璃。 院中可真熱鬧斋攀,春花似錦淳蔼、人聲如沸鹉梨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至祭阀,卻和暖如春专控,著一層夾襖步出監(jiān)牢的瞬間踩官,已是汗流浹背蔗牡。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留黔攒,地道東北人督惰。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓商虐,卻偏偏與公主長(zhǎng)得像秘车,于是被迫代替她去往敵國(guó)和親割笙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咳蔚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354