天天看點

實踐GoF的23種設計模式:裝飾者模式

摘要:裝飾者模式通過組合的方式,提供了能夠動态地給對象/子產品擴充新功能的能力。理論上,隻要沒有限制,它可以一直把功能疊加下去,具有很高的靈活性。

本文分享自華為雲社群《【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 結構

實踐GoF的23種設計模式:裝飾者模式

場景上下文

在簡單的分布式應用系統(示例代碼工程)中,我們設計了 Sidecar 邊車子產品,它的用處主要是為了 1)友善擴充 

network.Socket

 的功能,如增加日志、流控等非業務功能;2)讓這些附加功能對業務程式隐藏起來,也即業務程式隻須關心看到 

network.Socket

 接口即可。

實踐GoF的23種設計模式:裝飾者模式

代碼實作

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)
 }      

總結實作裝飾者模式的幾個關鍵點:

  1. 定義需要被裝飾的抽象接口,後續的裝飾器都是基于該接口進行擴充。
  2. 為抽象接口提供一個基礎實作。
  3. 定義裝飾器,并實作被裝飾的抽象接口。
  4. 裝飾器持有被裝飾的抽象接口作為成員屬性。“裝飾”的意思是在原有功能的基礎上擴充新功能,是以必須持有原有功能的抽象接口。
  5. 在裝飾器中,對于需要擴充功能的方法,新增擴充功能。
  6. 不需要擴充功能的方法,直接調用被裝飾接口的原生方法即可。
  7. 為裝飾器定義一個工廠方法,入參為被裝飾接口。
  8. 使用時,通過裝飾器的工廠方法,把所有裝飾器和被裝飾者串聯起來。

擴充

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 的函數式程式設計的特點,總結下來有如下幾個關鍵點:

  1. 确定被裝飾的接口,上述例子為 

    http.HandlerFunc

  2. 定義裝飾器類型,是一個函數類型,入參和傳回值都是被裝飾接口,上述例子為 

    func(http.HandlerFunc) http.HandlerFunc

  3. 定義裝飾函數,入參為被裝飾的接口和裝飾器可變清單,上述例子為 

    Decorate

     方法。
  4. 在裝飾方法中,通過for循環周遊裝飾器,完成對被裝飾接口的裝飾。這裡是用來類似 Functional Options 的技巧,一定要注意裝飾器的順序!
  5. 實作具體的裝飾器,上述例子為 

    WithBasicAuth

     和 

    WithLogger

     函數。
  6. 在裝飾器中,完成功能擴充之後,記得調用被裝飾者的接口,這樣才能将所有裝飾器和被裝飾者串起來。
  7. 在使用時,通過裝飾函數完成對被裝飾者的裝飾,上述例子為 

    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}
 }      
實踐GoF的23種設計模式:裝飾者模式

使用時,可以這樣:

// 使用時,可以這樣
 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等功能。
  • ......

優缺點

優點

  1. 遵循開閉原則,能夠在不修改老代碼的情況下擴充新功能。
  2. 可以用多個裝飾器把多個功能組合起來,理論上可以無限組合。

缺點

  1. 一定要注意裝飾器裝飾的順序,否則容易出現不在預期内的行為。
  2. 當裝飾器越來越多之後,系統也會變得複雜。

與其他模式的關聯

裝飾者模式和代理模式具有很高的相似性,但是兩種所強調的點不一樣。前者強調的是為本體對象添加新的功能;後者強調的是對本體對象的通路控制。

裝飾者模式和擴充卡模式的差別是,前者隻會擴充功能而不會修改接口;後者則會修改接口。

點選關注,第一時間了解華為雲新鮮技術~

繼續閱讀