Go 語言設計模式之裝飾模式

裝飾模式算是一種很好理解的設計模式了,相信接觸過設計模式的同學都會對這種模式印象深刻,但是可能你不知道,裝飾模式在業界的應用也非常的廣泛,遠超你的想象,讓我們來一起看看吧。

1. 模式介紹

裝飾模式(Decorator Pattern)它的定義是這樣的:Attach additional responsibilities to an object dynamically keeping the same interface. Decorators provide a flexible alternative to subclassing for extending functionality。

動態地給一個對象添加一些額外的職責。就增加功能來說,裝飾模式相比生成子類更爲靈活。

相較與繼承,對象必須實現或者複用父類的函數或方法,裝飾模式使用組合的方式代替繼承,在不影響其他對象的情況下,動態地給一個對象添加一些額外的職責,更輕量的實現給原始類和對象增強功能的效果,在解決繼承關係過於複雜、需要動態添加或者撤銷職責的場景有很好的效果。

裝飾模式其實從字面上更好理解:一個對象,我想要給他什麼功能,就裝飾什麼功能,就像一間空屋子,用廚具的裝飾方法來裝飾,就會有廚房的功能,用書房的裝飾方法來裝飾,就會有書房的功能,以此類推,還有臥室、浴室等等。

主要解決繼承關係過於複雜的場景。組合優於繼承,可以 “使用組合來替代繼承”

2. 模式 demo

2.1 UML

裝飾模式(Decorator Pattern)的整體結構如下:

從 UML 圖中,我們可以看到,裝飾模式主要包含兩個角色:普通對象(Component)和裝飾器(Decorator),裝飾器和普通對象是聚合的關係,也就是說:普通對象和裝飾器是部分與整體的關係,普通對象是裝飾器的組成部分。

2.2 標準 demo

我們依據標準的 UML 圖,寫出一個具體的例子(對應 UML 圖):

(請大圖觀看)

首先定義Componet(對照上圖) 接口: House

type House interface {
   Live()
}

然後定義Componet接口的實現類ConcreteComponet(對照上圖):DxmHouse

type DxmHouse struct{}

func (d *DxmHouse) Live() {
   fmt.Println("dxmer are working")
}

然後定義包裝類接口Decorator(對照上圖):

type Decorator struct {
   h House
}

func (d *Decorator) SetHose(house House) {
   d.h = house
}

之後分別定義包裝類接口的兩個實現類: KitchenDecorator

type KitchenDecorator struct {
   Decorator
}

func (k *KitchenDecorator) Live() {
   fmt.Println("---------廚房包裝開始--------")
   k.h.Live()
   fmt.Println("---------廚房包裝結束--------")
}

以及BedroomDecorator

type BedroomDecorator struct {
   Decorator
}

func (b *BedroomDecorator) Live() {
   fmt.Println("---------臥室包裝開始---------")
   b.h.Live()
   fmt.Println("---------臥室包裝結束---------")
}

運行調用函數:

func main() {
   dxm := &DxmHouse{}

   k := &KitchenDecorator{}
   k.SetHose(dxm)
   b := &BedroomDecorator{}
   b.SetHose(k)
   b.Live()
}

運行結果:

---------臥室包裝開始---------
---------廚房包裝開始--------
dxmer are working
---------廚房包裝結束--------
---------臥室包裝結束---------

3. 源碼解析

在 Go 的語言基礎庫中,經常能夠看到很多場景使用了裝飾模式。

3.1 GO 語言 IO 庫中的使用

(請大圖觀看)

