你也是業務開發?提前用這個設計模式預防產品加需求吧

yakamoz_07發表於2023-01-14

大家好,我是每週在這裡陪大家一起進步的網管。

今天繼續更新設計模式相關的文章,我在前面兩篇關於模板模式和策略模式的文章裡給大家說過一個我總結的”暴論”:“模板、策略和職責鏈三個設計模式是解決業務系統流程複雜多變這個痛點的利器”。這篇文章我們就來一起說說這第三個設計模式利器—職責鏈模式。

職責鏈模式

職責鏈——英文名 Chain of responsibility 有時也被翻譯成責任鏈模式。我看網上叫責任鏈的更多一些,這裡大家知道它們是一個東西就行了。

它是一種行為型設計模式。使用這個模式,我們能為請求建立一條由多個處理器組成的鏈路,每個處理器各自負責自己的職責,相互之間沒有耦合,完成自己任務後請求物件即傳遞到鏈路的下一個處理器進行處理。

職責鏈在很多流行框架裡都有被用到,像中介軟體、攔截器等框架元件都是應用的這種設計模式,這兩個組價大家應該用的比較多。在做Web 介面開發的時候,像記錄訪問日誌、解析Token、格式化介面響應的統一結構這些類似的專案公共操都是在中介軟體、攔截器裡完成的,這樣就能讓這些基礎操作與介面的業務邏輯進行解耦。

中介軟體、攔截器這些元件都是框架給我們設計好的直接往裡面套就可以,今天我們的文章裡要講的是,怎麼把職責鏈應用到我們核心的業務流程設計中,而不僅僅是隻做那些基礎的公共操作。

職責鏈的價值

上面我們說了職責鏈在專案公共元件中的一些應用,讓我們能在核心邏輯的前置和後置流程中增加一些基礎的通用功能。但其實在一些核心的業務中,應用職責鏈模式能夠讓我們無痛地擴充套件業務流程的步驟

比如淘寶在剛剛創立的時候購物生成訂單處理流程起初可能是這樣的。

你也是業務開發?提前用這個設計模式預防產品加需求吧

職責鏈模式—購物下單—清純版

整個流程比較乾淨“使用者引數校驗–購物車資料校驗–商品庫存校驗–運費計算–扣庫存—生成訂單”,我們姑且把它稱為清純版的購物下單流程,這通常都是在產品從0到1的時候,流程比較清純,線上購物你能實現線上選品、下單、支付這些就行了。

不過大家都是網際網路衝浪老手了,也都是吃過見過的主,這個流程要是一直這麼清純,公司的PM和運營就可以走人了。等購物網站跑起來,有消費者了之後,為了提高銷售額,一般會加一些,某些品類商品滿減的促銷手段。

運營也不能閒著,多談點客戶,造個購物節,到時候優惠券安排上多吸引點使用者。那這樣在下訂單的流程中,就得判斷購物車裡的商品是否滿足折扣條件、使用者是否有優惠卷,有的話進行金額減免。相當於我們下單的內部流程中間加了兩個子流程。

你也是業務開發?提前用這個設計模式預防產品加需求吧

職責鏈模式—購物下單—老練版

為了實現新加的邏輯,我們就得在寫好的訂單流程中最起碼加兩個 if else 分支才能加上這兩個邏輯。不過最要命的是因為整個流程耦合在一起,修改了以後我們就得把整個流程全測一遍。並且有了上面的經驗我們也應該知道這個流程以後肯定還會擴充套件,比如再給你加上社群砍一刀、拼單這些功能,以後每次在訂單生成流程中加入步驟都得修改已經寫好的程式碼,怕不怕?

有朋友可能會說,網際網路電商購物可能確實流程比較多樣化,每個公司的流程不一樣。我們再舉個病人去醫院看病的例子,病人看病大體上基本步驟需要有:

掛號—>診室看病—>收費處繳費—>藥房拿藥

但是有可能有的病人需要化驗、拍片子等等,他們在醫院就醫的流程可能是這樣的:

掛號—>初診—>影像科拍片—>複診室—>收費處繳費—>藥房拿藥

所以就醫這個流程也是會根據病人情況的不同,步驟有所增加的。

那麼現在我們可以確定:假如一個流程的步驟不固定,為了在流程中增加步驟時,不必修改原有已經開發好,經過測試的流程,我們需要讓整個流程中的各個步驟解耦,來增加流程的擴充套件性,這種時候就可以使用職責鏈模式啦,這個模式可以讓我們先設定流程鏈路中有哪些步驟,再去執行

用職責鏈模式實現流程

如果讓我們設計責任鏈應該怎麼設計呢?應該提供和實現哪些方法?怎麼使用它把流程裡的步驟串起來呢?這裡我們用職責鏈模式把就診看病這個場景中的流程步驟實現一遍給大家做個演示,購物下單的流程類似,我們們下去可以自己嘗試實現一遍,先學會職責鏈模式的結構做些Mock示例,掌握熟練了後面再嘗試著用它解決業務中的問題。

