鏈式呼叫 | 我的程式碼沒有else

TIGERB發表於2020-04-04

嗯,我的程式碼沒有else系列,一個設計模式業務真實使用的golang系列。

鏈式呼叫 | 我的程式碼沒有else

前言

本系列主要分享,如何在我們的真實業務場景中使用設計模式。

本系列文章主要採用如下結構:

  • 什麼是「XX設計模式」?
  • 什麼真實業務場景可以使用「XX設計模式」?
  • 怎麼用「XX設計模式」?

本文主要介紹「責任鏈模式」如何在真實業務場景中使用。

什麼是「責任鏈模式」?

首先把一系列業務按職責劃分成不同的物件,接著把這一系列物件構成一個鏈,然後在這一系列物件中傳遞請求物件,直到被處理為止。

我們從概念中可以看出責任鏈模式有如下明顯的優勢:

  • 按職責劃分:解耦
  • 物件鏈:邏輯清晰

但是有一點直到被處理為止,代表最終只會被一個實際的業務物件執行了實際的業務邏輯,明顯適用的場景並不多。但是除此之外,上面的那兩點優勢還是讓人很心動,所以,為了適用於目前所接觸的絕大多數業務場景,把概念進行了簡單的調整,如下:

首先把一系列業務按職責劃分成不同的物件,接著把這一系列物件構成一個鏈,直到“鏈路結束”為止。(結束:異常結束,或鏈路執行完畢結束)

簡單的直到“鏈路結束”為止轉換可以讓我們把責任鏈模式適用於任何複雜的業務場景。

以下是責任鏈模式的具體優勢:

  • 直觀:一眼可觀的業務呼叫過程
  • 無限擴充套件:可無限擴充套件的業務邏輯
  • 高度封裝:複雜業務程式碼依然高度封裝
  • 極易被修改:複雜業務程式碼下修改程式碼只需要專注對應的業務類(結構體)檔案即可,以及極易被調整的業務執行順序

什麼真實業務場景可以用「責任鏈模式(改)」?

滿足如下要求的場景:

業務極度複雜的所有場景

任何雜亂無章的業務程式碼,都可以使用責任鏈模式(改)去重構、設計。

我們有哪些真實業務場景可以用「責任鏈模式(改)」呢?

比如電商系統的下單介面,隨著業務發展不斷的發展,該介面會充斥著各種各樣的業務邏輯。

怎麼用「責任鏈模式(改)」?

關於怎麼用,完全可以生搬硬套我總結的使用設計模式的四個步驟:

  • 業務梳理
  • 業務流程圖
  • 程式碼建模
  • 程式碼demo

業務梳理

步驟 邏輯
1 引數校驗
2 獲取地址資訊
3 地址資訊校驗
4 獲取購物車資料
5 獲取商品庫存資訊
6 商品庫存校驗
7 獲取優惠資訊
8 獲取運費資訊
9 使用優惠資訊
10 扣庫存
11 清理購物車
12 寫訂單表
13 寫訂單商品表
14 寫訂單優惠資訊表
XX 以及未來會增加的邏輯...

業務的不斷髮展變化的:

  • 新的業務被增加
  • 舊的業務被修改

比如增加的新的業務,訂金預售:

  • 4|獲取購物車資料後,需要校驗商品參見訂金預售活動的有效性等邏輯。
  • 等等邏輯

注:流程不一定完全準確

業務流程圖

我們通過梳理的文字業務流程得到了如下的業務流程圖:

鏈式呼叫 | 我的程式碼沒有else

程式碼建模

責任鏈模式主要類主要包含如下特性:

  • 成員屬性
    • nextHandler: 下一個等待被呼叫的物件例項 -> 穩定不變的
  • 成員方法
    • SetNext: 把下一個物件的例項繫結到當前物件的nextHandler屬性上 -> 穩定不變的
    • Do: 當前物件業務邏輯入口 -> 變化的
    • Run: 呼叫當前物件的DonextHandler不為空則呼叫nextHandler.Do -> 穩定不變的

套用到下單介面虛擬碼實現如下:

一個父類(抽象類):

- 成員屬性
	+ `nextHandler`: 下一個等待被呼叫的物件例項
- 成員方法
	+ 實體方法`SetNext`: 實現把下一個物件的例項繫結到當前物件的`nextHandler`屬性上
	+ 抽象方法`Do`: 當前物件業務邏輯入口
	+ 實體方法`Run`: 實現呼叫當前物件的`Do`,`nextHandler`不為空則呼叫`nextHandler.Do`

