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

go 复制代码
type House interface {
   Live()
}

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

go 复制代码
type DxmHouse struct{}

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

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

go 复制代码
type Decorator struct {
   h House
}

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

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

erlang 复制代码
type KitchenDecorator struct {
   Decorator
}

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

以及BedroomDecorator

erlang 复制代码
type BedroomDecorator struct {
   Decorator
}

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

运行调用函数:

css 复制代码
func main() {
   dxm := &DxmHouse{}

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

   b.Live()
}

运行结果:

sql 复制代码
---------卧室包装开始---------
---------厨房包装开始--------
dxmer are working
---------厨房包装结束--------
---------卧室包装结束---------

3. 源码解析

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

3.1 GO语言IO库中的使用

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

go 复制代码
// 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

go 复制代码
// 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

scss 复制代码
// 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即可。

最后进行调用:

go 复制代码
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

scss 复制代码
// 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

go 复制代码
// 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()函数的返回值。如下代码:

scss 复制代码
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

arduino 复制代码
// 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

go 复制代码
// 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

less 复制代码
// 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语言设计模式之责任链模式 的文章)。

相关推荐
假装我不帅1 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
神仙别闹1 小时前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
计算机-秋大田2 小时前
基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
货拉拉技术2 小时前
货拉拉-实时对账系统(算盘平台)
后端
掘金酱3 小时前
✍【瓜分额外奖金】11月金石计划附加挑战赛-活动命题发布
人工智能·后端
代码之光_19803 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi3 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
架构师那点事儿4 小时前
golang 用unsafe 无所畏惧,但使用不得到会panic
架构·go·掘金技术征文
颜淡慕潇4 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决