深圳幻海软件技术有限公司 欢迎您!

Go学设计模式--装饰器和职责链,哪个模式实现中间件更科学?

2023-02-28

大家好,我是每周在这里陪你进步的网管~,本次我们继续填坑,说一下装饰器模式。上篇文章我们说过装饰器是代理模式的特殊应用,而且很多人说中间件是用装饰器模式实现的,有的人说是用职责链实现的,那么这篇文章我们就来一起看看他们的异同。什么是装饰器装饰器模式(DecoratorPattern)也叫作包装器模式

大家好,我是每周在这里陪你进步的网管~,本次我们继续填坑,说一下装饰器模式。

上篇文章我们说过装饰器是代理模式的特殊应用,而且很多人说中间件是用装饰器模式实现的,有的人说是用职责链实现的,那么这篇文章我们就来一起看看他们的异同。

什么是装饰器

装饰器模式(Decorator Pattern)也叫作包装器模式(Wrapper Pattern),指在不改变原有对象的基础上,动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活,属于结构型设计模式。

给对象添加新行为最简单直观的办法就是扩展本体对象,通过继承的方式达到目的。但是使用继承不可避免地有如下两个弊端:

继承是静态的,在编译期间就已经确定,无法在运行时改变对象的行为。

子类只能有一个父类,当需要添加的新功能太多时,容易导致类的数量剧增。

而使用装饰器模式,我们通过将现有对象放置在实现了相同一套接口的包装器对象中来动态地向现有对象添加新行为。在包装器中进行我们代码的扩展,有助于重用功能并且不会修改现有对象的代码,符合“开闭原则”。

这里被放置在包装对象的“现有对象”通常会被叫做“组件”(Component),而包装组件的包装器对象就是我们常说的“装饰器”(Decorator),因为装饰器会组件实现相同接口,故客户端无法识别两者的差异,也就不需要在增加装饰器时对客户端调用代码进行修改了。

从上面关于装饰器模式的描述中 ,会感觉他跟代理模式很像。这是因为他们本来在结构上也几乎一样,装饰器算是代理的一个特殊应用--装饰器模式的一个特点是可以嵌套多层装饰器,相当于给代理再加代理。不过代理强调的是对本体对象的访问控制,而装饰器是用来对本地进行增强,两者在使用目的上不一样。

上面装饰器模式的用处特点用文字描述了这么多,下面我们用 UML 类图展示一下它的结构,让我们在写代码前对模式中的各个角色有个更清晰的认识。

装饰器的结构

用 UML 类图表示装饰器模式的结构如下:

从图中可以看到装饰器模式中主要有如下几个角色:

  • 客户端:会用多层装饰器来封装组件, 最后调用装饰好的包装器的方法,启动执行。
  • 组件接口:Component声明装饰器对象和被装饰的组件对象要实现的公用接口。
  • 组件实现:具体的组件实现类它的Operation方法中定义了组件的基础行为, 装饰类可以增强这些行为。
  • 基础装饰类:拥有一个指向被封装对象的成员变量。 在自己的Operation​方法中调用被装饰对象的Operation方法
  • 具体装饰类:重写父类的Operation​方法实现增强逻辑。类图里已经给出了要实现的主要逻辑,第四步的基础装饰类并不需要一定存在,完全可以由具体装饰类来持有对被装饰对象的引用,并实现增强逻辑,这样一来整体的结构会更简单一些。

注意:图中的方法名在代码实现里可自己定义,不需要完全跟图里给出的方法名一样。

我们可以跟上节代理模式的UML类图做个对比,两者在结构上非常相似,尤其是省略了BaseDecorator这一层后,在结构上基本上是一摸一样,这样我们一直再强调的--"装饰器是代理模式的特殊应用"的一个论据。

下面我们看一下实现装饰器模式的代码模版,本文中提供了Go语言实现一个简单装饰器模式的代码模版。

装饰器模式代码实现

清楚了装饰器模式结构的组成后,再来写代码就会清晰很多,接下来我们演示一下用装饰器模式实现增强游戏主机的一个例子。

首先我们定义一个游戏主机的产品接口,它就是上面类图中组件和装饰器的公共接口。

