摘要:裝飾者模式通過組合的方式,提供了能夠動態地給物件/模組擴充套件新功能的能力。理論上,只要沒有限制,它可以一直把功能疊加下去,具有很高的靈活性。
本文分享自華為雲社群《【Go實現】實踐GoF的23種設計模式:裝飾者模式》,作者: 元閏子。
我們經常會遇到「給現有物件/模組新增功能」的場景,比如 http router 的開發場景下,除了最基礎的路由功能之外,我們常常還會加上如紀錄檔、鑑權、流控等 middleware。如果你檢視框架的原始碼,就會發現 middleware 功能的實現用的就是裝飾者模式(Decorator Pattern)。
GoF 給裝飾者模式的定義如下:
Decorators provide a flexible alternative to subclassing for extending functionality. Attach additional responsibilities to an object dynamically.
簡單來說,裝飾者模式通過組合的方式,提供了能夠動態地給物件/模組擴充套件新功能的能力。理論上,只要沒有限制,它可以一直把功能疊加下去,具有很高的靈活性。
如果寫過 Java,那麼一定對 I/O Stream 體系不陌生,它是裝飾者模式的經典用法,使用者端程式可以動態地為原始的輸入輸出流新增功能,比如按字串輸入輸出,加入緩衝等,使得整個 I/O Stream 體系具有很高的可延伸性和靈活性。
在
Sidecar 的這個功能場景,很適合使用裝飾者模式來實現,程式碼如下:
// demo/network/socket.go package network // 關鍵點1: 定義被裝飾的抽象介面 // Socket 網路通訊Socket介面 type Socket interface { // Listen 在endpoint指向地址上起監聽 Listen(endpoint Endpoint) error // Close 關閉監聽 Close(endpoint Endpoint) // Send 傳送網路報文 Send(packet *Packet) error // Receive 接收網路報文 Receive(packet *Packet) // AddListener 增加網路報文監聽者 AddListener(listener SocketListener) } // 關鍵點2: 提供一個預設的基礎實現 type socketImpl struct { listener SocketListener } func DefaultSocket() *socketImpl { return &socketImpl{} } func (s *socketImpl) Listen(endpoint Endpoint) error { return Instance().Listen(endpoint, s) } ... // socketImpl的其他Socket實現方法 // demo/sidecar/flowctrl_sidecar.go package sidecar // 關鍵點3: 定義裝飾器,實現被裝飾的介面 // FlowCtrlSidecar HTTP接收端流控功能裝飾器,自動攔截Socket接收報文,實現流控功能 type FlowCtrlSidecar struct { // 關鍵點4: 裝飾器持有被裝飾的抽象介面作為成員屬性 socket network.Socket ctx *flowctrl.Context } // 關鍵點5: 對於需要擴充套件功能的方法,新增擴充套件功能 func (f *FlowCtrlSidecar) Receive(packet *network.Packet) { httpReq, ok := packet.Payload().(*http.Request) // 如果不是HTTP請求,則不做流控處理 if !ok { f.socket.Receive(packet) return } // 流控後返回429 Too Many Request響應 if !f.ctx.TryAccept() { httpResp := http.ResponseOfId(httpReq.ReqId()). AddStatusCode(http.StatusTooManyRequest). AddProblemDetails("enter flow ctrl state") f.socket.Send(network.NewPacket(packet.Dest(), packet.Src(), httpResp)) return } f.socket.Receive(packet) } // 關鍵點6: 不需要擴充套件功能的方法,直接呼叫被裝飾介面的原生方法即可 func (f *FlowCtrlSidecar) Close(endpoint network.Endpoint) { f.socket.Close(endpoint) } ... // FlowCtrlSidecar的其他方法 // 關鍵點7: 定義裝飾器的工廠方法,入參為被裝飾介面 func NewFlowCtrlSidecar(socket network.Socket) *FlowCtrlSidecar { return &FlowCtrlSidecar{ socket: socket, ctx: flowctrl.NewContext(), } } // demo/sidecar/all_in_one_sidecar_factory.go // 關鍵點8: 使用時,通過裝飾器的工廠方法,把所有裝飾器和被裝飾者串聯起來 func (a AllInOneFactory) Create() network.Socket { return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), a.producer) }
總結實現裝飾者模式的幾個關鍵點:
定義需要被裝飾的抽象介面,後續的裝飾器都是基於該介面進行擴充套件。
為抽象介面提供一個基礎實現。
定義裝飾器,並實現被裝飾的抽象介面。
裝飾器持有被裝飾的抽象介面作為成員屬性。「裝飾」的意思是在原有功能的基礎上擴充套件新功能,因此必須持有原有功能的抽象介面。
在裝飾器中,對於需要擴充套件功能的方法,新增擴充套件功能。
不需要擴充套件功能的方法,直接呼叫被裝飾介面的原生方法即可。
為裝飾器定義一個工廠方法,入參為被裝飾介面。
使用時,通過裝飾器的工廠方法,把所有裝飾器和被裝飾者串聯起來。
在 Sidecar 的場景上下文中,被裝飾的 Socket
是一個相對複雜的介面,裝飾器通過實現 Socket
介面來進行功能擴充套件,是典型的物件導向風格。
如果被裝飾者是一個簡單的介面/方法/函數,我們可以用更具 Go 風格的實現方式,考慮前文提到的 http router 場景。如果你使用原生的 net/http
進行 http router 開發,通常會這麼實現:
func main() { // 註冊/hello的router http.HandleFunc("/hello", hello) // 啟動http伺服器 http.ListenAndServe("localhost:8080", nil) } // 具體的請求處理邏輯,型別是 http.HandlerFunc func hello(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello, world")) }
其中,我們通過 http.HandleFunc
來註冊具體的 router, hello
是具體的請求處理方法。現在,我們想為該 http 伺服器增加紀錄檔、鑑權等通用功能,那麼可以把 func(w http.ResponseWriter, r *http.Request)
作為被裝飾的抽象介面,通過新增紀錄檔、鑑權等裝飾器完成功能擴充套件。
// demo/network/http/http_handle_func_decorator.go // 關鍵點1: 確定被裝飾介面,這裡為原生的http.HandlerFunc type HandlerFunc func(ResponseWriter, *Request) // 關鍵點2: 定義裝飾器型別,是一個函數型別,入參和返回值都是 http.HandlerFunc 函數 type HttpHandlerFuncDecorator func(http.HandlerFunc) http.HandlerFunc // 關鍵點3: 定義裝飾函數,入參為被裝飾的介面和裝飾器可變列表 func Decorate(h http.HandlerFunc, decorators ...HttpHandlerFuncDecorator) http.HandlerFunc { // 關鍵點4: 通過for迴圈遍歷裝飾器,完成對被裝飾介面的裝飾 for _, decorator := range decorators { h = decorator(h) } return h } // 關鍵點5: 實現具體的裝飾器 func WithBasicAuth(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("Auth") if err != nil || cookie.Value != "Pass" { w.WriteHeader(http.StatusForbidden) return } // 關鍵點6: 完成功能擴充套件之後,呼叫被裝飾的方法,才能將所有裝飾器和被裝飾者串起來 h(w, r) } } func WithLogger(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { log.Println(r.Form) log.Printf("path %s", r.URL.Path) h(w, r) } } func hello(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello, world")) } func main() { // 關鍵點7: 通過Decorate函數完成對hello的裝飾 http.HandleFunc("/hello", Decorate(hello, WithLogger, WithBasicAuth)) // 啟動http伺服器 http.ListenAndServe("localhost:8080", nil) }
上述的裝飾者模式的實現,用到了類似於
確定被裝飾的介面,上述例子為 http.HandlerFunc
。
定義裝飾器型別,是一個函數型別,入參和返回值都是被裝飾介面,上述例子為 func(http.HandlerFunc) http.HandlerFunc
。
定義裝飾函數,入參為被裝飾的介面和裝飾器可變列表,上述例子為 Decorate
方法。
在裝飾方法中,通過for迴圈遍歷裝飾器,完成對被裝飾介面的裝飾。這裡是用來類似
實現具體的裝飾器,上述例子為 WithBasicAuth
和 WithLogger
函數。
在裝飾器中,完成功能擴充套件之後,記得呼叫被裝飾者的介面,這樣才能將所有裝飾器和被裝飾者串起來。
在使用時,通過裝飾函數完成對被裝飾者的裝飾,上述例子為 Decorate(hello, WithLogger, WithBasicAuth)
。
在 Go 標準庫中,也有一個運用了裝飾者模式的模組,就是 context
,其中關鍵的介面如下:
package context // 被裝飾介面 type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any } // cancel裝飾器 type cancelCtx struct { Context // 被裝飾介面 mu sync.Mutex done atomic.Value children map[canceler]struct{}= err error } // cancel裝飾器的工廠方法 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { // ... c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } } // timer裝飾器 type timerCtx struct { cancelCtx // 被裝飾介面 timer *time.Timer deadline time.Time } // timer裝飾器的工廠方法 func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { // ... c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } // ... return c, func() { c.cancel(true, Canceled) } } // timer裝飾器的工廠方法 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } // value裝飾器 type valueCtx struct { Context // 被裝飾介面 key, val any } // value裝飾器的工廠方法 func WithValue(parent Context, key, val any) Context { if parent == nil { panic("cannot create context from nil parent") } // ... return &valueCtx{parent, key, val} }
使用時,可以這樣:
// 使用時,可以這樣 func main() { ctx := context.Background() ctx = context.WithValue(ctx, "key1", "value1") ctx, _ = context.WithTimeout(ctx, time.Duration(1)) ctx = context.WithValue(ctx, "key2", "value2") }
不管是 UML 結構,還是使用方法,context
模組都與傳統的裝飾者模式有一定出入,但也不妨礙 context
是裝飾者模式的典型運用。還是那句話,
I/O 流,比如為原始的 I/O 流增加緩衝、壓縮等功能。
Http Router,比如為基礎的 Http Router 能力增加紀錄檔、鑑權、Cookie等功能。
......
遵循
可以用多個裝飾器把多個功能組合起來,理論上可以無限組合。
一定要注意裝飾器裝飾的順序,否則容易出現不在預期內的行為。
當裝飾器越來越多之後,系統也會變得複雜。
裝飾者模式和代理模式具有很高的相似性,但是兩種所強調的點不一樣。前者強調的是為本體物件新增新的功能;後者強調的是對本體物件的存取控制。
裝飾者模式和介面卡模式的區別是,前者只會擴充套件功能而不會修改介面;後者則會修改介面。