大概三年前的某个深夜我盯着项目里那十七个以Notify开头的类第一次认真琢磨装饰器模式Decorator Pattern到底能救多少代码。当时那是一个消息通知模块需求方从“先发个短信就行”一路加码到“短信邮件站内信都要、要记录日志、要失败重试、要延迟到上班时间再发”短短三周类越来越多改一个逻辑要牵连七八个文件。后来我用装饰器模式把整块重写类数量直接砍掉一半后面再加新增强一行旧代码都不用动。这篇文章就把我从踩坑到用顺手的全过程记录下来适合那些正在被“组合爆炸”折磨、或者刚接触装饰器模式想搞懂它到底怎么用的同学。1. 先还原那个让我决定重写的需求现场1.1 三周内需求变了四次的“简单通知功能”故事起点特别朴素运营那边说“用户下单后发一条短信通知就行”。我当时的实现也特别朴素——一个SmsNotify类一个send()方法里面拼好文案调短信服务商的接口前后十分钟搞定。结果第二周需求就来了“短信之外还要发邮件和站内信。” OK这也不算难我抽出接口MessageSender让短信、邮件、站内信三个实现类各自干活调用方根据场景选择渠道。到这里为止代码还挺干净。第三周开始不对劲了。先是“每次发送都要把时间、渠道、结果记录下来方便排查”过了两天又说“部分用户收到的通知内容要脱敏不能明文存日志”再然后“晚上下单的邮件别半夜打扰用户延迟到第二天上午9点再发”最后是“发送失败要自动重试三次”。每一个需求单独拿出来都不难但合在一起事情就变味了。1.2 继承方案在第几次改动后彻底失控我当时的第一反应是写个子类。SmsNotify要加日志写一个SmsNotifyWithLog。邮件要加日志再写一个EmailNotifyWithLog。短信要日志又要重试那就SmsNotifyWithLogAndRetry。第三周结束时项目里躺着下面这种表基础渠道增强能力组合继承方案需要的类短信 / 邮件 / 站内信日志3 个短信 / 邮件 / 站内信日志 重试3 个短信 / 邮件 / 站内信日志 加密3 个短信 / 邮件 / 站内信日志 重试 加密 延迟3 个表面看只有十几个类但继续往下想就害怕了如果基础渠道数量是 m可选增强能力数量是 n用继承实现所有组合最坏情况要写m × 2ⁿ个类。3 个渠道、4 种增强就是 3 × 16 48 个类而且假设你后面又加了第 5 种增强“限流”类数量直接翻倍到 96 个。更难受的不是数量是改代码。日志逻辑要从短信类复制到邮件类再复制到站内信类每次改日志格式要全局搜索替换漏一个就是线上事故。继承在这个场景下的问题不是“不够用”而是“结构性不可维护”——增强能力和具体实体强耦合加一种新增强等于把所有相关渠道类都动一遍这恰恰违反了开闭原则。1.3 第一版重写把“增强”从“实体”里拆出来熬夜重构那晚我想明白了一件事短信、邮件、站内信这些“实体”其实很稳定会变的是“增强行为”——日志、加密、延迟、重试。那为什么不反过来设计实体保持单纯增强行为独立成类然后用“一层包一层”的方式叠加。这就是装饰器模式最朴素的思想我不修改短信发送这个动作本身而是给它外面套一个“日志外壳”再套一个“重试外壳”套完之后对外仍然是一个MessageSender调用方完全感知不到里面的结构变化。当时我还没意识到这个“套壳后类型不变”的特性是整个模式最精妙的地方也是后面所有坑的根源。带着这个思路我正式梳理了装饰器模式的标准结构。2. 装饰器模式的四梁八柱角色、结构与代码骨架2.1 四个角色的职责分配装饰器模式一共四个角色少一个都不完整抽象构件Component定义业务接口是所有参与者的共同契约。在通知例子里就是MessageSender。具体构件ConcreteComponent真正干活的类也就是被装饰的原始对象如SmsSender。抽象装饰器Decorator持有一个 Component 类型的引用并实现和 Component 相同的接口。它存在的意义是让“装饰器”这个类型能和“构件”互相替换。具体装饰器ConcreteDecorator在转发调用给内部 target 的前后插入自己的增强逻辑如LoggingDecorator、RetryDecorator。这四个角色最关键的是第三和第四个抽象装饰器保证了“接口不变”具体装饰器保证了“行为增强”。两者缺一不可少了抽象装饰器调用方就得区分“外面这层是装饰器还是本体”类型透明就没了。2.2 先跑通最小骨架附完整 Java 代码用通知场景写一个最小可运行的骨架代码量不大但它把四个角色的关系展示得很清楚。// 抽象构件定义稳定接口 public interface MessageSender { void send(String message); } // 具体构件真正发短信的类 public class SmsSender implements MessageSender { Override public void send(String message) { System.out.println([短信渠道] 发送: message); } } // 抽象装饰器持有一个 MessageSender 引用接口与构件一致 public abstract class MessageSenderDecorator implements MessageSender { protected MessageSender target; public MessageSenderDecorator(MessageSender target) { this.target target; } Override public void send(String message) { target.send(message); } } // 具体装饰器 A日志 public class LoggingDecorator extends MessageSenderDecorator { public LoggingDecorator(MessageSender target) { super(target); } Override public void send(String message) { System.out.println([日志] LocalDateTime.now() 开始发送); target.send(message); System.out.println([日志] 发送结束); } } // 具体装饰器 B加密 public class EncryptDecorator extends MessageSenderDecorator { public EncryptDecorator(MessageSender target) { super(target); } Override public void send(String message) { String encrypted 【密文】 message 【END】; target.send(encrypted); } }使用的时候就是套娃式构造MessageSender sender new LoggingDecorator(new EncryptDecorator(new SmsSender())); sender.send(您好您的验证码是 123456);输出顺序应该是先打日志“开始发送”然后内部把消息加密后交给短信渠道发送再打日志“发送结束”。这里有个值得注意的细节LoggingDecorator在最外层所以“开始发送”最先打印EncryptDecorator在中间所以传给短信渠道的内容已经变成了密文。这个顺序问题后面我会专门讲坑。2.3 递归组合的本质为什么装饰器能无限套娃很多初学者问过我同一个问题LoggingDecorator明明也是MessageSender它内部持有的target也是MessageSender那它调target.send()的时候不还是调了个MessageSender的方法吗到底怎么做到“层层增强”的答案是装饰器组合本质就是一个递归调用链。每个装饰器的send()方法里target.send(message)这一行调用会在运行期被动态分发到它下一个内层对象的方法上。你从最外层调用send()它会先执行自己的前置逻辑然后调用内一层的send()内一层再执行自己的前置逻辑……直到最里面真正的SmsSender.send()执行完控制权再一层层往回返执行各层的前置后置逻辑。这个结构就像快递包裹最外层纸箱拆开里面还有个盒子盒子里还有气泡膜气泡膜里才是真东西。你拿到的始终是个“包裹”要经过一层层拆解才触达核心。装饰器模式允许你在外面无限套壳只要每一层都遵循同一个接口套多少层都不影响调用方。3. 实战把通知功能升级成“全能外挂”3.1 需求清单日志、加密、延迟、重试光看骨架不过瘾我把当时真实落地的四个装饰器逐一写出来。先列需求日志记录每次发送的时间、内容摘要、发送结果。加密对敏感通知内容做加密处理防止日志泄露明文。延迟支持延迟到指定时间再发送比如晚上下单的邮件推到次日 9 点。重试发送失败时自动重试最多 3 次间隔 2 秒。这四个增强之间彼此独立顺序不同效果不同正好适合用装饰器来组织。3.2 四个具体装饰器的实现要点日志装饰器比较简单重点说一下加密、延迟和重试这三个有“状态”的装饰器。public class RetryDecorator extends MessageSenderDecorator { private final int maxAttempts; private final long intervalMillis; public RetryDecorator(MessageSender target, int maxAttempts, long intervalMillis) { super(target); this.maxAttempts maxAttempts; this.intervalMillis intervalMillis; } Override public void send(String message) { for (int attempt 1; attempt maxAttempts; attempt) { try { target.send(message); return; } catch (Exception e) { if (attempt maxAttempts) { throw e; // 最后一次失败直接抛出 } System.out.println([重试] 第 attempt 次失败 intervalMillis ms 后重试); try { Thread.sleep(intervalMillis); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException(重试被中断, ie); } } } } }这里有个容易忽略的细节重试装饰器只在target.send()抛出异常时才重试。如果底层渠道发送失败但不抛异常、只返回 false那return直接执行了重试逻辑根本不会触发。所以使用重试装饰器时一定要确认底层构件的失败语义是“异常抛出”还是“返回值标记”否则装饰器形同虚设。加密装饰器也一样必须在构造时传入密钥而且加密逻辑要尽量薄不要把加解密算法写死在装饰器里public class EncryptDecorator extends MessageSenderDecorator { private final String secretKey; public EncryptDecorator(MessageSender target, String secretKey) { super(target); this.secretKey secretKey; } Override public void send(String message) { String encrypted encrypt(message, secretKey); target.send(encrypted); } private String encrypt(String plaintext, String key) { // 实际项目中这里调用 AES 等加密工具类 // 为了演示只做简单 Base64 编码 return Base64.getEncoder().encodeToString(plaintext.getBytes()); } }延迟装饰器是最容易写错的一个。如果直接用Thread.sleep()阻塞当前线程会把调用方线程也卡住。我当时在项目里用的是ScheduledExecutorService让消息调度到延迟时间之后再真正发送public class DelayDecorator extends MessageSenderDecorator { private final ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); private final Duration delay; public DelayDecorator(MessageSender target, Duration delay) { super(target); this.delay delay; } Override public void send(String message) { scheduler.schedule(() - target.send(message), delay.toMillis(), TimeUnit.MILLISECONDS); } }延迟装饰器改变了方法的同步语义——调用send()会立即返回实际发送发生在调度线程里。这意味着外层如果有日志装饰器日志里“发送结束”会先于真实发送发生日志顺序就和实际执行顺序不一致了。解决办法是把延迟装饰器放在最内层让它只延迟“真实发送”不延迟增强逻辑的编排。3.3 用静态工厂把装饰链收口四个装饰器都写好了如果你让业务代码自己new装饰链用不了几天就会乱套——有人从头套到尾有人从尾套到头结果千奇百怪。我当时的做法是提供一个通知工厂把常用的装饰链组合沉淀成方法public class NotificationFactory { public static MessageSender createSmsWithFullFeatures(String secretKey) { MessageSender sender new SmsSender(); sender new LoggingDecorator(sender); sender new EncryptDecorator(sender, secretKey); sender new DelayDecorator(sender, Duration.ofHours(8)); sender new RetryDecorator(sender, 3, 2000); return sender; } public static MessageSender createEmailWithLoggingAndRetry() { MessageSender sender new EmailSender(); sender new LoggingDecorator(sender); sender new RetryDecorator(sender, 3, 2000); return sender; } }工厂方法把“装饰链如何组装”这件事集中管理起来业务方只需要调用NotificationFactory.createSmsWithFullFeatures(key)拿到一个MessageSender不知道也不关心里面套了几层。后续新增增强能力也不改工厂方法的签名只改内部组装逻辑。3.4 验证一次调用整条链路如何协同组装好的链子是RetryDecorator - DelayDecorator - EncryptDecorator - LoggingDecorator - SmsSender从外到里一层层包。调用send(验证码 123456)后执行流是这样的RetryDecorator进入循环尝试调下一层。DelayDecorator把任务提交给调度线程主线程立即返回。调度线程到点后调用EncryptDecorator把“验证码 123456”加密成密文。LoggingDecorator打印“开始发送”记录的是密文不是明文符合脱敏需求。SmsSender真正把密文发出去。如果SmsSender抛异常异常沿调用链冒泡回RetryDecorator触发重试整个过程再来一遍。这个例子也说明了一个重要原则装饰器链的顺序就是业务的执行顺序。你把加密放在日志外面日志记的就是密文把加密放在日志里面日志记的就是明文。需求是“日志不许出现明文”那就必须让EncryptDecorator包在LoggingDecorator外面。这也是为什么我强烈建议用工厂方法收口装饰链——一旦某个环境搞错了顺序排查起来非常痛苦。4. 你其实早就在用框架与源码里的装饰器身影4.1 Java I/O 流最经典的教科书案例很多设计模式的文章讲装饰器都以 Java I/O 流为例因为这确实是标准库里最大规模的装饰器应用。看这行代码BufferedReader reader new BufferedReader( new InputStreamReader( new FileInputStream(test.txt), StandardCharsets.UTF_8));FileInputStream是具体构件BufferedReader是典型的具体装饰器——它给字符流增加了“缓冲”能力但对外仍然是Reader类型。你可以继续往上套LineNumberReader或者换一个构件套在StringReader上BufferedReader 内部不用改一行代码。I/O 流家族里FilterInputStream的所有子类都是装饰器BufferedInputStream加缓冲DataInputStream加基本类型读取能力PushbackInputStream加回退读取能力。你可以任意组合它们这正是装饰器模式在真实项目里存活了几十年的证明。4.2 Python 的 语法背后发生了什么Python 的装饰器语法是装饰器模式的一种语言级实现但它加了语法糖之后很多人反而没意识到这玩意儿和 Java 手写装饰器是同一个思想。import functools import time def timing(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) print(f{func.__name__} 耗时 {time.perf_counter() - start:.4f}s) return result return wrapper timing def send_message(text): time.sleep(1) print(f发送: {text})timing等同于send_message timing(send_message)本质就是把原函数包进一个外壳函数外壳保留了原函数的签名和行为再附加计时逻辑。这里有个新手必踩的坑如果不加functools.wraps(func)装饰后的函数__name__会变成wrapper依赖函数名做日志或路由判断的地方会出诡异问题。这和 Java 装饰器导致getClass()返回装饰器类名是同一个坑——装饰会掩盖身份信息需要显式保留。4.3 Web 中间件与 Spring AOP改名换姓的装饰器如果你写过 Koa 或 Express应该对下面这种中间件链不陌生app.use(loggingMiddleware); app.use(authMiddleware); app.use(rateLimitMiddleware); app.use(handler);每个中间件接收(req, res, next)在调用next()前后执行自己的逻辑请求一层层穿透中间件到达真正的业务处理函数。这就是装饰器模式在 Web 框架里的变体——通道换成了next回调增强方式依然是“前置逻辑 转发 后置逻辑”。Spring AOP 里的Transactional、Cacheable本质上也是装饰器思想的实现只不过它用动态代理在运行时生成代理对象不需要手动 new 每个装饰器。但底层逻辑一样目标方法被包装事务、缓存这些横切逻辑在目标方法前后执行。理解了装饰器模式再看 Spring AOP 的拦截器设计会顺畅很多。5. 选型对比装饰器、继承、代理、适配器到底怎么挑5.1 装饰器 vs 继承类爆炸问题的数学解释前面提到过继承实现组合需求最坏需要m × 2ⁿ个类m 是基础构件数n 是增强能力数。用装饰器模式需要的类数量是m n 1m 个具体构件、n 个具体装饰器、1 个抽象装饰器因为组合是在运行期动态完成的不需要为每一种组合创建类。两者还有一个本质差异继承是静态编译期绑定装饰器是动态运行期组合。继承的关系在编译完成时就锁死了一个类只能是某一个组合的结果装饰器可以在运行时自由选择套哪几层、以什么顺序套甚至可以给同一个对象套不同的壳。所以我的选型建议是增强能力少且固定用继承无所谓增强能力多且自由组合优先装饰器增强能力会在运行期动态变化只能装饰器。5.2 装饰器 vs 代理看起来像本质不同这两个是最容易被混为一谈的因为代码结构几乎一样——都是持有一个被包装对象的引用都对方法调用做了拦截。我区分它们的标准是看“意图”维度装饰器代理核心意图给对象增加新行为控制对对象的访问对象来源由外部传入装饰器不负责创建代理通常自己创建或查找被代理对象接口保持必须保持接口不变类型透明可以改变接口也可以做访问控制典型例子Java IO 装饰流Spring AOP、RPC 远程代理、懒加载代理举一个最能说明问题的例子权限代理可以在调用真实方法前检查当前用户有没有权限没有就直接拒绝甚至不调用真实方法装饰器不会拒绝调用它只会给调用“加料”。如果某个类描述的是“增强”那大概率是装饰器如果描述的是“控制”那就是代理。5.3 装饰器 vs 适配器一个加法、一个翻译适配器模式核心是“转换接口”把 A 类型的接口转换成 B 类型让原本不兼容的类能协作。装饰器是“保持接口不变”在同一个接口之下增加功能。生活中的例子更好理解适配器是电源转换头你把欧标插头插进国标插座需要转换头插头型号变了装饰器是手机壳你给手机套个防摔壳充电口还是那个充电口接口没变只是抗摔能力增强了。在代码里如果一个类实现了某个新接口、把旧接口的方法“翻译”成新接口的调用那是适配器如果一个类实现的是和原对象完全相同的接口那才是装饰器。6. 踩坑实录装饰器模式最容易翻车的六个场景6.1 装饰顺序不对结果完全相反我在实战里被坑得最狠的一次就是顺序问题。加密装饰器和日志装饰器的顺序直接决定了日志里记录的是明文还是密文重试装饰器和延迟装饰器的顺序直接决定了“失败后是立刻重试还是再延迟一次”。一旦线上环境把顺序搞反轻则日志泄露敏感信息重则重试风暴打垮下游服务。我总结了一个判断方法从外到内看谁先拿到消息谁就最先处理消息。最外层的装饰器对消息有第一处置权最内层的构件是最后处理消息的。你希望“先加密再记日志”就把加密放在日志外层你希望“日志记录原始内容加密只影响实际发送”就把日志放在加密外层。这种决策必须在设计阶段就确定下来并且用工厂方法固定禁止业务方自行拼装。6.2 依赖具体类型的调用方会失效装饰器模式要成立前提是调用方只面向抽象构件编程。但现实里总有代码会写出这种行为// 这是反面教材 public void sendWithFallback(MessageSender sender) { if (sender instanceof SmsSender) { // 单独处理短信渠道的特殊逻辑 } sender.send(test); }一旦sender被LoggingDecorator或RetryDecorator包过instanceof SmsSender就变成 false这段特殊逻辑会直接失效。同理equals()、hashCode()、List.contains()这些基于对象身份的判断都会出问题。解决办法有两个层面。设计层面尽量保证业务逻辑不依赖具体类型架构层面如果确实需要访问“被装饰对象的特定能力”可以考虑在抽象构件接口里暴露这些能力或者用一个unwrap方法手动解套。但解套本身就是危险的信号说明你的抽象可能设计得不够完整。6.3 深链路的调试与可读性之痛装饰器套到四五层之后日志和堆栈会变得很难看。异常堆栈里是一长串at xxxDecorator.send(xxxDecorator.java:12)从下往上一层接一层没有经验的同事看着就头大。我项目里甚至出现过套了七层的“装饰塔”排查一个发送问题花了两个小时。我的应对策略有两个。一是给每个装饰器写清晰的 toString打印出它的类型和内部 target 的类型这样日志里一条 toString 就能看出整条装饰链的结构。二是在工厂方法里限制装饰链的最大层数超过五层就停下来问自己是不是该重新设计了装饰器模式的威力在于灵活组合但灵活不代表无限制堆叠。6.4 过度装饰什么时候不要用装饰器装饰器模式很优雅但它不是银弹。我见过有人给一个只有两种渠道、一种增强的项目硬套装饰器类没少写代码反而难读。判断标准很简单增强能力少于两个且基本不变 → 直接在实现类里加逻辑别用装饰器。增强能力之间强依赖比如“加密必须在日志之前且两者永远一起出现” → 与其用两个装饰器不如合并成一个或者直接在业务里写清楚。增强逻辑和业务逻辑强耦合拆不掉 → 装饰器反而制造了人为的割裂感。记住 YAGNI 原则不要为还不需要的灵活性付代价。一个项目的代码量和它的灵活性需求应该是匹配的过度设计比不够设计更恶心因为不够设计至少好懂。6.5 并发与状态装饰器不是无状态的初学者最容易忽略的是装饰器的状态问题。比如你在日志装饰器里加了一个AtomicInteger counter统计发送次数这个装饰器对象会被多个线程共享counter 是线程安全的但如果换成普通的int计数并发场景下数据就乱了。还有延迟装饰器里的ScheduledExecutorService它内部有线程池资源不能每次 new 一个否则线程资源会泄漏。我在项目里约定装饰器尽量保持无状态或只持有不可变配置和线程安全的组件。凡是需要可变状态的要么抽出去要么用并发安全的数据结构。这个约定让装饰器在并发环境下的行为可预测排查问题时不用先怀疑装饰器内部状态错乱。6.6 更优雅的替代方案什么时候换别的路最后说两句实在话。装饰器模式解决的是“动态叠加增强”的问题但如果你发现自己在不断写装饰器可能说明你的问题本质上不是“增强”而是“策略选择”——这时候策略模式加工厂可能更清晰如果你发现不同增强之间有复杂的依赖和交互可能pipeline模式更适合你如果你需要的是在运行期频繁修改组合策略那不妨考虑规则引擎或配置化方案。我个人的经验是装饰器模式最适合的依然是横切关注点——日志、监控、重试、限流、缓存这一类与业务逻辑正交的能力。一旦装饰器开始掺和业务判断就该停下来重新思考了。最后分享一个实战小技巧我后来在项目里给装饰器统一加了一个getInnerTarget()方法用来返回内部持有的target对象专门用于单元测试里断言装饰链的结构。测试某个工厂方法是否按预期顺序组装装饰器时不需要真的发一次消息只需要解套验证每一层是不是期望的类型。这个小工具帮我省下了无数和“顺序配错”搏斗的时间。如果你也正在用装饰器模式重构一个越改越乱的模块多用几层装饰不可怕可怕的是顺序乱、类型依赖、状态失控——这三关过了装饰器就是一把趁手的好刀。