// PS5 产品接口
type PS5 interface {
 StartGPUEngine()
 GetPrice() int64
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

然后我们提供一个基础的产品实现类作为装饰器模式中的组件。

// CD 版 PS5主机
"本文使用的完整可运行源码
去公众号「网管叨bi叨」发送【设计模式】即可领取"
type PS5WithCD struct{}

func (p PS5WithCD) StartGPUEngine() {
 fmt.Println("start engine")
}
func (p PS5WithCD) GetPrice() int64 {
 return 5000
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

这里给出的是一个 CD 版的游戏主机,平时玩游戏的同学都会知道,一般还会有数字版的主机,价格会便宜点,这种情况我们可以提供一个数字版游戏主机的实现作为组件实现类。

// PS5 数字版主机
type PS5WithDigital struct{}

func (p PS5WithDigital) StartGPUEngine() {
 fmt.Println("start normal gpu engine")
}

func (p PS5WithDigital) GetPrice() int64 {
 return 3600
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

那么除了这两种基础的产品类型,厂商一般还会开发各种主题限定配色的主机、增加了硬件配置的主机等等,这两种在价格上肯定会跟基础版有些不一样,针对这种层面的扩展我们可以使用装饰器来实现,避免对基础组件类的更改。

下面是用两个装饰器实现的Plus版和主题配色版的两个增强。

"本文使用的完整可运行源码
去公众号「网管叨bi叨」发送【设计模式】即可领取"
// Plus 版的装饰器
func (p *PS5MachinePlus) SetPS5Machine(ps5 PS5) {
 p.ps5Machine = ps5
}

func (p PS5MachinePlus) StartGPUEngine() {
 p.ps5Machine.StartGPUEngine()
 fmt.Println("start plus plugin")
}

func (p PS5MachinePlus) GetPrice() int64 {
 return p.ps5Machine.GetPrice() + 500
}

// 主题色版的装饰器
type PS5WithTopicColor struct {
 ps5Machine PS5
}

func (p *PS5WithTopicColor) SetPS5Machine(ps5 PS5) {
 p.ps5Machine = ps5
}

func (p PS5WithTopicColor) StartGPUEngine() {
 p.ps5Machine.StartGPUEngine()
 fmt.Println("尊贵的主题色主机,GPU启动")
}
func (p PS5WithTopicColor) GetPrice() int64 {
 return p.ps5Machine.GetPrice() + 200
}
  • 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.

根据装饰器模式的特点,两个增强还可以叠加在一起,组合出即高配主题限定版主机...... 呃,是不是有点某游戏大厂每年发新机时给你的感觉了,就是不出第二代,每年给你多发几个限定配色、升级下屏幕,说的就是你 XXX(各位自己评论里脑补一下)

好了,在客户端我们把装饰器和组件组合起来就能获得一款高配主题限定版主机......

"本文使用的完整可运行源码
去公众号「网管叨bi叨」发送【设计模式】即可领取"
func main() {
 ps5MachinePlus := PS5MachinePlus{}
 ps5MachinePlus.SetPS5Machine(PS5WithCD{})
 // ps5MachinePlus.SetPS5Machine(PS5WithDigital{}) // 可以在更换主机
 ps5MachinePlus.StartGPUEngine()
 price := ps5MachinePlus.GetPrice()
 fmt.Printf("PS5 CD 豪华Plus版,价格: %d 元\n\n", price)

 ps5WithTopicColor := PS5WithTopicColor{}
 ps5WithTopicColor.SetPS5Machine(ps5MachinePlus)
 ps5WithTopicColor.StartGPUEngine()
 price = ps5WithTopicColor.GetPrice()
 fmt.Printf("PS5 CD 豪华Plus 经典主题配色版,价格: %d 元\n", price)
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

本文的完整源码,已经同步收录到我整理的电子教程里啦,可向我的公众号「网管叨bi叨」发送关键字【设计模式】领取。

装饰器和几个模式的区别

装饰器和代理在结构上类似,在行为上跟职责链模式类似,现在我们总结一下他们之间的区别

装饰器模式 VS 代理模式

  • 装饰器模式就是代理模式的一个特殊应用。
  • 装饰器模式强调自身功能的扩展。
  • 代理模式强调对代理过程的控制。

装饰器 VS 职责链模式

装饰器和职责链在行为上看都是多个单元进行组合完成逻辑处理,但是装饰器注重给某样东西添加扩展,最终会得到一个产品。而职责链更强调分步骤完成某个流程,更像是一个任务链表,而且与装饰器模式不同的是,职责链可以随时终止。

举个例子来说,针对OA系统请假审批这个场景,假设员工请假需要得到组长、总监和经理的批准才行。在这种情况下,使用装饰器模式实现的话无论您的请假在前面的环节被批准还是被拒绝,整个链条都不会中断,最终我们会得到三个级别审批人对申请的全部反馈。

而使用职责链模式的话,在每个阶段,每个审批人都有权批准或拒绝。如果请求在任何级别被拒绝,那么整个流程就会结束,请求不会继续流转到下一个级别的审批人那里。

所以看到这里,你觉得像Web框架的中间件这种东西应该拿职责链还是装饰器实现呢?

总结

装饰器模式有不少优点,它是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态地给一个对象扩展功能,即插即用。通过使用不同装饰类及这些装饰类的排列组合,可以实现不同效果,完全遵循程序设计的“开闭原则”。

但装饰器的使用必将会给程序带来更高的复杂性,更低的可读性,子类集成的代码结构会更直白易懂一些,而且虽然装饰器符合“开闭原则”,但是它会给程序带来更多的类,动态装饰在多层装饰时会更复杂。

所以总体上使用装饰器模式的时候也是两害相较取其轻,为了不频繁修改已经成型的子类而引入更多装饰器类。

应用的时候一定要谨记装饰器是“增强”某个事物用的,可千万别把事物本身实现的主逻辑用装饰器实现了。