装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承,给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这样的需求,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。
装饰器类一般有两种写法,一种是洋葱式的,一种是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… ,当时说是选项模式,网上说其实这也算是一种装饰器模式,应该是流水线形式的装饰器,没有嵌套的逻辑,就是选择性的给一些变量赋值
适用的场景有上面说的选择性配置,中间件,记住一个重点吧,是用来给原始类添加功能的