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 圖中的Decorator
和ConcreteDecorator1
和ConcreteDecorator2
)
// 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
實現了Read
和NewReader
即可。
最後進行調用:
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
}
這是一個私有的結構體,實體主要用於background
和todo
兩個變量,這也是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
和實現類ConcreteDecorator1
、ConcreteDecorator2
,而是用各種函數替代:WithValue(...)
、WithTimeout(...)
、WithCancel(...)
、WithDeadline(...)
。
以上這幾個函數會返回三個私有的結構體:cancelCtx
、valueCtx
、timerCtx
, 三個結構體都實現了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