Go 設計模式之責任鏈模式

其實很多人不知道,責任鏈模式是我們工作中經常遇到的模式,特別是 web 後端工程師,我們工作中每時每刻都在用:因爲市面上大部分的 web 框架的過濾器基本都是基於這個設計模式爲基本模式搭建的。

  1. 模式介紹

我們先來看一下責任鏈模式(Chain Of Responsibility Design Pattern )的英文介紹:Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

翻譯成中文就是:將請求的發送和接收解耦,讓多個接收對象都有機會處理這個請求。將這些接收對象串成一條鏈,並沿着這條鏈傳遞這個請求,直到鏈上的某個接收對象能夠處理它爲止。

這麼說比較抽象,用更加容易理解的話來進一步解讀一下。在責任鏈模式中,一個請求過來,會有多個處理器(也就是剛剛定義中說的 “接收對象”)依次處理同一個請求。即請求先經過 A 處理器處理,然後再把請求傳遞給 B 處理器,B 處理器處理完後再傳遞給 C 處理器,以此類推,形成一個鏈條。鏈條上的每個處理器各自承擔各自的處理職責,所以叫作責任鏈模式。

  1. 模式 demo

2.1 UML

責任鏈模式(Chain Of Responsibility Design Pattern )的整體結構如下:

2.2 標準 demo

我們依據標準的 UML 圖,寫出一個具體的例子(對應 UML 圖):

首先定義一個接口 IHandler

type IHandler interface {
    SetNext(handler IHandler)
    Handle(score int)
}

然後分別構建三個不同的實現: ConcreteHandler1

type ConcreteHandler1 struct {
    Next IHandler
}
func (c *ConcreteHandler1) Handle(score int) {
    if score < 0 {
        fmt.Println("ConcreteHandler1 處理")
        return
    }
    if c.Next != nil {
        c.Next.Handle(score)
    }
    return
}
func (c *ConcreteHandler1) SetNext(handler IHandler) {
    c.Next = handler
}

ConcreteHandler2

type ConcreteHandler2 struct {
    Next IHandler
}
func (c *ConcreteHandler2) Handle(score int) {
    if score > 0 {
        fmt.Println("ConcreteHandler2 處理")
        return
    }
    if c.Next != nil {
        c.Next.Handle(score)
    }
    return
}
func (c *ConcreteHandler2) SetNext(handler IHandler) {
    c.Next = handler
}

ConcreteHandler3

type ConcreteHandler3 struct {
    Next IHandler
}
func (c *ConcreteHandler3) Handle(score int) {
    if score == 0 {
        fmt.Println("ConcreteHandler3 處理")
        return
    }
    if c.Next != nil {
        c.Next.Handle(score)
    }
    return
}
func (c *ConcreteHandler3) SetNext(handler IHandler) {
    c.Next = handler
}

最後是 main函數:

func main() {
    handler1 := &ConcreteHandler1{}
    handler2 := &ConcreteHandler2{}
    handler3 := &ConcreteHandler3{}
    handler1.SetNext(handler2)
    handler2.SetNext(handler3)
    handler1.Handle(10)
}

打印結果爲:

ConcreteHandler2
 
處理

2.3 改進版 demo

通過以上標準例子不難發現: main函數承接了很多 client 自身之外的 “額外工作”:構建和拼接組裝責任鏈,這不利於後續 client 端的使用和擴展:一不小心可能責任鏈拼就接錯了或者拼接少節點了。我們可以對 UML 做一個改進:增加一個節點管理模塊。改進圖如下:

對比上文的 uml 圖,新增加了一個 ChainHandler結構體用來管理拼接的 Handler,client 端無需瞭解 Handler的業務, Handler的組合可以使用鏈表,也可以使用數組 (當前用了數組)。具體實現如下:先定義 Handler接口:

type Handler interface {
    Handle(score int)
}

然後分別實現 Handler接口的三個結構體: ConcreteHandlerOne

type ConcreteHandlerOne struct {
    Handler
}
func (c *ConcreteHandlerOne) Handle(score int) {
    if score < 0 {
        fmt.Println("ConcreteHandler1 處理")
        return
    }
}

ConcreteHandlerTwo