首先我們透過上面流程擴充套件的痛點可以想到,流程中每個步驟都應由一個處理物件來完成邏輯抽象、所有處理物件都應該提供統一的處理自身邏輯的方法,其次還應該維護指向下一個處理物件的引用,當前步驟自己邏輯處理完後,就呼叫下一個物件的處理方法,把請求交給後面的物件進行處理,依次遞進直到流程結束。

總結下來,實現責任鏈模式的物件最起碼需要包含如下特性:

  • 成員屬性
    • nextHandler: 下一個等待被呼叫的物件例項
  • 成員方法
    • SetNext: 把下一個物件的例項繫結到當前物件的nextHandler屬性上;
    • Do: 當前物件業務邏輯入口,他是每個處理物件實現自己邏輯的地方;
    • Execute: 負責職責鏈上請求的處理和傳遞;它會呼叫當前物件的DonextHandler不為空則呼叫nextHandler.Do

如果抽象成 UML 類圖表示的話,差不多就是下面這個樣子。

你也是業務開發?提前用這個設計模式預防產品加需求吧

定義了一個職責鏈模式處理物件的介面Handler,由ConcreteHandler –具體處理物件的型別來實現。

觀察上圖以及上面物件特性的分析,其實是能看出 SetNextExecute 這兩個行為是每個 ConcreteHandler 都一樣的,所以這兩個可以交給抽象處理型別來實現,每個具體處理物件再繼承抽象型別,即可減少重複操作。

所以責任鏈模式的抽象和提煉可以進化成下圖這樣:

你也是業務開發?提前用這個設計模式預防產品加需求吧

瞭解完職責鏈模式從介面和型別設計上應該怎麼實現後,我們進入程式碼實現環節,職責鏈模式如果用純物件導向的語言實現起來還是很方便的,把上面的UML類圖直接翻譯成介面、抽象類,再搞幾個實現類就完事。

想把上面這個UML類圖翻譯成Go程式碼還是有點難度的。這裡我們們提供一個用 Go 實現職責鏈模式完成醫院就診流程的程式碼示例。

職責鏈 Go 程式碼實現

雖然 Go 不支援繼承,不過我們還是能用型別的匿名組合來實現,下面以病人去醫院看病這個處理流程為例提供一個具體示例。

看病的具體流程如下:

掛號—>診室看病—>收費處繳費—>藥房拿藥

我們的目標是利用責任鏈模式,實現這個流程中的每個步驟,且相互間不耦合,還支援向流程中增加步驟。

先來實現職責鏈模式裡的公共部分—即模式的介面和抽象類


type PatientHandler interface {
 Execute(*patient) error
 SetNext(PatientHandler) PatientHandler
 Do(*patient) error
}
// 充當抽象型別,實現公共方法,抽象方法不實現留給實現類自己實現
type Next struct {
 nextHandler PatientHandler
}

func (n *Next) SetNext(handler PatientHandler) PatientHandler {
 n.nextHandler = handler
 return handler
}

func (n *Next) Execute(patient *patient) (err error) {
 // 呼叫不到外部型別的 Do 方法,所以 Next 不能實現 Do 方法
 if n.nextHandler != nil {
  if err = n.nextHandler.Do(patient); err != nil {
   return
  }

  return n.nextHandler.Execute(patient)
 }

 return
}

上面程式碼中Next型別充當了模式中抽象類的角色,關於這個Next型別這裡再重點說明一下。

在我們的職責鏈的UML圖裡有說明Do方法是一個抽象方法,留給具體處理請求的類來實現,所以這裡Next型別充當抽象型別,只實現公共方法,抽象方法留給實現類自己實現。並且由於 Go 並不支援繼承,即使Next實現了Do方法,也不能達到在父類方法中呼叫子類方法的效果—即在我們的例子裡面用Next 型別的Execute方法呼叫不到外部實現型別的Do方法。

所以我們這裡選擇Next型別直接不實現Do方法,這也是在暗示這個型別是專門用作讓實現類進行內嵌組合使用的。

接下來我們定義職責鏈要處理的請求,再回看一下我們的UML圖,實現處理邏輯和請求傳遞的DoExecute方法的引數都是流程中要處理的請求。這裡是醫院接診的流程,所以我們定義一個患者類作為流程的請求。

//流程中的請求類--患者
type patient struct {
 Name              string
 RegistrationDone  bool
 DoctorCheckUpDone bool
 MedicineDone      bool
 PaymentDone       bool
}

複製

然後我們按照掛號—>診室看病—>收費處繳費—>藥房拿藥這個流程定義四個步驟的處理類,來分別實現每個環節的邏輯。


