装饰器模式

98 阅读5分钟

装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承,给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这样的需求,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。

装饰器类一般有两种写法,一种是洋葱式的,一种是pipeline流水线式的

洋葱式的golang实现如下

// 接口,原始类和装饰类都继承于此
type IUser interface {
   Do()
}

// 原始类
type User struct {
}

func (d *User) Do() {
   //核心业务逻辑
   fmt.Println("user 原始逻辑")
}

// 装饰类A
type DecorateA struct {
   base IUser
}

func (d *DecorateA) Do() {
   //装饰类A的附加功能
   fmt.Println("DecorateA")
   if d.base != nil {
      d.base.Do()
   }
}
func WithDecorateA(iuser IUser) IUser {
   a := new(DecorateA)
   a.base = iuser
   return a
}

// 装饰类B
type DecorateB struct {
   base IUser
}

func (d *DecorateB) Do() {
   //装饰类B的附加功能
   fmt.Println("DecorateB")
   if d.base != nil {
      d.base.Do()
   }
}

func WithDecorateB(iuser IUser) IUser {
   b := new(DecorateB)
   b.base = iuser
   return b
}

// 装饰类C
type DecorateC struct {
   base IUser
}

func (d *DecorateC) Do() {
   //装饰类C的附加逻辑
   fmt.Println("DecorateC")
   if d.base != nil {
      d.base.Do()
   }
}
func WithDecorateC(iuser IUser) IUser {
   c := new(DecorateC)
   c.base = iuser
   return c
}

func TestDecorate() {
   user1 := WithDecorateA(WithDecorateB(WithDecorateC(new(User))))
   user1.Do()

   user2 := WithDecorateC(WithDecorateB(WithDecorateA(new(User))))
   user2.Do()
}

拆解一下上面的代码块,主要包括几个部分

  • 接口,原始类和装饰器类都继承这个接口,接口里面有一个方法,这个方法对于原始类来说就是原始业务逻辑实现,对于装饰器类来说就是每一个装饰器的附加功能
  • 原始类,原始类除了其他业务逻辑,要实现一个和接口一样的方法,里面写核心原始功能
  • 装饰器类,每一个装饰器类完成一个特定的附加功能,(1)装饰器类要有一个小写的成员变量,类型是接口,这个成员变量是装饰器类和原始类连接起来的关键,每一个类的方法里面都会调用这个变量对应的方法,实现套娃(2)装饰器类实现接口的方法里面除了自己的附加功能,还要调用成员变量的对应方法(3)每一个装饰器类为了优雅一点,封装创建装饰器的方法 WithDecorateX,这个方法里面创建装饰器,给装饰器的成员变量赋值,这样形式上看起来非常像洋葱的感觉,一层包一层,如果不封装,在使用的地方创建,代码很难看,就像下面这样,会有很多中间状态暴露
user := new(User)
a := new(DecorateA)
b := new(DecorateB)
c := new(DecorateC)
a.base = user
b.base = a
c.base = b
c.Do()

从上面TestDecorate可以看到,这种形式很像洋葱,输出如下

DecorateA
DecorateB
DecorateC
user 原始逻辑
DecorateC
DecorateB
DecorateA
user 原始逻辑

可以看到,外层的先执行,然后一层层往里执行,这种写法的缺点就是嵌套太深,如果有很多装饰器,代码看起来非常难看,也很长,阅读性差。所以下面介绍另一种pipeline流水线形式的

// 原始逻辑的方法
func Origin() {
   fmt.Println("原始逻辑")
}

// 原始方法类型 就是上面origin的函数形式,方便作为参数输入
type doFunc func()

// 装饰器  输入是未经该装饰器修饰的方法,输出是附加了该装饰器修饰的方法
func HandleDecorateA(do doFunc) doFunc {
   return func() {
      //装饰类A的附加功能
      fmt.Println("DecorateA")
      do()
   }
}
func HandleDecorateB(do doFunc) doFunc {
   return func() {
      //装饰类B的附加功能
      fmt.Println("DecorateB")
      do()
   }
}
func HandleDecorateC(do doFunc) doFunc {
   return func() {
      //装饰类C的附加功能
      fmt.Println("DecorateC")
      do()
   }
}

// 装饰器方法类型  输入是未加修饰的方法,输出是修饰之后的方法 定义这个类型,方便作为参数写入
type handleFunc func(doFunc) doFunc

// 流水线装饰原始方法 输入参数1是原始方法,后面是装饰器数组,返回是经过所有装饰器装饰的方法
func Handle(origin doFunc, handles ...handleFunc) doFunc {
   for i := 0; i < len(handles); i++ {
      origin = handles[i](origin) //这里就是消除洋葱形式的关键,其实也是将数组里面的方法拿出来嵌套,但是封装起来屏蔽用户了,用户无感知,用户输入的是流水线形式的数组
   }
   return origin
}



func TestDecoratePipeline() {
   h := Handle(Origin, HandleDecorateA, HandleDecorateB, HandleDecorateC)
   h()
}

拆解上面的代码,有几个部分

  • 原始方法,里面是核心业务逻辑
  • 装饰器方法,输入输出都是原始方法类型,内部逻辑实现附加功能
  • 集中装饰原始功能的方法handle,输入有原始方法,装饰器方法数组,内部逻辑是对原始方法装饰所有装饰器
  • 原始方法,装饰器方法的类型别名,主要是为了方便作为参数输入,不是必须

上面TestDecoratePipeline的输出如下

DecorateC
DecorateB
DecorateA
原始逻辑

可以看出流水线是从后往前执行

后记:

pipeline格式的不是面向对象的实现,网上也没有类似的例子,http实现的类似功能也不是面向对象的

适用场景

1.其实之前整理过一篇文章juejin.cn/post/716577… ,当时说是选项模式,网上说其实这也算是一种装饰器模式,应该是流水线形式的装饰器,没有嵌套的逻辑,就是选择性的给一些变量赋值

适用的场景有上面说的选择性配置,中间件,记住一个重点吧,是用来给原始类添加功能的