装饰者模式实战:用包装代替继承,告别类爆炸

发布时间:2026/9/8 17:32:28
装饰者模式实战:用包装代替继承,告别类爆炸 1. 从一次“类爆炸”的改造说起装饰者模式到底解决了什么如果你写代码超过两年大概率经历过这种场景产品经理提了一个需求要给现有的消息推送服务增加“加密传输”能力。你一看好办继承一个子类就完事了。没过一周又要求支持“压缩传输”。行再写个子类。再往后需求变成“既要压缩又要加密”“先加密再压缩”“带签名的加密压缩”……你会发现继承体系开始失控。4种独立能力排列组合理论上你要维护几十个子类才能覆盖所有情况。这就是设计模式里说的“类爆炸”。我第一次意识到装饰者模式的价值就是在处理一个类似的报表导出功能时。当时的导出器有PDF、Excel两种基础格式要叠加水印、加密、签名、压缩四类附加能力。如果用继承硬写组合数量是2乘以2的四次方也就是32个类。而引入装饰者模式之后基础组件2个装饰器4个一共6个类就解决了全部排列组合问题而且以后增加一种新能力只需要新增一个装饰器类完全不碰已有代码。装饰者模式的核心思想一句话就能讲明白不修改原有代码不通过继承扩展子类而是用“包装”的方式把新职责动态地附加到对象上。这种扩展方式比继承更灵活因为它是在运行时组合的你可以在程序运行过程中像搭积木一样决定到底给对象叠加哪些能力。这个模式非常高频。你要是做Java开发天天用的BufferedReader、FileInputStream这一整套IO流就是装饰者模式的标准教科书。要是做Android开发ContextWrapper体系也赫然是装饰者的忠实拥趸。后面我会详细拆这两个源码案例。这篇内容我准备这样讲先用最直观的代码给你看装饰者的四个角色再走一个完整实战案例然后重点聊聊调用链执行顺序、与相似模式的边界最后从JDK和Android源码里找实证再分享几个实战中容易踩的坑。通篇会用Java写示例但思路是通用的C、Python、TypeScript都能直接平移。2. 四个角色与一个最小骨架先敲明白装饰者长什么样很多教程上来就贴UML图看得人昏昏欲睡。我换个方式先讲清楚四个角色再带你写一个能跑的最小例子。2.1 四个角色分别是谁、各自干什么装饰者模式涉及4个角色我用人话翻译一遍抽象组件Component定义业务接口。它是整个继承和包装体系的公共父类型让装饰器和被装饰对象能互相替换。具体组件ConcreteComponent被装饰的原始对象实现核心业务逻辑。它就是功能扩展的“地基”。抽象装饰器Decorator持有一个抽象组件的引用同时继承/实现抽象组件。这个角色是承上启下的关键它保证了装饰器的嵌套能力。具体装饰器ConcreteDecorator在调用被包装对象方法的前后附加额外的职责。关键点在于装饰器和被装饰对象都实现了同一个接口。这保证了无论外面包了多少层外层拿到的仍然是一个“组件”可以继续被包装也可以直接调用。2.2 用20行代码搭一个最小的装饰者骨架假设我们有一个TextProcessor接口负责处理文本然后有一个基础实现再加一个给文本加HTML标签的装饰器。完整代码如下// 1. 抽象组件定义业务接口 public interface TextProcessor { String process(String text); } // 2. 具体组件基础实现原样返回文本 public class PlainTextProcessor implements TextProcessor { Override public String process(String text) { return text; } } // 3. 抽象装饰器核心是持有TextProcessor引用 public abstract class TextDecorator implements TextProcessor { protected TextProcessor wrapped; public TextDecorator(TextProcessor wrapped) { this.wrapped wrapped; } Override public String process(String text) { return wrapped.process(text); } } // 4. 具体装饰器给文本包一层HTML标签 public class HtmlTagDecorator extends TextDecorator { public HtmlTagDecorator(TextProcessor wrapped) { super(wrapped); } Override public String process(String text) { String result wrapped.process(text); return html result /html; } }用起来是这个效果TextProcessor processor new HtmlTagDecorator(new PlainTextProcessor()); System.out.println(processor.process(hello)); // 输出: htmlhello/html你再写一个UpperCaserDecorator就可以这样嵌套TextProcessor processor new UpperCaserDecorator(new HtmlTagDecorator(new PlainTextProcessor())); System.out.println(processor.process(hello)); // 输出: htmlHELLO/html注意观察执行顺序外层装饰器的process方法先拿到wrapped.process(text)的内层结果再附加自己的逻辑。2.3 为什么装饰者必须和被装饰对象实现同一个接口这是装饰者模式最容易忽略、也最核心的设计约束。很多初学者想不通装饰器为什么不能单独定义一个类内部持有组件引用然后想加什么方法就加什么方法原因是如果装饰器不实现组件的抽象接口它就无法被继续装饰也无法在原有组件类型被使用的地方无缝替换。装饰者模式最大的优势是“可以无限嵌套”这种能力完全建立在“所有参与者都是同一抽象类型”之上。一旦装饰器本身变成了一个独立类型你就没法给装饰器再套装饰器了动态叠加的能力当场消失。另一个原因是面向对象的多态。调用方只需要依赖抽象组件TextProcessor完全不用关心这个对象内部被包装了几层。如果你在业务代码里到处写PlainTextProcessor这种具体类型那就废掉了装饰者模式一大半功力。注意有不少人把静态代理和装饰者模式混为一谈二者的代码结构确实很像区别在于意图。代理模式侧重于控制访问、延迟加载、权限控制装饰者模式侧重于增强功能。这个边界我放在第5节详细展开。3. 实战用装饰者模式给“奶茶加料”写一套自动计价系统理论说再多不如来一个能跑通的完整案例。这次我用一个大家都能秒懂的场景奶茶店点单系统。一杯基础奶茶可以加珍珠、加椰果、加奶盖、加布丁每种料价格不同还可以无限叠加。请看这个需求如何用装饰者模式优雅实现。3.1 需求抽象与接口定义每杯饮品都有一个描述和一个价格核心行为就两个。于是抽象组件设计如下public interface MilkTea { String getDescription(); double cost(); }这个接口就是整个体系的公共父类型。基础饮品实现它所有加料装饰器也实现它。后边你会看到无论嵌套多少层MilkTea这个类型始终是唯一需要对外暴露的门面。3.2 实现基础饮品组件这里做两个基础单品原味奶茶和红豆奶茶。public class OriginalMilkTea implements MilkTea { Override public String getDescription() { return 原味奶茶; } Override public double cost() { return 10.0; } } public class RedBeanMilkTea implements MilkTea { Override public String getDescription() { return 红豆奶茶; } Override public double cost() { return 13.0; } }注意这里我刻意没有把“红豆”做成装饰器。因为红豆是基础配方的一部分不是可选项。判断一个能力到底属于“基础组件”还是“装饰器”标准很简单没有它这个产品还算不算这个品类的产品没有加料的原味奶茶依然是奶茶但没有红豆的红豆奶茶就是另一杯原味奶茶。前者用装饰器后者就必须进基础组件。3.3 实现抽象装饰器现在先定义顶层装饰器。它持有一个MilkTea引用并且将接口方法的调用透传给内层对象public abstract class ToppingDecorator implements MilkTea { protected MilkTea milkTea; public ToppingDecorator(MilkTea milkTea) { this.milkTea milkTea; } Override public String getDescription() { return milkTea.getDescription(); } Override public double cost() { return milkTea.cost(); } }很多初学者会问既然ToppingDecorator只是透传为什么不能把milkTea改成public直接暴露字段这里有一个隐含设计protected字段搭配构造器注入可以限制子类必须通过构造传入依赖避免出现一个没有目标对象的“悬浮装饰器”。这是面向对象封装性的基本素养。3.4 实现具体装饰器珍珠、椰果、奶盖重点来了。以珍珠为例它的逻辑是在内层描述后面追加描述在内层价格基础上加价public class PearlDecorator extends ToppingDecorator { public PearlDecorator(MilkTea milkTea) { super(milkTea); } Override public String getDescription() { return milkTea.getDescription() 珍珠; } Override public double cost() { return milkTea.cost() 2.0; } } public class CoconutDecorator extends ToppingDecorator { public CoconutDecorator(MilkTea milkTea) { super(milkTea); } Override public String getDescription() { return milkTea.getDescription() 椰果; } Override public double cost() { return milkTea.cost() 3.0; } } public class CheeseFoamDecorator extends ToppingDecorator { public CheeseFoamDecorator(MilkTea milkTea) { super(milkTea); } Override public String getDescription() { return milkTea.getDescription() 奶盖; } Override public double cost() { return milkTea.cost() 5.0; } }如果每个装饰器的getDescription和cost都要自己拼接你会发现重复代码不少。这里可以做一个优化把字符串拼接和价格叠加提升到抽象层。比如ToppingDecorator增加抽象方法toppingName()和toppingPrice()子类只需要返回自己的加料名和价格。这个优化不是装饰者模式本身要求的但却是实战中减少样板代码的有效手段类似模板方法模式思想在装饰器内部的局部应用。3.5 动态组合验证到了点单环节你会瞬间体会到装饰者模式的魅力public class MilkTeaShop { public static void main(String[] args) { // 一杯原味奶茶加珍珠 MilkTea tea1 new PearlDecorator(new OriginalMilkTea()); System.out.println(tea1.getDescription() 价格 tea1.cost()); // 原味奶茶 珍珠价格12.0 // 一杯原味奶茶加珍珠再加椰果 MilkTea tea2 new CoconutDecorator( new PearlDecorator(new OriginalMilkTea())); System.out.println(tea2.getDescription() 价格 tea2.cost()); // 原味奶茶 珍珠 椰果价格15.0 // 一杯红豆奶茶什么都不加 MilkTea tea3 new RedBeanMilkTea(); System.out.println(tea3.getDescription() 价格 tea3.cost()); // 红豆奶茶价格13.0 // 一杯原味奶茶加珍珠、椰果、奶盖、布丁 MilkTea tea4 new CheeseFoamDecorator( new CoconutDecorator(new PearlDecorator(new OriginalMilkTea()))); System.out.println(tea4.getDescription() 价格 tea4.cost()); // 原味奶茶 珍珠 椰果 奶盖价格20.0 } }所有排列组合都不需要为“原味加珍珠加椰果”这样的组合单独建一个类。杯子还是那个杯子你在运行时决定往里面加什么料。这比继承方案要清爽太多——用继承实现时每加一种新料你都要考虑现有类的组合关系系统内类数量呈指数级上升。新增一款配料比如“芋圆”只需要这样public class TaroBallDecorator extends ToppingDecorator { public TaroBallDecorator(MilkTea milkTea) { super(milkTea); } Override public String getDescription() { return milkTea.getDescription() 芋圆; } Override public double cost() { return milkTea.cost() 4.0; } }不需要动OriginalMilkTea不需要动RedBeanMilkTea不需要动任何一个其他装饰器。这是典型的对修改关闭、对扩展开放。4. 层层装饰的执行顺序从递归视角理解调用链这一节值得单独拎出来讲。我见过很多代码看懂了装饰者的类结构但一到“多重装饰后的行为顺序”就犯迷糊调试起来一头雾水。4.1 装饰器的嵌套构造与递归调用过程以new CheeseFoamDecorator(new CoconutDecorator(new OriginalMilkTea()))为例。构造过程由内到外先创建OriginalMilkTea再把它包进CoconutDecorator最后将整个对象包进CheeseFoamDecorator。当你调用最外层对象的cost()方法时发生了什么呢用递归视角拆解CheeseFoamDecorator.cost()执行先调用milkTea.cost()即CoconutDecorator.cost()CoconutDecorator.cost()执行先调用milkTea.cost()即OriginalMilkTea.cost()OriginalMilkTea.cost()直接返回10.0回到第2步CoconutDecorator.cost()拿到10.0加上自己的3.0返回13.0回到第1步CheeseFoamDecorator.cost()拿到13.0加上自己的5.0返回18.0。整个调用过程像洋葱一样一层剥开露出下一层直到最内层返回结果再一层层往回穿。理解了这个递归结构你就能准确预测任何多层装饰下的行为。我建议你在IDE里给接口方法打断点观察调用栈的变化。第一次跟踪时你会非常直观地看到调用栈是从最外层一路压进最内层的然后逐层弹栈返回。这个观察经验对理解装饰器的行为模型帮助巨大。4.2 设计装饰逻辑时的顺序考究既然装饰器是层层包裹的那么不同层的装饰逻辑就有“先后顺序”问题。同样三个装饰器A、B、C以A(B(C(obj)))和C(B(A(obj)))两种方式嵌套执行效果可能完全不同。举个真实项目的例子。数据传输管道有三个装饰器加密、压缩、日志。如果按new EncryptDecorator(new CompressDecorator(new LogDecorator(...)))的顺序包装那么数据流向是进入加密装饰器时先加密再交给压缩装饰器压缩最后落到日志装饰器记录。从调用方的视角看日志记录的内容是外层加密后的密文还是压缩前的原始数据取决于整体管线如何组合。常见的误区是在不知道各层职责边界的情况下随意堆叠装饰器导致最终行为与预期不符。举个例子给奶茶加料的顺序对价格没有影响——加法交换律保证了结果一致。但描述字符串的顺序会变化如果业务上需要“珍珠在前、椰果在后”你就必须按特定顺序构造。这里我的建议是存在前置/后置逻辑的装饰器要明确它在整条链里的位置最好在命名上体现出来比如PreEncryptDecorator、PostCompressDecorator同一层级的装饰器如果叠加顺序无所谓要保证代码可读性按业务习惯固定一种顺序不要今天这个顺序、明天那个顺序构造层次较深时考虑用工厂或者Builder封装组合逻辑避免业务代码变成一条超长的构造链。4.3 装饰器对接口方法透传的注意点抽象装饰器里getDescription()和cost()是逐层透传之后再做自己的增强。但如果某个组件有更多方法比如增加了一个getCalories()卡路里方法抽象装饰器要一并透传否则具体装饰器会丢失这部分职责。一个实战经验接口越膨胀装饰器透传越痛苦。这也从侧面反映出装饰者模式的适用边界——它最适合接口方法少且稳定的场景。如果接口有十几个方法定义一个装饰器类要写十几遍透传代码这时代价会变得很大。5. 装饰者模式为什么总被拿来和继承、代理、适配器比较面试里关于装饰者模式的高频问题就是让你区分它与代理模式、适配器模式之间的差别。把它们放在一起对比不是为了考概念而是帮助你判断在真实需求里到底该选谁。5.1 组合优先于继承动态与静态的本质差异继承是静态的、编译期确定的。用继承扩展功能你在写代码时就必须知道需要哪些组合运行时无法改变。装饰者是动态的、运行期确定。你可以在程序跑起来之后根据条件决定给对象套哪几层装饰器。用一个类比继承是一栋楼开盘前就定好了户型你在图纸上选了“三室一厅书房”这个子类装饰者是毛坯房交付后你想刷墙就请刷墙队想铺地板就请地板队今天先刷墙明天再铺地板都行而且两队还可以同时进场。设计模式第一原则“组合优于继承”在装饰者模式里体现得淋漓尽致。组合带来的可插拔性让系统扩展的粒度从“类级别”降到了“行为级别”。代价是什么继承体系类型清晰每个子类都是一个明确的类型而装饰者体系下对象的具体类型被层层包装掩盖了。后面第7节我会讲它在实战中带来的类型识别问题。5.2 装饰者与代理模式结构相似意图不同代理模式和装饰者模式的Java代码结构非常像都持有目标对象引用都实现目标接口都在外层附加逻辑。它们可以做到几乎相同的代码形态。关键区别在意图上代理模式的核心是控制访问。比如你不想让调用方直接触达真实对象远程代理、安全代理、延迟加载代理代理的主要职责是“管理对象的生命周期与访问权限”。装饰者模式的核心是增强能力。它不管对象能不能被访问只负责给对象添加责任。网上有句话总结得很到位代理模式说“我要控制你”装饰者模式说“我要包着你变得更强”。面试时如果能说出这个区别顺带补充一句“结构相似不代表可以互换应用场景取决于意图”这是比较加分的回答。5.3 装饰者与适配器模式接口变换与功能叠加的区别适配器模式做的事情是“接口转换”。比如你有Type-C接口但设备只接受USB-A你需要一个转接头把Type-C变成USB-A。源代码被适配后对外暴露出的是另一套接口。装饰者模式不改变接口它保持原有接口不变只是在原有接口行为执行前后增加职责。装饰者不做接口转换只做行为叠加。这个差异直接影响了使用方式适配器通常在系统集成阶段出现用来把外部系统接口“翻译”成本系统需要的接口装饰者通常在业务扩展阶段出现用来在不修改源码的前提下扩展能力。对比维度装饰者模式代理模式适配器模式目标动态增强功能叠加职责控制访问管理对象生命周期将接口转换成客户端需要的接口接口保持不变通常保持不变改变接口嵌套支持多层嵌套一般单层一般单层关注点对象内部行为的扩展对象访问的外部控制对象接口的匹配实例BufferedReader, ContextWrapper延迟加载代理、远程代理InputStreamReader, Slf4j门面实现5.4 与继承方案的成本对比还有一个常见的思考角度装饰者模式到底比继承好在哪我们算一笔代码账。假设基础组件是MilkTea加料有珍珠、椰果、奶盖三种。用继承实现的话一个基础奶茶类三个单料子类珍珠奶茶、椰果奶茶、奶盖奶茶三个双料子类珍珠椰果奶茶、珍珠奶盖奶茶、椰果奶盖奶茶一个三料子类这已经是8个类了。加料种类越往后增长组合数越多。同时类与类之间的共性会越来越难抽取维护成本会呈指数级上升新建一个类还可能影响已有类的逻辑。用装饰者实现呢1个抽象组件接口2个基础组件1个抽象装饰器3个具体装饰器总共7个类已经覆盖了所有组合。再加一种新料只需要新增1个具体装饰器类。随着系统规模增大装饰者的优势越来越明显。6. 真实世界里的装饰者影子JDK与Android源码的底层证据聊设计模式不能只停留在demo层面。源码里的应用才是检验你是否真正理解模式的试金石。我会挑两个最经典的实例JDK的IO流和Android的ContextWrapper。6.1 JDK IO流一个教科书级别的装饰者结构Java开发者几乎每天都在用new BufferedReader(new FileReader(test.txt))。这套IO类结构堪称装饰者模式最普及的标本。Reader是抽象组件定义了读字符的接口FileReader、StringReader、CharArrayReader这些是具体组件BufferedReader、LineNumberReader是具体装饰器——它们把“带缓冲”“带行号”的能力动态加到任意Reader之上。关键在于FilterReader这个类是抽象装饰器它内部持有一个Reader引用而BufferedReader并不是直接继承FilterReader但整体结构与装饰者是完全一致的。你还能看到更随意的组合new BufferedReader(new InputStreamReader(new FileInputStream(a.txt), StandardCharsets.UTF_8))。这一行里嵌套了文件读取、字节转字符、缓冲三层装饰。每一层都解决一类问题却彼此完全解耦。如果没有装饰者模式Java标准库大概要为每种组合都搞一个专属类那IO体系早就爆炸了。通常大家不会觉得BufferedReader很难用因为依赖的抽象类型始终是Reader。这就是装饰者的设计魔力你拿到的是一个“看起来像Reader、用起来像Reader但行为已经被加强”的对象。6.2 Android的ContextWrapper装饰者的安卓身影做过Android开发的人都知道Context是一个抽象类ContextImpl是真真正正干活的底层实现而ContextWrapper则是装饰者的抽象角色。ContextWrapper内部持有Context mBase所有方法都通过mBase透传。开发者经常接触的Activity、Service、Application本质上都继承自ContextWrapper在特定场景覆写了部分行为。比如ContextThemeWrapper就给ContextWrapper包装了主题信息。这种做法的价值在于系统可以在不修改ContextImpl的情况下通过不同的ContextWrapper子类给应用提供不同“视角”的上下文能力。比如你拿到的Context如果是Application的它和应用进程生命周期绑定如果是Activity的它可能额外携带界面主题等信息。当面试官问“Android里哪里用到了装饰者模式”ContextWrapper是绝对不会错的答案。这比单纯背概念深刻得多也从侧面说明装饰者是Android系统层面的基础架构选择之一。6.3 微信小程序与前端生态中的装饰者痕迹不局限于Java装饰者思想在前端领域也广泛存在。比如Express框架的中间件机制、Koa的洋葱模型、Redux的middleware本质上都可以理解为装饰者模式的变体每个中间件包住下一个中间件请求贯穿整条链。Java Web开发常用的Filter过滤器其职责链模型也与装饰者互为表里。理解设计模式这件事最忌讳的就是“背类图”。当你发现同一个结构在不同语言、不同框架里反复出现就开始真正形成架构直觉了。7. 实战中必须避免的坑类型丢失、过度包装与调试噩梦模式虽好用错了场景、用多了程度照样会让代码变成灾难。这一节总结我自己代码评审中经常看到的几个真实反例。7.1 装饰后类型丢失无法调用具体类型的特色方法这是使用装饰者模式最常踩的坑。比如MilkTea接口定义了getDescription()与cost()而RedBeanMilkTea自己还有一个isGlutenFree()方法调用方传入的原始类型是RedBeanMilkTea代码想调用它特有的方法MilkTea tea new PearlDecorator(new RedBeanMilkTea()); tea.isGlutenFree(); // 编译错误MilkTea接口中没有这个方法这是装饰者模式天然的代价因为所有层次都抽象为MilkTea你一旦给RedBeanMilkTea套上装饰器就会丢失其具体子类的特有方法。如果业务上这些方法不可或缺要么把那些方法也提升到接口里要么就需要从设计层面避免“对具体子类做装饰后再调用其特有方法”这种用法。选择装饰者模式前需要思考你的组件接口是否覆盖了未来可能需要的关键能力如果接口预留不足装饰器会掩盖子类的多样性。所以接口设计在这个模式里尤为重要。7.2 装饰顺序产生的逻辑差异与歧义前面说过装饰顺序可能显著影响结果。在写代码时如果同一套装饰器在不同地方以不同顺序组装未来维护的人会非常头疼。尤其在业务含义上有些操作天然有先后逻辑。举例加密和压缩。如果先加密再压缩压缩模块面对的是密文压缩率通常很差如果先压缩再加密压缩模块面对的是明文压缩率高最终密文也没泄露明文信息。两种顺序都合理但结果完全不同。建议在核心入口用一个工厂方法或者配置中心统一管理装饰顺序禁止业务代码随意嵌套。这样既能保留装饰者的灵活又能避免因顺序不一致引发的线上事故。7.3 无节制的多层装饰导致调试困难装饰者很灵活但是灵活到了滥用程度调试体验会断崖式下降。我曾经在一个遗留系统里见过包装了6层的对象光是想搞清楚一次方法调用最终执行到哪个具体组件就得在IDE里踩断点踩好几个来回。还有一种变体问题同一个装饰器被重复套了两次。比如误把日志装饰器套了两遍日志就会打印两遍缓冲装饰器套两层可能不会产生功能问题但白白浪费内存。排查这些问题通常要靠肉眼逐层检查构造链效率很低。我的经验是装饰层数尽量控制在3层以内。超过这个范围就要反思是不是职责拆得过细了。给装饰器起名时把职责讲清楚。BufferedMilkTea这种名字谁也看不懂最好命名为PearlDecorator这种自解释风格。必要时提供一个静态工厂或Builder来组织装配过程业务方只需要传参表示“要珍珠、椰果、不要奶盖”由内部负责构造装饰链。一旦出现问题只需要排查这一个地方。7.4 与静态代理的误用在一个项目里同时出现大量相似的包装类很多团队会把装饰者和代理混着用。比如给Service层做日志、做权限、做事务这类需求如果用装饰者模式去实现每个业务Service都要提供一个对应装饰器代码量不小而且容易和Spring AOP这类已有方案重叠。判断用装饰者还是用代理还要看扩展方向如果是给同一个业务行为无限叠加细节能力那装饰者是合理的如果是给所有业务方法统一加交关注逻辑日志、权限、缓存其实更像代理或者面向切面编程的范畴。硬套装饰者会导致灾难性的类膨胀。提示很多框架层面已经有成熟的AOP机制或过滤器链能解决大部分横切逻辑的问题。装饰者模式应该留给确实需要“运行时动态可选叠加”的业务场景。7.5 序列化与克隆的额外考量如果你的装饰器包装的对象需要序列化比如放到Redis或消息队列里要格外小心。因为装饰器内部持有的是对象引用默认的Java序列化会把整个包装链都序列化进去。一旦某层装饰器里有不可序列化的字段比如数据库连接、线程池就会出现NotSerializableException。我在报表服务中就遇过这个坑给数据源装饰了一层加密逻辑这层内部持有加密密钥对象包含SecretKey结果把这个对象序列化到缓存时直接爆异常。解决方案是给装饰器实现自定义writeReplace方法或者将装饰器设计成不含运行时状态、只含处理逻辑的形式。装饰器最好是无状态对象这样才能安全地放入各种容器。8. 写在最后设计模式的落地判断比背类图更重要回到开头那个报表导出的例子。最终我用6个类替换了原本预想的数十个子类每增加一种导出能力就递交一个一次性修改的PR其他代码全部不碰。上线之后需求变化还在继续不同的客户要不同的能力组合部分客户甚至会在后台配置里动态指定“导出PDF时加密且压缩导出Excel时只加水印”。这种灵活度继承体系基本做不到装饰者模式却可以建立在运行时配置之上。不过我也要泼一盆冷水不是所有“动态加功能”的需求都适合装饰者模式。如果功能叠加的类型极少、组合固定用继承硬写几个子类反而更简单直观。如果你只是想在调用前后各加一行打印写一个代理或者直接改原方法都行没必要为模式而模式。装饰者模式真正的威力场景是组合数多、扩展频繁、希望保持运行时装配的自由度。另外我对设计模式本身的理解是它们不是用来背的“标准答案”而是建立设计交流语言的工具。当你跟团队说“这里用装饰者包装一下”实际上是在说“我们需要在不修改原类的前提下支持新的可选行为组合”。掌握了这种语义比会画类图重要一百倍。如果你是在系统学习设计模式建议把这个模式与组合模式、策略模式、责任链模式放在一起学你会看到它们解决类似问题的不同切面。比如责任链模式和装饰者模式都由层层传递构成区别在于接口方法的分发方式以及消费上的差异。多画调用时序、多在实践中复盘比看几十篇教程都管用。

相关新闻