摘要:裝飾者模式通過組合的方式,提供了能夠動态地給對象/子產品擴充新功能的能力。理論上,隻要沒有限制,它可以一直把功能疊加下去,具有很高的靈活性。
本文分享自華為雲社群《【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 體系具有很高的可擴充性和靈活性。
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等功能。
- ......
優缺點
優點
- 遵循開閉原則,能夠在不修改老代碼的情況下擴充新功能。
- 可以用多個裝飾器把多個功能組合起來,理論上可以無限組合。
缺點
- 一定要注意裝飾器裝飾的順序,否則容易出現不在預期内的行為。
- 當裝飾器越來越多之後,系統也會變得複雜。
與其他模式的關聯
裝飾者模式和代理模式具有很高的相似性,但是兩種所強調的點不一樣。前者強調的是為本體對象添加新的功能;後者強調的是對本體對象的通路控制。
裝飾者模式和擴充卡模式的差別是,前者隻會擴充功能而不會修改接口;後者則會修改接口。
點選關注,第一時間了解華為雲新鮮技術~