// Reception 掛號處處理器
type Reception struct {
 Next
}

func (r *Reception) Do(p *patient) (err error) {
 if p.RegistrationDone {
  fmt.Println("Patient registration already done")
  return
 }
 fmt.Println("Reception registering patient")
 p.RegistrationDone = true
 return
}

// Clinic 診室處理器--用於醫生給病人看病
type Clinic struct {
 Next
}

func (d *Clinic) Do(p *patient) (err error) {
 if p.DoctorCheckUpDone {
  fmt.Println("Doctor checkup already done")
  return
 }
 fmt.Println("Doctor checking patient")
 p.DoctorCheckUpDone = true
 return
}

// Cashier 收費處處理器
type Cashier struct {
 Next
}

func (c *Cashier) Do(p *patient) (err error) {
 if p.PaymentDone {
  fmt.Println("Payment Done")
  return
 }
 fmt.Println("Cashier getting money from patient patient")
 p.PaymentDone = true
 return
}

// Pharmacy 藥房處理器
type Pharmacy struct {
 Next
}

func (m *Pharmacy) Do (p *patient) (err error) {
 if p.MedicineDone {
  fmt.Println("Medicine already given to patient")
  return
 }
 fmt.Println("Pharmacy giving medicine to patient")
 p.MedicineDone = true
 return
}

複製

處理器定義好了,怎麼給用他們串成患者就診這個流程呢?

func main() {
 receptionHandler := &Reception{}
 patient := &patient{Name: "abc"}
 // 設定病人看病的鏈路
 receptionHandler.SetNext(&Clinic{}).SetNext(&Cashier{}).SetNext(&Pharmacy{})
  receptionHandler.Execute(patient)
}

複製

上面的鏈式呼叫看起來是不是很清爽,嘿嘿別高興太早,這裡邊有個BUG— 即Reception接診掛號這個步驟提供的邏輯沒有呼叫到,所以我們這裡再定義個StartHandler 型別,它不提供處理實現只是作為第一個Handler向下轉發請求


// StartHandler 不做操作,作為第一個Handler向下轉發請求
type StartHandler struct {
 Next
}

// Do 空Handler的Do
func (h *StartHandler) Do(c *patient) (err error) {
 // 空Handler 這裡什麼也不做 只是載體 do nothing...
 return
}

複製

這也是Go 語法限制,公共方法Exeute並不能像物件導向那樣先呼叫this.Do 再呼叫this.nextHandler.Do 具體原因我們們上邊已經解釋過了,如果覺得不清楚的可以拿Java實現一遍看看區別,再琢磨一下為啥Go裡邊不行。

所以整個流程每個環節都能被正確執行到,應該這樣把處理類串起來。


func main() {
 patientHealthHandler := StartHandler{}
 //
 patient := &patient{Name: "abc"}
 // 設定病人看病的鏈路
 patientHealthHandler.SetNext(&Reception{}).// 掛號
  SetNext(&Clinic{}). // 診室看病
  SetNext(&Cashier{}). // 收費處交錢
  SetNext(&Pharmacy{}) // 藥房拿藥
 //還可以擴充套件,比如中間加入化驗科化驗,影像科拍片等等

 // 執行上面設定好的業務流程
 if err := patientHealthHandler.Execute(patient); err != nil {
  // 異常
  fmt.Println("Fail | Error:" + err.Error())
  return
 }
 // 成功
 fmt.Println("Success")
}

複製

總結

職責鏈模式所擁有的特點讓流程中的每個處理節點都只需關注滿足自己處理條件的請求進行處理即可,對於不感興趣的請求,會直接轉發給下一個節點物件進行處理。

另外職責鏈也可以設定中止條件,針對我們文中的例子就是在Execute方法里加判斷,一旦滿足中止後就不再繼續往鏈路的下級節點傳遞請求。Gin 的中介軟體的abort方法就是按照這個原理實現的,同時這也是職責鏈跟裝飾器模式的一個區別,裝飾器模式無法在增強實體的過程中停止,只能執行完整個裝飾鏈路。

後面大家可以看看針對那些可能未來經常會變的核心業務流程,可以在設計初期就考慮使用職責鏈來實現,減輕未來流程不停迭代時不好擴充套件的痛點。當然職責鏈也不是萬能的,對於那些固定的流程顯然是不適合的。我們們千萬不要手裡拿著錘子就看什麼都是釘子,所有的設計模式一定要用在合適的地方。

既然這裡提到了裝飾器,那麼下一期就寫寫裝飾器吧,不對,裝飾器算是代理模式的一個特殊應用,那就還是先介紹代理未來再介紹裝飾器吧,這樣閱讀體驗會更好一些。

喜歡這系列文章的朋友們還請多多關注,轉發起來吧。

  • END -
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章