Go 設計模式之責任鏈模式
其實很多人不知道,責任鏈模式是我們工作中經常遇到的模式,特別是 web 後端工程師,我們工作中每時每刻都在用:因爲市面上大部分的 web 框架的過濾器基本都是基於這個設計模式爲基本模式搭建的。
- 模式介紹
我們先來看一下責任鏈模式(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 處理器,以此類推,形成一個鏈條。鏈條上的每個處理器各自承擔各自的處理職責,所以叫作責任鏈模式。
- 模式 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
管理的模式。
- 源碼解析
在日常框架和語言基礎庫中,經常能夠看到很多場景使用了責任鏈模式。
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
來處理請求。
- 總結
責任鏈模式的基本思想就是要處理的請求 (通常會是結構體,然後作爲函數參數);依次經過多個處理對象處理,這些處理函數可以動態的添加和刪除,具備很高的靈活性和擴展性,通常會對這些處理函數做統一處理,存儲方式一般是通過鏈表、數組、hash map 等存儲結構。
責任鏈模式的應用非常廣泛:
-
業務場景:作爲敏感詞(涉黃、政治、反動等此)過濾的設計結構
-
技術框架:路由、router 過濾器、日誌 log 框架等等
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/SjSWc1je2L9lr0OgGUXrGA