使用職責鏈模式來重構你的程式碼

jacintoface發表於2019-02-12

在這裡我們先想象一個場景,假設我們是一個售賣手機的網站,前期出過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 優惠券
複製程式碼

這段程式碼可以得到我們想要的結果,但是這段程式碼的可維護性和可閱讀性都很差,下面我們可以使用職責鏈模式來嘗試改進它。

顧名思義,責任鏈模式(Chain of Responsibility Pattern)為請求建立了一個接收者物件的鏈。這種模式給予請求的型別,對請求的傳送者和接收者進行解耦。在這種模式中,通常每個接收者都包含對另一個接收者的引用。如果一個物件不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推。

問題解決: 職責鏈上的處理者負責處理請求,客戶只需要將請求傳送到職責鏈上即可,無須關心請求的處理細節和請求的傳遞,所以職責鏈將請求的傳送者和請求的處理者解耦了。

程式碼如下:

// 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函式執行結果完全一致,但是程式碼結構已經清晰了很多,我們避免了一些冗餘的ifelse,並且將一個體積龐大的函式拆分成邏輯更加清晰的小函式。但是這樣拆分明顯也有一個問題,請求的傳遞是強耦合的,即500order和200的order是強耦合的,如果我們需要新增一個400元的訂單,那麼我又需要去修改函式內部,那麼有沒有更加靈活的方式呢?答案是有的。

我們可以用一個職責鏈類Chain和特定的傳遞請求的字串`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

// Chain.prototype.setNextSuccessor 指定在鏈中的下一個節點
// Chain.prototype.passRequest 傳遞請求給某個節點
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(2, true, 500) // 輸出:200 元定金預購,得到 50 優惠券
chainOrder500.passRequest(3, true, 500) // 輸出:普通購買,無優惠券
chainOrder500.passRequest(1, false, 0) // 輸出:手機庫存不足
複製程式碼

這樣我們可以靈活的增加,修改,刪除節點順序,如果突然來一個需求說需要支援300元的定金, 那我們直接新增一個節點即可

var order300 = function(){
 // 具體實現略
};
chainOrder300= new Chain( order300 );
chainOrder500.setNextSuccessor( chainOrder300);
chainOrder300.setNextSuccessor( chainOrder200); 
複製程式碼

使用職責鏈模式,我們可以只增加一個節點並且調整順序即可就,並不會像第一個函式一樣需要修改訂單函式程式碼,因為在實際場景中,業務邏輯遠遠比這個複雜,出錯率也比這個例子高得多。

非同步的職責鏈

在上文中,我們使用同步返回的`nextSuccess`表示需要傳遞請求給下一個節點,但是實際開發中肯定會遇到非同步場景,這時候我們需要手動的觸發一個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() 
複製程式碼

職責鏈模式優點

  1. 降低了請求傳送者和請求接受者的耦合關係,我們不用關心它走了哪一個請求,我們都交給第一個節點處理就行
  2. 增強給物件指派職責的靈活性。通過改變鏈內的成員或者調動它們的次序,允許動態地新增或者刪除責任。
  3. 增加新的請求處理類很方便。

缺點

  1. 不能保證請求一定被接收, 除非我們在鏈尾處理了這種請求。
  2. 系統效能將受到一定影響,在請求過程中,大部分節點僅僅做了請求傳遞的作用,並且系統會增加額外的職責鏈類

相關文章