type ConcreteHandlerTwo struct {
    Handler
}
func (c *ConcreteHandlerTwo) Handle(score int) {
    if score > 0 {
        fmt.Println("ConcreteHandler2 處理")
        return
    }
}

ConcreteHandlerThree

type ConcreteHandlerThree struct {
    Handler
}
func (c *ConcreteHandlerThree) Handle(score int) {
    if score == 0 {
        fmt.Println("ConcreteHandler3 處理")
        return
    }
}

main函數調用 (client 調用):

func main() {
    chain := &ChainHandler{}
    chain.AddHandler(&ConcreteHandlerOne{})
    chain.AddHandler(&ConcreteHandlerTwo{})
    chain.AddHandler(&ConcreteHandlerThree{})
    chain.Handle(10)
}

最終的實現結構圖:

日常工作中出現的責任鏈模式(Chain Of Responsibility Design Pattern )一般都是以上這種包含 Hanlder管理的模式。

  1. 源碼解析

在日常框架和語言基礎庫中,經常能夠看到很多場景使用了責任鏈模式。

3.1 beego 過濾器

可以對比改進版 demo 的 uml 圖,beego 的過濾器就是按照這種模式來設計的(當前參照的 beego 版本是 2.0)。

3.1.1 client 端

調用端首先是過濾器的註冊:

web
.
InsertFilter
(
"/v2/api/*"
,
 web
.
BeforeRouter
,
 auth
.
AuthAPIFilter
)

然後在 github.com/beego/beego/v2@v2.0.3/server/web/router.go的 ControllerRegister結構體的 serveHttp函數中

if len(p.filters[BeforeRouter]) > 0 && p.execFilter(ctx, urlPath, BeforeRouter) {
        goto Admin
}

以上 p.execFilter(ctx,urlPath,BeforeRouter)處,啓動調用。

3.1.2 Handler 接口

Handler 接口很簡單

// HandleFunc define how to process the request
type HandleFunc func(ctx *beecontext.Context)
    ...
type FilterFunc = HandleFunc

3.1.3 Handler 接口實現

接口的實現擴展比較靈活,直接把用戶定義的函數作爲接口的實現。與 client 端中的過濾器註冊聯動。

// 過濾器註冊
web.InsertFilter("/v2/api/*", web.BeforeRouter, auth.AuthAPIFilter)
// 自定義過濾器
var AuthAPIFilter = func(ctx *context.Context) {
    isAccess := validateAccess(ctx)
    if !isAccess {
        res, _ := json.Marshal(r)
        ctx.WriteString(string(res))
        // ctx.Redirect(401, "/401")
    }
}

3.1.4 Handler 管理

Handler的管理模塊是在 github.com/beego/beego/v2@v2.0.3/server/web/router.go的中的 FilterRouter和 ControllerRegister兩個結構體中

// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
    routers      map[string]*Tree
    enablePolicy bool
    enableFilter bool
    policies     map[string]*Tree
    filters      [FinishRouter + 1][]*FilterRouter
    pool         sync.Pool
    // the filter created by FilterChain
    chainRoot *FilterRouter
    // keep registered chain and build it when serve http
    filterChains []filterChainConfig
    cfg *Config
}
type FilterRouter struct {
    filterFunc     FilterFunc
    next           *FilterRouter
    tree           *Tree
    pattern        string
    returnOnOutput bool
    resetParams    bool
}

FilterRouter是一個鏈表,包含用戶自定義的過濾函數; ControllerRegister對 FilterRouter進行管理。

3.2 Go 源碼 http.handler

我們在使用 Go 構建 http web 服務器的時候,使用的 http.Handler 就是使用的責任鏈模式。

package main
import (
    "net/http"
)
func main() {
    s := http.NewServeMux()
    s.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
        // todo ....
        return
    })
    http.ListenAndServe(":80", s)
}

以 2.3的UML圖爲標準,整體的對照結構圖如下:

3.2.1 client 端

整個模式的啓動是隨着 http server 啓動後,接受到請求後的處理開始的。在 net/http/server.go的 serve函數中

