首頁  >  文章  >  後端開發  >  深入聊聊設計模式利器之「職責鏈模式」(附go實現流程)

深入聊聊設計模式利器之「職責鏈模式」(附go實現流程)

藏色散人
藏色散人轉載
2023-01-17 11:43:371636瀏覽

這篇文章為大家帶來了關於golang設計模式的相關知識,其中主要介紹了職責鏈模式是什麼及其作用價值,還有職責鏈Go代碼的具體實現方法,下面一起來看一下,希望對需要的朋友有幫助。

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

職責鏈模式

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

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

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

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

職責鏈的價值

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

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

深入聊聊設計模式利器之「職責鏈模式」(附go實現流程)

「職責連結模式—購物下單—清純版

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

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

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

深入聊聊設計模式利器之「職責鏈模式」(附go實現流程)

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

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

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

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

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

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

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

那麼現在我們可以確定:

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

用職責鏈模式實作流程

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

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

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

    成員屬性
    • nextHandler: 下一個等待被呼叫的物件實例
  • 成員方法
    • SetNext: 把下一個物件的實例綁定到目前物件的nextHandler屬性上;
    • Do: 目前物件業務邏輯入口,他是每個處理物件實作自己邏輯的地方;
    • Execute : 負責職責鏈上所要求的處理與傳遞;它會呼叫目前物件的DonextHandler不為空則呼叫nextHandler.Do;
如果抽象化成UML 類別圖表示的話,差不多就是下面這個樣子。

深入聊聊設計模式利器之「職責鏈模式」(附go實現流程)

定義了一個職責鏈模式處理物件的介面

Handler,由ConcreteHandler –具體處理物件的類型來實作。

觀察上圖以及上面物件特性的分析,其實是能看出

SetNextExecute 這兩個行為是每個ConcreteHandler#都一樣的,所以這兩個可以交給抽象處理類型來實現,每個特定處理物件再繼承抽象類型,即可減少重複操作。

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

深入聊聊設計模式利器之「職責鏈模式」(附go實現流程)

#了解完職責鏈模式從介面和型別設計上應該怎麼實現後,我們進入程式碼實作環節,職責鏈模式如果用純粹物件導向的語言實作起來還是很方便的,把上面的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的Dofunc (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方法就是按照这个原理实现的,同时这也是职责链跟装饰器模式的一个区别,装饰器模式无法在增强实体的过程中停止,只能执行完整个装饰链路。

后面大家可以看看针对那些可能未来经常会变的核心业务流程,可以在设计初期就考虑使用职责链来实现,减轻未来流程不停迭代时不好扩展的痛点。当然职责链也不是万能的,对于那些固定的流程显然是不适合的。咱们千万不要手里拿着锤子就看什么都是钉子,所有的设计模式一定要用在合适的地方。

既然这里提到了装饰器,那么下一期就写写装饰器吧,不对,装饰器算是代理模式的一个特殊应用,那就还是先介绍代理未来再介绍装饰器吧,这样阅读体验会更好一些。

喜欢这系列文章的朋友们还请多多关注,转发起来吧。

推荐学习:《go视频教程

以上是深入聊聊設計模式利器之「職責鏈模式」(附go實現流程)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:learnku.com。如有侵權,請聯絡admin@php.cn刪除