Go 中 io 包中的很多地方用了裝飾模式,這裏以bufio.Reader爲例子,首先定義一個被包裝類的接口io.Reader(請對照 2.1UML 圖中的Component

type Reader interface {
   Read(p []byte) (n int, err error)
}

然後定義io.Reader的實現類os.File(請對照 2.1UML 圖中的ConcreteComponet

// File represents an open file descriptor.
type File struct {
   *file // os specific
}
...

func (f *File) Read(b []byte) (n int, err error) {
   if err := f.checkValid("read"); err != nil {
      return 0, err
   }
   n, e := f.read(b)
   return n, f.wrapErr("read", e)
}

之後定義io.Reader的實現類bufio.Reader(請對照 2.1UML 圖中的DecoratorConcreteDecorator1ConcreteDecorator2

// Buffered input.

// Reader implements buffering for an io.Reader object.
type Reader struct {
   buf          []byte
   rd           io.Reader // reader provided by the client
   r, w         int       // buf read and write positions
   err          error
   lastByte     int // last byte read for UnreadByte; -1 means invalid
   lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}

// NewReader returns a new Reader whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
   return NewReaderSize(rd, defaultBufSize)
}


...

// Read reads data into p.
// It returns the number of bytes read into p.
// The bytes are taken from at most one Read on the underlying Reader,
// hence n may be less than len(p).
// To read exactly len(p) bytes, use io.ReadFull(b, p).
// At EOF, the count will be zero and err will be io.EOF.
func (b *Reader) Read(p []byte) (n int, err error) {
   n = len(p)
   if n == 0 {
      if b.Buffered() > 0 {
         return 0, nil
      }
      return 0, b.readErr()
   }
   if b.r == b.w {
      if b.err != nil {
         return 0, b.readErr()
      }
      if len(p) >= len(b.buf) {
         // Large read, empty buffer.
         // Read directly into p to avoid copy.
         n, b.err = b.rd.Read(p)
         if n < 0 {
            panic(errNegativeRead)
         }
         if n > 0 {
            b.lastB
yte = int(p[n-1])
            b.lastRuneSize = -1
         }
         return n, b.readErr()
      }
      // One read.
      // Do not use b.fill, which will loop.
      b.r = 0
      b.w = 0
      n, b.err = b.rd.Read(b.buf)
      if n < 0 {
         panic(errNegativeRead)
      }
      if n == 0 {
         return 0, b.readErr()
      }
      b.w += n
   }


   // copy as much as we can
   // Note: if the slice panics here, it is probably because
   // the underlying reader returned a bad count. See issue 49795.
   n = copy(p, b.buf[b.r:b.w])
   b.r += n
   b.lastByte = int(b.buf[b.r-1])
   b.lastRuneSize = -1
   return n, nil

}

函數有點長,可以無視,只關注結構體bufio.Reader實現了ReadNewReader即可。

最後進行調用:

func main() {
   f, err := os.Open("tmp")
   if err != nil {
      fmt.Println(err.Error())
      return
   }

   reader := bufio.NewReader(f)
   for {
      msg, r := reader.ReadString('\n')
      if r != io.EOF && r != nil {
         fmt.Println(err.Error())
         return
      }
      fmt.Println(msg)
      if r == io.EOF {
         break
      }

   }

}

可見bufio.Reader實現了標準的裝飾模式,以此類推,bufio.Writer也是同樣的。

其實不僅僅是 Go 語言,其他語言的 IO 標準庫也大量的使用了裝飾模式。

3.2 Go 基礎庫 Context

(請大圖觀看)

首先回顧一下 Context 的用途:Context 是一種用於跨多個 Goroutine 傳遞請求,協同工作的機制。正如它的名字一樣,就是協程之間的上下文。接下來看看它的實現機制,首先定義一個Context接口:(請對照 2.1UML 圖中的Component

// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
   
   Deadline() (deadline time.Time, ok bool)

   //
   Done() <-chan struct{}

   //
   Err() error

   //
   Value(key any) any
}

然後又定義了emptyCtx結構體並實現了Context(請對照 2.1UML 圖中的ConcreteComponet

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
   return
}

func (*emptyCtx) Done() <-chan struct{} {
   return nil
}

func (*emptyCtx) Err() error {
   return nil
}

func (*emptyCtx) Value(key any) any {
   return nil
}

這是一個私有的結構體,實體主要用於backgroundtodo兩個變量,這也是context.Background() 和 context.TODO()函數的返回值。如下代碼:

var (
   background = new(emptyCtx)
   todo       = new(emptyCtx)
)

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
   return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
   return todo
}

不同於標準的裝飾類 UML 圖,沒有像 Go 語言的 IO 庫一樣那麼標準,Context沒有實現 2.1UML 圖中的Decorator和實現類ConcreteDecorator1ConcreteDecorator2,而是用各種函數替代:WithValue(...)WithTimeout(...)WithCancel(...)WithDeadline(...)

以上這幾個函數會返回三個私有的結構體:cancelCtxvalueCtxtimerCtx, 三個結構體都實現了Context接口,並且timerCtx繼承與cancelCtx

具體的結構請參照 3.2 開頭的結構圖。

valueCtx

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
   Context
   key, val any
}

cancelCtx

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
   Context

   mu       sync.Mutex            // protects following fields
   done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
   children map[canceler]struct{} // set to nil by the first cancel call
   err      error                 // set to non-nil by the first cancel call
}

timerCtx

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
   cancelCtx
   timer *time.Timer // Under cancelCtx.mu.

   deadline time.Time
}

4 總結

裝飾模式的基本思想是通過組合和委託的方式,在不改變對象自身的情況下來動態增強對象的功能,通過裝飾模式,可以將對象的功能分爲不同的層級,每個層級的對象可以按照一定的順序動態地組合進來,形成一個具有多種功能的對象。裝飾模式的設計方式可以讓項目能夠更加靈活的組合對象,從而實現複雜的功能。

裝飾模式的應用場景非常廣泛,除了各類語言的語言 IO 基礎庫及 Go 的 context 之外,我們常用的 Web 框架中的 router 過濾器,也常常使用裝飾模式去實現(還可能會用責任鏈實現,請參考 Go 設計模式之責任鏈模式)。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/4Kgi3BibTrTiOgKrJMUZRw