func (c *conn) serve(ctx context.Context) {
    ...
    // HTTP cannot have multiple simultaneous active requests.[*]
    // Until the server replies to this request, it can't read another,
    // so we might as well run the handler in this goroutine.
    // [*] Not strictly true: HTTP pipelining. We could let them all process
    // in parallel even if their responses need to be serialized.
    // But we're not going to implement HTTP pipelining because it
    // was never deployed in the wild and the answer is HTTP/2.
    serverHandler{c.server}.ServeHTTP(w, w.req)
    ...
}

可以看到 http server 的原理很簡單,就是 for 死循環等待接收,然後一個請求過來,就對應的生成一個單獨的協程 goroutine去處理。

3.2.2 Handler 接口

Go 源碼中對責任鏈模式的實現非常標準,Handler 接口與設計模式中的 Handler 接口同名,在 net/http/server.go中:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

爲了擴展方便,在使用過程中並非直接使用,而是中間又加了一層抽象層(相當於 Java 中的抽象類了,Go 中沒有抽象類)

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

3.2.3 Handler 接口實現

與上文中提到的 Beego 的過濾器類似,Go 的 Handler 設計的也非常容易擴展,用戶自定義的請求處理函數 Handler 都會變成 Handler的子類。

func main() {
    s := http.NewServeMux()
    s.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
        // todo ....
        return
    })
    http.ListenAndServe(":80", s)
}
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    // 強制類型轉換,轉成了實現了Hanlder的“抽象類”HandlerFunc
    mux.Handle(pattern, HandlerFunc(handler)) 
}

注意看上文的 HandleFunc中的 mux.Handle(pattern,HandlerFunc(handler))這一行,將用戶自定義的處理函數強制轉換成了上文 3.2.2 中的 Handler的 "抽象類" HandlerFunc類型,進而實現了繼承。

3.2.4 Handler 接口的管理類 ChainHandler

Go 中對 Handler 的管理類是在 net/http/server.go文件的 ServeMux結構體和 muxEntry結構體中:

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    es    []muxEntry // slice of entries sorted from longest to shortest.
    hosts bool       // whether any patterns contain hostnames
}
type muxEntry struct {
    h       Handler
    pattern string
}

其中,用戶自定以的處理函數都被封裝到了 muxEntry結構體的 Handler中,一個自定義的函數對應一個 muxEntry, ServeMux使用 hashmap 對 muxEntry集合進行管理(上文的 beego 中是使用的鏈表,上文 demo 中使用了數組)。當 web server 接收到請求的時候, ServeMux會根據 hashmap 找到相應的 handler 然後處理。

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    // *******尋找handler*******
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    ...
    if path != r.URL.Path {
        _, pattern = mux.handler(host, path)
        u := &url.URL{Path: path, RawQuery: r.URL.RawQuery}
        return RedirectHandler(u.String(), StatusMovedPermanently), pattern
    }
    // *******尋找handler*******
    return mux.handler(host, r.URL.Path)
}
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock()
    defer mux.mu.RUnlock()
    // Host-specific pattern takes precedence over generic ones
    if mux.hosts {
        // *******尋找handler*******
        h, pattern = mux.match(host + path)
    }
    if h == nil {
        // *******尋找handler*******
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // ********通過hashmap找到相關handler*********
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

在程序運行過程中,用戶註冊自定義的函數被轉化成了 Handler,然後 Handler又結合用戶自定義的 URL地址被 ServeMux以 URL爲 Key、 Handler爲 Value 做成 hashmap 管理起來;等到請求來的時候, ServeMux就根據用戶請求的 URL地址,從 hashmap 中找到具體的 Hanlder來處理請求。

  1. 總結

責任鏈模式的基本思想就是要處理的請求 (通常會是結構體,然後作爲函數參數);依次經過多個處理對象處理,這些處理函數可以動態的添加和刪除,具備很高的靈活性和擴展性,通常會對這些處理函數做統一處理,存儲方式一般是通過鏈表、數組、hash map 等存儲結構。

責任鏈模式的應用非常廣泛:

  1. 業務場景:作爲敏感詞(涉黃、政治、反動等此)過濾的設計結構

  2. 技術框架:路由、router 過濾器、日誌 log 框架等等

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SjSWc1je2L9lr0OgGUXrGA