子類一(引數校驗)
- 繼承抽象類父類
- 實現抽象方法`Do`:具體的引數校驗邏輯

子類二(獲取地址資訊)
- 繼承抽象類父類
- 實現抽象方法`Do`:具體獲取地址資訊的邏輯

子類三(獲取購物車資料)
- 繼承抽象類父類
- 實現抽象方法`Do`:具體獲取購物車資料的邏輯

......略

子類X(以及未來會增加的邏輯)
- 繼承抽象類父類
- 實現抽象方法`Do`:以及未來會增加的邏輯
複製程式碼

但是,golang裡沒有的繼承的概念,要複用成員屬性nextHandler、成員方法SetNext、成員方法Run怎麼辦呢?我們使用合成複用的特性變相達到“繼承複用”的目的,如下:

一個介面(interface):

- 抽象方法`SetNext`: 待實現把下一個物件的例項繫結到當前物件的`nextHandler`屬性上
- 抽象方法`Do`: 待實現當前物件業務邏輯入口
- 抽象方法`Run`: 待實現呼叫當前物件的`Do`,`nextHandler`不為空則呼叫`nextHandler.Do`

一個基礎結構體:

- 成員屬性
	+ `nextHandler`: 下一個等待被呼叫的物件例項
- 成員方法
	+ 實體方法`SetNext`: 實現把下一個物件的例項繫結到當前物件的`nextHandler`屬性上
	+ 實體方法`Run`: 實現呼叫當前物件的`Do`,`nextHandler`不為空則呼叫`nextHandler.Do`

子類一(引數校驗)
- 合成複用基礎結構體
- 實現抽象方法`Do`:具體的引數校驗邏輯

子類二(獲取地址資訊)
- 合成複用基礎結構體
- 實現抽象方法`Do`:具體獲取地址資訊的邏輯

子類三(獲取購物車資料)
- 合成複用基礎結構體
- 實現抽象方法`Do`:具體獲取購物車資料的邏輯

......略

子類X(以及未來會增加的邏輯)
- 合成複用基礎結構體
- 實現抽象方法`Do`:以及未來會增加的邏輯
複製程式碼

同時得到了我們的UML圖:

鏈式呼叫 | 我的程式碼沒有else

程式碼demo

package main

//---------------
//我的程式碼沒有`else`系列
//責任鏈模式
//@auhtor TIGERB<https://github.com/TIGERB>
//---------------

import (
	"fmt"
	"runtime"
)

// Context Context
type Context struct {
}

// Handler 處理
type Handler interface {
	// 自身的業務
	Do(c *Context) error
	// 設定下一個物件
	SetNext(h Handler) Handler
	// 執行
	Run(c *Context) error
}

// Next 抽象出來的 可被合成複用的結構體
type Next struct {
	// 下一個物件
	nextHandler Handler
}

// SetNext 實現好的 可被複用的SetNext方法
// 返回值是下一個物件 方便寫成鏈式程式碼優雅
// 例如 nullHandler.SetNext(argumentsHandler).SetNext(signHandler).SetNext(frequentHandler)
func (n *Next) SetNext(h Handler) Handler {
	n.nextHandler = h
	return h
}

// Run 執行
func (n *Next) Run(c *Context) (err error) {
	// 由於go無繼承的概念 這裡無法執行當前handler的Do
	// n.Do(c)
	if n.nextHandler != nil {
		// 合成複用下的變種
		// 執行下一個handler的Do
		if err = (n.nextHandler).Do(c); err != nil {
			return
		}
		// 執行下一個handler的Run
		return (n.nextHandler).Run(c)
	}
	return
}

// NullHandler 空Handler
// 由於go無繼承的概念 作為鏈式呼叫的第一個載體 設定實際的下一個物件
type NullHandler struct {
	// 合成複用Next的`nextHandler`成員屬性、`SetNext`成員方法、`Run`成員方法
	Next
}

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

// ArgumentsHandler 校驗引數的handler
type ArgumentsHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *ArgumentsHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "校驗引數成功...")
	return
}

// AddressInfoHandler 地址資訊handler
type AddressInfoHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *AddressInfoHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "獲取地址資訊...")
	fmt.Println(runFuncName(), "地址資訊校驗...")
	return
}

// CartInfoHandler 獲取購物車資料handler
type CartInfoHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *CartInfoHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "獲取購物車資料...")
	return
}

// StockInfoHandler 商品庫存handler
type StockInfoHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *StockInfoHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "獲取商品庫存資訊...")
	fmt.Println(runFuncName(), "商品庫存校驗...")
	return
}

