Go语言设计模式之装饰模式

2023-10-21

装饰模式算是一种很好理解的设计模式了,相信接触过设计模式的同学都会对这种模式印象深刻,但是可能你不知道,装饰模式在业界的应用也非常的广泛,甚至远超你的想象,让我们来一起看看吧。

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)的整体结构如下:

image.png

从UML图中,我们可以看到,装饰模式主要包含两个角色: 普通对象(Component)和装饰器(Decorator),装饰器和普通对象是聚合的关系,也就是说:普通对象和装饰器是部分与整体的关系,普通对象是装饰器的组成部分。

2.2 标准demo

我们依据标准的UML图,写出一个具体的例子(对应UML图):

image.png
(请放大图片观看)

首先定义Componet(对照上图)接口: House

1
2
3
type House interface {
Live()
}

然后定义Componet接口的实现类ConcreteComponet(对照上图):DxmHouse

1
2
3
4
5
type DxmHouse struct{}

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

然后定义包装类接口Decorator(对照上图):

1
2
3
4
5
6
7
type Decorator struct {
h House
}

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

之后分别定义包装类接口的两个实现类:
KitchenDecorator

1
2
3
4
5
6
7
8
9
type KitchenDecorator struct {
Decorator
}

func (k *KitchenDecorator) Live() {
fmt.Println("---------厨房包装开始--------")
k.h.Live()
fmt.Println("---------厨房包装结束--------")
}

以及BedroomDecorator

1
2
3
4
5
6
7
8
9
type BedroomDecorator struct {
Decorator
}

func (b *BedroomDecorator) Live() {
fmt.Println("---------卧室包装开始---------")
b.h.Live()
fmt.Println("---------卧室包装结束---------")
}

运行调用函数:

1
2
3
4
5
6
7
8
9
10
func main() {
dxm := &DxmHouse{}

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

b.Live()
}

运行结果:

1
2
3
4
5
---------卧室包装开始---------
---------厨房包装开始--------
dxmer are working
---------厨房包装结束--------
---------卧室包装结束---------

3. 源码解析

在Go的语言基础库中,经常能够看到很多场景使用了装饰模式。

3.1 GO语言IO库中的使用

go_demo.drawio.png

Go中io包中的很多地方用了装饰模式,这里以bufio.Reader为例子,首先定义一个被包装类的接口io.Reader(请对照2.1UML图中的Component

1
2
3
4
5
6
7
8
9
10
11
12
// that happen after reading some bytes and also both of the
// allowed EOF behaviors.
//
// Implementations of Read are discouraged from returning a
// zero byte count with a nil error, except when len(p) == 0.
// Callers should treat a return of 0 and nil as indicating that
// nothing happened; in particular it does not indicate EOF.
//
// Implementations must not retain p.
type Reader interface {
Read(p []byte) (n int, err error)
}

然后定义io.Reader的实现类os.File(请对照2.1UML图中的ConcreteComponet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// File represents an open file descriptor.
type File struct {
*file // os specific
}
...

// Read reads up to len(b) bytes from the File and stores them in b.
// It returns the number of bytes read and any error encountered.
// At end of file, Read returns 0, io.EOF.
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 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.lastByte = 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即可。

最后进行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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.drawio.png
(请放大图观看)

首先回顾一下Context的用途:Context是一种用于跨多个Goroutine传递请求,协同工作的机制。正如它的名字一样,就是协程之间的上下文。
接下来看看它的实现机制,首先定义一个Context接口:(请对照2.1UML图中的Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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()函数的返回值。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

1
2
3
4
5
6
// 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

1
2
3
4
5
6
7
8
9
10
// 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

1
2
3
4
5
6
7
8
9
// 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语言设计模式之责任链模式 的文章)。

请关注我

更多精彩内容,请搜索我的微信公众号 码农RyuGou

或者扫码