【Go實現】實踐GoF的23種設計模式:裝飾者模式
簡單的分散式應用系統(示例程式碼工程):https://github.com/ruanrunxue/Practice-Design-Pattern--Go-Implementation
簡介
我們經常會遇到“給現有物件/模組新增功能”的場景,比如 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 體系具有很高的可擴充套件性和靈活性。
UML 結構
場景上下文
在簡單的分散式應用系統(示例程式碼工程)中,我們設計了 Sidecar 邊車模組,它的用處主要是為了 1)方便擴充套件 network.Socket
的功能,如增加日誌、流控等非業務功能;2)讓這些附加功能對業務程式隱藏起來,也即業務程式只須關心看到 network.Socket
介面即可。
程式碼實現
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)
}
總結實現裝飾者模式的幾個關鍵點:
-
定義需要被裝飾的抽象介面,後續的裝飾器都是基於該介面進行擴充套件。
-
為抽象介面提供一個基礎實現。
-
定義裝飾器,並實現被裝飾的抽象介面。
-
裝飾器持有被裝飾的抽象介面作為成員屬性。“裝飾”的意思是在原有功能的基礎上擴充套件新功能,因此必須持有原有功能的抽象介面。
-
在裝飾器中,對於需要擴充套件功能的方法,新增擴充套件功能。
-
不需要擴充套件功能的方法,直接呼叫被裝飾介面的原生方法即可。
-
為裝飾器定義一個工廠方法,入參為被裝飾介面。
-
使用時,通過裝飾器的工廠方法,把所有裝飾器和被裝飾者串聯起來。
擴充套件
Go 風格的實現
在 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)
}
上述的裝飾者模式的實現,用到了類似於 Functional Options 的技巧,也是巧妙利用了 Go 的函數語言程式設計的特點,總結下來有如下幾個關鍵點:
-
確定被裝飾的介面,上述例子為
http.HandlerFunc
。 -
定義裝飾器型別,是一個函式型別,入參和返回值都是被裝飾介面,上述例子為
func(http.HandlerFunc) http.HandlerFunc
。 -
定義裝飾函式,入參為被裝飾的介面和裝飾器可變列表,上述例子為
Decorate
方法。 -
在裝飾方法中,通過for迴圈遍歷裝飾器,完成對被裝飾介面的裝飾。這裡是用來類似 Functional Options 的技巧,一定要注意裝飾器的順序!
-
實現具體的裝飾器,上述例子為
WithBasicAuth
和WithLogger
函式。 -
在裝飾器中,完成功能擴充套件之後,記得呼叫被裝飾者的介面,這樣才能將所有裝飾器和被裝飾者串起來。
-
在使用時,通過裝飾函式完成對被裝飾者的裝飾,上述例子為
Decorate(hello, WithLogger, WithBasicAuth)
。
Go 標準庫中的裝飾者模式
在 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等功能。
-
......
優缺點
優點
-
遵循開閉原則,能夠在不修改老程式碼的情況下擴充套件新功能。
-
可以用多個裝飾器把多個功能組合起來,理論上可以無限組合。
缺點
-
一定要注意裝飾器裝飾的順序,否則容易出現不在預期內的行為。
-
當裝飾器越來越多之後,系統也會變得複雜。
與其他模式的關聯
裝飾者模式和代理模式具有很高的相似性,但是兩種所強調的點不一樣。前者強調的是為本體物件新增新的功能;後者強調的是對本體物件的訪問控制。
裝飾者模式和介面卡模式的區別是,前者只會擴充套件功能而不會修改介面;後者則會修改介面。
文章配圖
可以在 用Keynote畫出手繪風格的配圖 中找到文章的繪圖方法。
參考
[1] 【Go實現】實踐GoF的23種設計模式:SOLID原則, 元閏子
[2] 【Go實現】實踐GoF的23種設計模式:建造者模式, 元閏子
[3] Design Patterns, Chapter 4. Structural Patterns, GoF
[4] 裝飾模式, refactoringguru.cn
[5] Golang Decorator Pattern, Henry Du
更多文章請關注微信公眾號:元閏子的邀請