
1. 当继承开始膨胀装饰模式解决的到底是什么问题我最早被装饰模式Decorator Pattern打动是在接手一个线上日志组件的时候。当时的代码已经迭代了好几轮核心类叫FileLogger负责把日志写进文件。后来加了需求日志要加密于是有人写了EncryptedFileLogger。再过一阵子要求日志按大小自动切分又冒出来SplittingFileLogger。接着是压缩归档、添加时间戳前缀、脱敏过滤……你猜最后这个继承体系长什么样EncryptedSplittingCompressedTimestampedFileLogger类名长到连IDE的自动补全都要卡顿。这不是段子是真实会发生的代码腐化过程。每加一个功能就在继承树上新增一个子类功能之间一旦需要组合子类数量就是笛卡尔积式爆炸。3个功能两两组合需要3个子类4个功能全组合需要11个10个功能呢那是2的10次方减1个类。没人受得了。装饰模式解决的就是这个问题它让你在运行时动态地给对象叠加职责而不是在编译期通过继承去穷举所有组合。你不需要提前写好加密 切分 压缩这种组合类只需要分别写好加密装饰器切分装饰器压缩装饰器然后在调用方按需一层层套起来。想吃几层就包几层像点奶茶加料一样自由。这篇文章我不会只讲UML图和角色定义那些教科书上都有。我会把装饰模式从为什么要这么设计讲到代码怎么写再从它和代理模式、继承到底怎么选讲到实战里容易踩到什么坑。内容主打Java示例因为Java的IO流是装饰模式最经典的现实案例但关键的实现思路对C、Python、C#同样适用。适合谁看正在复习设计模式应付面试的开发者、写业务代码时经常被类爆炸困扰的工程师、以及想搞明白BufferedInputStream外面为什么还能套GZIPInputStream的新人。看完你能得到的是什么时候该用装饰器代码落到工程里到底长什么样以及哪些边界情况会坑你一把这三个层面的完整答案。2. 装饰模式的底层逻辑接口、包装与递归的组合教科书上把装饰模式拆成四个角色抽象组件Component、具体组件ConcreteComponent、装饰器基类Decorator、具体装饰器ConcreteDecorator。背下来很容易看懂就不一定了。我先用大白话拆一遍再讲一个很多人没想透的底层机制。2.1 四个角色分别是什么抽象组件Component是所有参与者的公共接口。不管是原始对象还是被装饰后的对象对外暴露的方法集合完全一致。这是整套模式的基石没有它装饰器就没法伪装成原对象。具体组件ConcreteComponent是你要装饰的原始对象。它实现接口拥有核心业务逻辑是所有装饰的起点。装饰器基类Decorator是个抽象类它实现了接口同时持有一个Component类型的引用。关键点在于这个引用指向谁不看编译期只看构造时传入谁。你可以把装饰器基类理解成一个透明的转发层它所有接口方法的默认实现都是转发给内部持有的那个组件。具体装饰器ConcreteDecorator继承装饰器基类在转发的基础上附加自己的行为比如加密、压缩、加前缀。多个具体装饰器可以互相嵌套形成一个职责链。2.2 生活类比俄罗斯套娃和多层外卖包装我给学生讲装饰模式时最常用的类比是外卖。你点了一份白米饭那个碗就是具体组件。商家问要不要加个卤蛋这是第一个装饰器它在原有对象上加了内容但你还是能拿到那份饭。要不要再加个鸡腿第二个装饰器套上去。最后外卖小哥还给包装袋打了个结——以上所有步骤都不需要改米饭本身的做法。俄罗斯套娃是另一个经典类比。最里面那个娃娃是原始组件外面每一个娃娃都是一层装饰器。打开一层发现还有一层但你始终知道它还是一个娃娃。装饰模式的核心思维就是这种递归包装每次包装后得到的依然是一个符合公共接口的对象因此可以继续被下一次包装当作底座。2.3 透明性是整个模式成立的前提为什么要让装饰器基类也实现同一个接口很多人以为只是为了语法上能调用方法其实背后有个更深的理由透明性Transparency。透明意味着被装饰后的对象对调用方来说和原始对象没有类型上的差别。调用方不需要知道自己在跟一个原始组件打交道还是在跟一个套了五层装饰器的组件打交道。它可以一视同仁地调用接口方法。这也是装饰模式和普通包装类最根本的分水岭。如果你包装出去的对象接口方法列表变了、返回类型变了、连语义都变了那不叫装饰那是适配器或者门面模式。2.4 一层包一层方法调用的实际执行链路假设有这样的调用链new PrefixDecorator(new EncryptDecorator(new PlainMessage()))然后调用方执行send()方法。真实执行顺序是这样的先进入最外层PrefixDecorator的send()它加完前缀逻辑调用内部引用EncryptDecorator的send()EncryptDecorator再调用PlainMessage的send()最内层执行完再一层层把结果返回上来。整个过程像剥洋葱一样从外向内剥再一层层包回来。这个执行链路的顺序是可以人为控制的。装饰器可以在调用内部对象之前做预处理也可以在调用之后做后处理这就给了装饰器极大的灵活性。到这里装饰模式的理论底子就清楚了。核心就三句话公共接口保证类型统一持有接口引用来实现向前转发层层递归来实现任意叠加。3. 从零实现一个可以跑的装饰器消息处理管道实战光讲概念等于纸上谈兵。我直接上一段完整可运行的Java代码场景选一个消息处理管道原始消息是明文我们给它依次叠加加密加前缀统计调用次数三种功能。这个场景在实际项目里很常见比如给监控上报的消息做脱敏、加密、格式化。3.1 定义抽象组件接口// 抽象组件消息处理器 public interface MessageHandler { String handle(String rawMessage); }接口就一个方法输入原始消息输出处理后的消息。所有组件和装饰器都实现它。3.2 实现具体组件// 具体组件明文消息处理器 public class PlainMessageHandler implements MessageHandler { Override public String handle(String rawMessage) { return rawMessage; // 什么都不做原样返回 } }这个类就是外卖里那份白米饭。装饰器往它身上加的每一层都通过它来传递。3.3 定义装饰器抽象基类// 装饰器基类核心是持有被装饰对象的引用 public abstract class MessageDecorator implements MessageHandler { protected MessageHandler wrappee; public MessageDecorator(MessageHandler wrappee) { this.wrappee wrappee; } Override public String handle(String rawMessage) { return wrappee.handle(rawMessage); // 默认直接转发 } }你可以把MessageDecorator理解成装饰器们的公共脚手架。它做了两件事固定所有装饰器都需要接收一个组件引用提供默认的转发逻辑。具体装饰器只需要覆盖handle()并加上自己的行为。3.4 写三个具体装饰器加密装饰器模拟Base64编码public class EncryptDecorator extends MessageDecorator { public EncryptDecorator(MessageHandler wrappee) { super(wrappee); } Override public String handle(String rawMessage) { String result wrappee.handle(rawMessage); // 这里用简单的反转代替真实加密核心是演示装饰器的动作时序 return new StringBuilder(result).reverse().toString(); } }前缀装饰器在结果前面加时间戳public class PrefixDecorator extends MessageDecorator { public PrefixDecorator(MessageHandler wrappee) { super(wrappee); } Override public String handle(String rawMessage) { String result wrappee.handle(rawMessage); return [TS- System.currentTimeMillis() ] result; } }统计装饰器记录调用次数public class CountDecorator extends MessageDecorator { private final AtomicInteger count new AtomicInteger(0); public CountDecorator(MessageHandler wrappee) { super(wrappee); } Override public String handle(String rawMessage) { count.incrementAndGet(); return wrappee.handle(rawMessage); } public int getCount() { return count.get(); } }3.5 组装装饰链并运行public class DecoratorDemo { public static void main(String[] args) { MessageHandler handler new PrefixDecorator( new EncryptDecorator( new CountDecorator( new PlainMessageHandler() ) ) ); String output handler.handle(hello decorator); System.out.println(output); // 输出示例: [TS-1710000000000] rotaroced olleh } }注意组装顺序new的时候从内往外写。最内层是PlainMessageHandler外层依次是计数、加密、前缀。运行的时候执行顺序反过来从外到内先加前缀调加密调计数调最内层的明文处理。这个例子里的实际业务价值很直观消息监控组件每次上报前只需要组装一条装饰链就能同时搞定格式化、编码、埋点统计三件事。而且将来想加脱敏新功能只需要写一个MaskDecorator不需要动任何已有类。3.6 用JDK源码验证Java IO流就是活教材如果你觉得上面的代码还不够有说服力看Java标准库。BufferedInputStream、DataInputStream、GZIPInputStream、ObjectInputStream它们清一色继承自FilterInputStream而FilterInputStream就是标准的装饰器基类——持有InputStream引用所有方法默认转发。你自己写文件读取时是不是写过这种嵌套try (DataInputStream in new DataInputStream( new BufferedInputStream( new FileInputStream(data.bin)))) { // 读取数据 }FileInputStream是具体组件BufferedInputStream负责加缓冲DataInputStream负责解析基本数据类型。每一行都是一个装饰器加了不同的职责。这就是我们日常都在用却不自觉的装饰模式。3.7 C场景下的实现要点Java之外的场景也简单提一下。C里实现装饰模式最大的差异在内存管理装饰器持有的是指针谁负责释放我的建议是装饰器基类里用裸指针接收但在实际使用中用std::unique_ptr或std::shared_ptr来管理所有权具体取决于装饰链的生命周期。更省心的做法是使用std::unique_ptr并在装饰器构造里移动所有权这样链式构造时天然表达外层拥有内层的语义。另外C的虚析构函数一定要写否则删除外层装饰器时不会正确调用内部组件的析构。Python就更轻松了因为Python有内建的装饰器语法糖不过那个语法糖本质是函数装饰器跟这里讨论的类级装饰模式不是一回事用类实现时思路与Java一致。4. 装饰器、继承与代理边界到底划在哪里面试和代码评审里最常出现的灵魂拷问是装饰模式跟继承有什么区别跟代理模式有什么区别跟适配器又有什么区别我把它们放在一张表里对比再逐个说清楚。对比维度继承装饰模式静态代理适配器扩展方式编译期固定类层次穷举运行时动态嵌套编译期或运行时绑定编译期固定接口关系子类是父类的一种装饰后仍是原接口代理类和目标类可同接口把A接口变成B接口控制点子类重写父类逻辑每个装饰器控制一段切片逻辑代理控制访问时机与方式只做接口转换职责方向功能叠加功能叠加访问控制、延迟加载兼容性适配客户端感知知道用的是哪个子类无感知透明通常无感知感知到是适配后的接口典型成本类数量爆炸对象数量增加、链路变深每个代理类只服务一个目标转换层可能损失语义4.1 装饰器 vs 继承不是替代品是互补关系继承的价值在于is-a关系子类在主类基础上做特化。如果功能组合是有限的比如最多两个维度交叉继承完全没问题。真正的分水岭出现在组合维度增多之后。举个例子你的类有两种加密算法、三种压缩格式、四种传输协议全组合需要24个类。这时候用继承不只是类多的问题还有一个更隐蔽的麻烦组合类内部充满重复代码——AESZipTCPTransport和RSAZipTCPTransport之间压缩和传输的代码几乎一样只不过加密段调了不同算法。装饰模式把每个维度抽象成独立的装饰器AES一个类、ZIP一个类、TCP一个类然后在运行时任意排列组合就是动态的超级组合。有人担心装饰模式对象太多性能差我的看法是除非这个方法被调用在每秒百万次级别的热路径上否则多一层间接调用的开销基本可以忽略。用继承换代码复杂度的降低这笔买卖大多数时候划算。4.2 装饰器 vs 代理增强与控制的分工不同这是最容易混淆的一对因为它们的类结构长得几乎一样都是持有目标对象转发方法调用。但语义上有本质区别代理模式的核心是控制。代理不想让调用方直接接触目标于是插入了一层负责鉴权、限流、延迟加载、日志记录。它不一定关心目标对象的业务结果好不好更关心谁调了、什么时候调的、能不能调。装饰模式的核心是增强。装饰器最终还是要调用内部组件但调用前后它会附加新的处理逻辑让结果变得更丰富。它关心的是怎么让输出变得更好。实操里怎么判断你就问自己一句去掉这一层调用方的代码逻辑有没有变化如果是代理去掉代理后调用方可能直接连目标对象都接触不到如果是装饰器去掉装饰器后调用链还是通的只是处理结果少了一些附加效果。另外代理模式创建的代理对象通常在编译期或者工厂里就固定了装饰器则天然支持运行期多层动态嵌套。4.3 装饰器 vs 适配器饿了吗给你送了个英式插头适配器解决的是接口不兼容的问题。一个很形象的类比你去国外旅行手机充电器接口和当地墙上的插座孔不匹配你用了一个转换插头——这就是适配器。它不增加任何功能不改变电流强弱只负责把一种形态转成另一种形态。装饰器更像外卖加料饭还是那份饭你只是多了卤蛋和鸡腿。如果某个类定义的是handle()但调用方需要的是process()你用适配器包一层就对了如果调用方用handle()你觉得结果不够好想加个缓存、加个校验这才是装饰器出场的时机。4.4 什么时候真的不该用装饰器我不建议把装饰模式当成万能膏药。以下场景它并不合适装饰层数过多且调用极频繁比如一个数据处理流水线套了十几层每层都有方法调用开销在性能敏感场景要慎重可以考虑把装饰链合并成单一类。需要强类型语义的场景装饰后的对象永远还是原接口类型你无法在编译期表达这是加了缓存能力的对象。如果这个特殊能力方法只属于某一个装饰器比如3.5里CountDecorator的getCount()调用方想要调用它还得向下转型这就破坏了封装。装饰器之间逻辑高度耦合比如加密装饰器必须在压缩装饰器之前执行顺序固定此时把顺序固化在代码里反而清晰没必要全盘装饰化。设计模式不是必须套用的条条框框它是工具箱。什么时候掏出哪个工具取决于问题本身。5. 实战踩坑记录装饰链的顺序、相等性与调试困境理论都会一写就崩。我把自己在真实项目里踩过、以及帮同事排查过的装饰模式相关坑都列出来每一个都是血泪教训。5.1 装饰顺序就是业务语义加前缀和加密的先后不是小事回到3.5的消息管道例子。PrefixDecorator(new EncryptDecorator(...))的执行顺序是先加密后加前缀。输出长这样[TS-...] rotaroced olleh前缀是明文的。如果反过来写EncryptDecorator(new PrefixDecorator(...))执行顺序变成先加前缀后加密。输出是[TS-...] hello decorator反转后的一整串前缀也被加密了。这两种方案在不同业务场景下都有道理如果前缀只是给运维排查用的追踪标识那它不该被加密用前者如果整条消息包括标识符都要整体加密传输用后者。但问题在于代码评审里肉眼几乎看不出语义差异。我踩过的坑就是某个安全需求上线后同事把两行代码顺序调换了导致所有日志消息里的追踪ID全变成了密文。排查了半天。建议在装饰器类的Javadoc里写清楚它是对原始消息操作还是对上游处理结果操作有条件的在关键位置加注释说明推荐嵌套顺序。5.2 装饰链越长栈调用越深异常堆栈越难读每套一层装饰器方法调用的栈深度就加一层异常堆栈也长一段。当你套了七八层装饰器最底层组件抛异常时堆栈的阅读成本非常高。我的经验是装饰器基类的异常处理策略要提前统一。要么所有装饰器都不捕获异常让原始异常一路向上抛保留完整堆栈要么只在最外层装饰器统一捕获一次把前面的链路清扫干净再包装一次业务异常。千万不要每一层都catch一下又包一层自定义异常那你会得到一个嵌套四次、每层都语焉不详的异常链。5.3 equals、hashCode与instanceof全部失效如果装饰器没有重写equals()和hashCode()那么decoratedObj.equals(nakedObj)永远返回false就算它们内部逻辑完全一样。这在使用HashMap、HashSet做缓存和去重时是致命的。另外instanceof PlainMessageHandler在装饰后的对象上永远返回false因为外层对象是PrefixDecorator类型。你的代码里如果写了if (handler instanceof PlainMessageHandler)这种酷似原汁原味判断的代码遇到装饰器就会静默走错分支。我自己处理这类问题的原则装饰器一般不重写equals()和hashCode()因为这会让装饰后是否等于装饰前的语义变得模糊。如果你需要比较业务内容应该在接口里定义一个业务方法比如getContent()比较时统一取方法返回值而不是依赖对象相等性。类的类型判断要走接口方法不要走instanceof。这反向要求你在设计接口时把我需要判断哪些类型提前变成我需要提供哪些行为标识。5.4 构造函数链的API膨胀装饰器的洋葱参数装饰器一旦变多最痛苦的不是运行期而是创建装饰链的那一坨代码。每个装饰器都可能有自己的配置参数比如加密算法的密钥、压缩等级、前缀格式。组装起来你可能看到一堵墙new A(new B(new C(new D(new E()...))))维护这种代码很考验耐心。我给出的实用解法是提供静态工厂方法或者叫命名构造器public static MessageHandler buildStandardPipeline(String secretKey, String prefix) { return new PrefixDecorator( new EncryptDecorator(secretKey, new CountDecorator( new PlainMessageHandler()))); }把管道怎么搭封装成一个方法业务侧只传参数不关心结构。这比让每个调用方各自组装卫生得多。5.5 一次性装饰器与重复装饰的坑对于无状态的装饰器比如加前缀、加密复用一个实例完全没问题。但对于有内部状态的装饰器比如计数装饰器如果你复用同一个实例去装饰多个不同的组件计数器会统计出张冠李戴的数据。另一个反直觉的问题是重复装饰同一个实例MessageHandler h2 new PrefixDecorator(h1); MessageHandler h3 new PrefixDecorator(h2);如果h1本身已经被装饰过一次那么h3再包一层PrefixDecorator执行时会加两次前缀。表面上代码没错但你的加一次前缀的意图落空了。排查时我经常看到这种低级但隐蔽的bug。建议在工厂方法里对入参做防护性检查或者用注释明确约定传入的组件必须是未被装饰过的原始组件。5.6 多线程场景下的共享状态装饰器里如果持有可变状态比如计数器、缓存多线程访问时要格外小心。推荐用AtomicInteger、ConcurrentHashMap等并发工具类或者在装饰器基类文档里明确标注非线程安全每次调用请创建新实例。这个问题在面试中经常被反向提问装饰器模式是否线程安全标准回答是装饰器本身的线程安全性取决于内部状态的线程安全性与模式本身无关。但作为工程实践无状态装饰器优先有状态装饰器务必做好同步。实战后的体会与一点建议写到这里装饰模式的理论、实现、选型和坑都过了一遍。最后聊聊我自己的感受。最早我以为装饰模式就是包一层后来才意识到它的精髓是组装思想——不通过预先定义继承关系来穷举功能组合而是通过运行时嵌套的简单结构得到近乎无限的组合能力。这个思想在我日常设计接口、拆分服务、编排中间件时都反复用到。学习阶段我最推荐的练习不是背UML而是亲手重写一遍Java的IO流嵌套。挑一个文件传输任务分别用FileInputStream、BufferedInputStream、DataInputStream、GZIPInputStream做多层嵌套然后在关键处打上断点观察调用栈如何一层层扩散。这个练习做完装饰模式从概念到血肉就都通了。如果你要在项目里引入装饰模式我还有一个实用建议先从包装外部SDK开始。比如你项目里引入了一个第三方消息推送SDK接口已经固定不要直接散落在业务代码里。用一个装饰器先加上日志再套一个装饰器做失败重试再套一个做降级开关。你会发现装饰模式在不改动第三方代码却能优雅附加横切能力这件事上几乎没有替代品。