// PromotionInfoHandler 獲取優惠資訊handler
type PromotionInfoHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *PromotionInfoHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "獲取優惠資訊...")
	return
}

// ShipmentInfoHandler 獲取運費資訊handler
type ShipmentInfoHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *ShipmentInfoHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "獲取運費資訊...")
	return
}

// PromotionUseHandler 使用優惠資訊handler
type PromotionUseHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *PromotionUseHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "使用優惠資訊...")
	return
}

// StockSubtractHandler 庫存操作handler
type StockSubtractHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *StockSubtractHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "扣庫存...")
	return
}

// CartDelHandler 清理購物車handler
type CartDelHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *CartDelHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "清理購物車...")
	// err = fmt.Errorf("CartDelHandler.Do fail")
	return
}

// DBTableOrderHandler 寫訂單表handler
type DBTableOrderHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *DBTableOrderHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "寫訂單表...")
	return
}

// DBTableOrderSkusHandler 寫訂單商品表handler
type DBTableOrderSkusHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *DBTableOrderSkusHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "寫訂單商品表...")
	return
}

// DBTableOrderPromotionsHandler 寫訂單優惠資訊表handler
type DBTableOrderPromotionsHandler struct {
	// 合成複用Next
	Next
}

// Do 校驗引數的邏輯
func (h *DBTableOrderPromotionsHandler) Do(c *Context) (err error) {
	fmt.Println(runFuncName(), "寫訂單優惠資訊表...")
	return
}

// 獲取正在執行的函式名
func runFuncName() string {
	pc := make([]uintptr, 1)
	runtime.Callers(2, pc)
	f := runtime.FuncForPC(pc[0])
	return f.Name()
}

func main() {
	// 初始化空handler
	nullHandler := &NullHandler{}

	// 鏈式呼叫 程式碼是不是很優雅
	// 很明顯的鏈 邏輯關係一覽無餘
	nullHandler.SetNext(&ArgumentsHandler{}).
		SetNext(&AddressInfoHandler{}).
		SetNext(&CartInfoHandler{}).
		SetNext(&StockInfoHandler{}).
		SetNext(&PromotionInfoHandler{}).
		SetNext(&ShipmentInfoHandler{}).
		SetNext(&PromotionUseHandler{}).
		SetNext(&StockSubtractHandler{}).
		SetNext(&CartDelHandler{}).
		SetNext(&DBTableOrderHandler{}).
		SetNext(&DBTableOrderSkusHandler{}).
		SetNext(&DBTableOrderPromotionsHandler{})
		//無限擴充套件程式碼...

	// 開始執行業務
	if err := nullHandler.Run(&Context{}); err != nil {
		// 異常
		fmt.Println("Fail | Error:" + err.Error())
		return
	}
	// 成功
	fmt.Println("Success")
	return
}
複製程式碼

程式碼執行結果:

[Running] go run "../easy-tips/go/src/patterns/responsibility/responsibility-order-submit.go"
main.(*ArgumentsHandler).Do 校驗引數成功...
main.(*AddressInfoHandler).Do 獲取地址資訊...
main.(*AddressInfoHandler).Do 地址資訊校驗...
main.(*CartInfoHandler).Do 獲取購物車資料...
main.(*StockInfoHandler).Do 獲取商品庫存資訊...
main.(*StockInfoHandler).Do 商品庫存校驗...
main.(*PromotionInfoHandler).Do 獲取優惠資訊...
main.(*ShipmentInfoHandler).Do 獲取運費資訊...
main.(*PromotionUseHandler).Do 使用優惠資訊...
main.(*StockSubtractHandler).Do 扣庫存...
main.(*CartDelHandler).Do 清理購物車...
main.(*DBTableOrderHandler).Do 寫訂單表...
main.(*DBTableOrderSkusHandler).Do 寫訂單商品表...
main.(*DBTableOrderPromotionsHandler).Do 寫訂單優惠資訊表...
Success
複製程式碼

結語

最後總結下,「責任鏈模式(改)」抽象過程的核心是:

  • 按職責劃分:業務邏輯歸類,收斂的過程。
  • 物件鏈:把收斂之後的業務物件構成物件鏈,依次被執行。
特別說明:
1. 我的程式碼沒有`else`,只是一個在程式碼合理設計的情況下自然而然無限接近或者達到的結果,並不是一個硬性的目標,務必較真。
2. 本系列的一些設計模式的概念可能和原概念存在差異,因為會結合實際使用,取其精華,適當改變,靈活使用。
複製程式碼

鏈式呼叫 | 我的程式碼沒有else

相關文章