大家好,我是每周陪你进步的网管~这次继续填坑,说说装饰者模式。上一篇我们说了装饰器是代理模式的一种特殊应用,也有很多人说中间件是使用装饰器模式实现的,也有人说是使用责任链实现的,那么我们来看看在本文中介绍它们的异同。什么是装饰器?DecoratorPattern,又称为WrapperPattern,是指在不改变原有对象的情况下,动态地给一个对象增加一些额外的职责。在添加功能方面,装饰器模式比生成子类更加灵活,属于结构化设计模式。向对象添加新行为的最简单、最直观的方法是扩展本体对象并通过继承来实现目标。但是使用继承不可避免地有以下两个缺点:继承是静态的,在编译时就已经确定了,对象的行为在运行时是无法改变的。一个子类只能有一个父类。当需要添加的新功能过多时,很容易造成类的数量急剧增加。然而,使用装饰器模式,我们通过将现有对象放置在实现相同接口集的包装器对象中,动态地向现有对象添加新行为。在包装器中扩展我们的代码有助于重用功能,并且不会修改现有对象的代码,符合“开闭原则”。这里放置包装对象的“已有对象”通常称为“组件”,而包装组件的包装对象就是我们常说的“装饰器”,因为装饰器会实现相同的组件接口,所以客户端无法识别两者的区别,所以在添加装饰器时不需要修改客户端调用代码。从上面对装饰者模式的描述,你会觉得它和代理模式非常相似。这是因为它们在结构上几乎相同。装饰器是代理的一种特殊应用——装饰器模式的一个特点是可以嵌套多层装饰器,相当于给代理添加了代理。但是代理强调的是对本体对象的访问控制,而装饰器是用来增强局部的,两者的目的是不同的。上面装饰者模式的用处和特点,已经用文字描述了这么多。下面我们用UML类图来展示它的结构,让我们在写代码之前,对模式中的各个角色有一个更清晰的认识。装饰器的结构由UML类图表示。装饰器模式的结构如下:从图中我们可以看出,装饰器模式中主要有以下几个角色:客户端:会使用多层装饰器来封装组件,最后调用被装饰的一个包装器方法启动执行。组件接口:组件声明了装饰器对象和被装饰组件对象要实现的公共接口。组件实现:具体的组件实现类在其Operation方法中定义了组件的基本行为,装饰类可以增强这些行为。基本装饰类:有一个指向封装对象的成员变量。在自身的Operation方法中调用被装饰对象的Operation方法具体装饰类:重写父类的Operation方法,实现增强逻辑。类图中已经给出了要实现的主要逻辑。第四步的基本装饰类不需要存在。具体的装饰类完全可以持有对装饰对象的引用并实现增强逻辑。这样整体结构会更简单。注意:图中的方法名可以在代码实现中自己定义,不需要和图中给出的方法名完全一致。我们可以和上一节代理模式的UML类图做个对比。两者在结构上非常相似,尤其是在省略了BaseDecorator层之后,结构基本一致,所以我们一直在强调--“装饰器是代理模式的一种特殊应用”的说法。让我们看一下实现装饰器模式的代码模板。本文提供了一个用Go语言实现简单装饰器模式的代码模板。装饰器模式代码实现装饰器模式结构的组成明确之后,写代码就会清晰很多。接下来,我们将演示一个使用装饰器模式来增强游戏机的例子。首先我们定义一个游戏机的产品接口,也就是上面类图中组件和装饰器的公共接口。//PS5productinterfacetypePS5interface{StartGPUEngine()GetPrice()int64}然后我们提供一个基本的产品实现类作为装饰器模式中的一个组件。//CD版PS5主机《本文用到的完整可运行源码去公众号》“网络管理”发送【设计模式】接收"typePS5WithCDstruct{}func(pPS5WithCD)StartGPUEngine(){fmt.Println("startengine")}func(pPS5WithCD)GetPrice()int64{return5000}这里是CD版的游戏机,平时玩游戏的同学就会知道,一般都有数字版的主机,价格会便宜一些。在这种情况下,我们可以提供数字版游戏机的实现作为组件实现类。//PS5数字控制台类型PS5WithDigitalstruct{}func(pPS5WithDigital)StartGPUEngine(){fmt.Println("启动普通gpu引擎")}func(pPS5WithDigital)GetPrice()int64{return3600}then另外对于这两种基本款,厂商一般也会开发出各种主题和配色的主机,以及硬件配置增加的主机等,这两种的价格肯定会与基本款有所不同。对于这一层我们可以使用装饰器来实现扩展,避免对基础组件类的改动。下面是Plus版本和主题颜色版本的两个增强,使用两个装饰器实现。"文中使用的完整可运行源码可以发到公众号"网络管理备忘[设计模式]获取"//加装版装饰器func(p*PS5MachinePlus)SetPS5Machine(ps5PS5){p.ps5Machine=ps5}func(pPS5MachinePlus)StartGPUEngine(){p.ps5Machine.StartGPUEngine()fmt.Println("startplusplugin")}func(pPS5MachinePlus)GetPrice()int64{returnp.ps5Machine.GetPrice()+500}//装饰器类型PS5WithTopicColor结构{ps5MachinePS5}func(p*PS5WithTopicColor)SetPS5Machine(ps5PS5){p.ps5Machine=ps5}func(pPS5WithTopicColor)StartGPUEngine(){p.ps5Machine.StartGPUEngine()fmt.Println("Prestigethemecolorhost,GPUstartup")}func(pPS5WithTopicColor)GetPrice()int64{returnp.ps5Machine.GetPrice()+200}根据decorator模式的特点,两种增强也可以叠加把它们结合起来,打造一款高端题材的限量版主机……呃,是不是有点像大游戏厂商的时候年年出新机,就是没出第二代,每年都会给你多出几台?极限配色,升级屏幕,你说的是XXX(评论区可以下定论)好吧,在客户端,我们可以把装饰器和组件结合起来,做一个高端主题的限量版主机。。...."本文用到的完整可运行源码去公众号『网络管理对话bi唯』发送【设计模式】获取"funcmain(){ps5MachinePlus:=PS5MachinePlus{}ps5MachinePlus.SetPS5Machine(PS5WithCD{})//ps5MachinePlus.SetPS5Machine(PS5WithDigital{})//可以换主机ps5MachinePlus.StartGPUEngine()price:=ps5MachinePlus.GetPrice()fmt.Printf("PS5CD豪华加版,价格:%d元\n\n",price)ps5WithTopicColor:=PS5WithTopicColor{}ps5WithTopicColor.SetPS5Machine(ps5MachinePlus)ps5WithTopicColor.StartGPUEngine()price=ps5WithTopicColor.GetPrice()fmt.Printf("PS5CDDeluxePlus经典主题配色版,价格:%d元\n",price)}完整源码这篇文章,已经同步收录在我整理的电子教程中。可以发关键词到我的公众号「网络管理口bi唰」【设计模式】接收装饰器和几种模式的区别。装饰器和代理在结构上是相似的。行为上类似于责任链模式,现在总结一下它们的区别装饰模式VS代理模式装饰模式是代理模式的一种特殊应用。装饰者模式强调自身功能的扩展。代理模式强调对代理进程的控制。DecoratorVSChainofResponsibilityModeDecorator和责任链在行为上都是多个单元组合完成逻辑处理,但decorators专注于对某物进行扩展,最终得到一个产品。责任链更强调一步步完成一个过程,更像是一个任务列表,与装饰者模式不同的是,责任链可以随时终止。比如OA系统中的请假审批场景,假设员工请假需要得到组长、总监、经理的批准。这样的话,如果你使用装饰者模式来实现,不管你的请假请求在之前的环节是被批准还是被拒绝,整个链条都不会被打断,最终我们会得到三者对申请的所有反馈批准人的级别。使用责任链模式,在每个阶段,每个审批者都有批准或拒绝的权利。如果请求在任何级别被拒绝,则整个流程结束并且请求不会继续流向下一级审批人。那么看到这里,你觉得像web框架的中间件这种东西应该用责任链还是装饰器来实现呢?总结一下,装饰者模式有很多优点。它是对继承的有力补充,比继承更灵活,在不改变原有对象的情况下,动态地将功能扩展到一个对象上,即插即用。通过使用不同的装饰类以及这些装饰类的排列组合,可以实现不同的效果,完全遵循程序设计的“开闭原则”。但是装饰器的使用肯定会给程序带来更高的复杂度和更低的可读性,子类集成的代码结构会更加直白易懂,而且装饰器虽然符合“开闭原则”,但是它会给程序带来更多的类,多层装饰时动态装饰会更复杂。所以一般来说,在使用装饰器模式的时候,两害相权取其轻,引入更多的装饰器类是为了不频繁修改已经形成的子类。应用的时候一定要记住,装饰器是用来“增强”某个东西的,但不要用装饰器来实现这个东西本身的主要逻辑。
