
装饰器模式这个名字凡是学过设计模式的都应该不陌生。但说句实在话我见过太多人把它和“继承”、“代理模式”搞混或者只是背了个“动态地给对象添加职责”的定义真到项目里该用的时候完全想不起来。我自己早年也踩过这个坑在一个报表导出功能里硬生生用继承堆了七八个子类后来改需求改到想骂人回头重构才真正体会到装饰器的妙处。这篇就把装饰器模式掰开揉碎讲清楚它到底解决什么问题、类图结构怎么理解、Java和C分别怎么写、JDK源码里哪里在用、以及写大作业或者做项目时怎么避坑。不管你是准备面试、写课程设计还是想优化手头的老代码这篇都能给你点实在的东西。1. 内容整体设计与思路拆解1.1 装饰器模式到底解决什么问题先聊一个最基础的问题为什么需要装饰器模式假设你有个奶茶店的点单系统核心对象是MilkTea现在要支持加珍珠、加椰果、加布丁、加奶盖每种配料都要算钱。新手最容易想到的做法就是继承PearlMilkTea、CoconutMilkTea、PuddingMilkTea、CheeseMilkTea……这还没完如果顾客要“珍珠椰果”呢再建一个PearlCoconutMilkTea“珍珠椰果布丁”呢这么搞下去类的数量会爆炸式增长。组合数学告诉我们给n种配料做排列组合理论上需要2的n次方个子类。4种配料就要16个类10种配料就是1024个类这谁顶得住。继承在这里暴露出的问题有两个类爆炸每增加一种组合方式就要新增一个类代码维护成本指数上升。静态绑定继承关系在编译期就确定了运行时没法动态改变对象的行为。装饰器模式的核心思路就是“用组合代替继承”。把每个配料做成一个包装类它们都实现同一个接口内部持有那个接口类型的引用。想加什么配料就一层一层把对象包起来。这种“俄罗斯套娃”式的结构在运行时可以任意组合自由度极高。用生活化的类比来解释你会更容易理解你买了一部裸机手机核心对象然后给它套上手机壳装饰器A又贴了张钢化膜装饰器B还挂了根挂绳装饰器C。手机还是那个手机但你通过“包装”给它扩展了防摔、防刮、便携等能力。哪天不想用了摘掉壳、撕掉膜手机本身毫发无损。这就是装饰器模式最核心的思想不改动原有代码就能给对象扩展功能而且可以通过多种装饰器的排列组合创造出无穷无尽的行为。1.2 为什么要用装饰器而不是继承这个问题我在面试别人的时候必问因为它是理解装饰器模式的关键分水岭。继承是“is-a”关系装饰器是“has-a”关系。你说PearlMilkTea是MilkTea的一种语义上确实站得住脚但这种“是”的关系在需求变化时特别脆弱。比如“加珍珠”这个行为不是每个奶茶店的珍珠都一样也不是每个顾客加的珍珠量都一样用继承表达这种细节差异非常痛苦。装饰器模式解决的场景非常明确需要透明且动态地扩展功能用户可能在运行时决定要加什么配料而不是在编写代码时就定死。需要避免类爆炸当功能组合数量庞大时装饰器用少量类就能覆盖所有组合。需要遵循开闭原则对扩展开放对修改关闭。加一个新配料不需要动原有的MilkTea类和已有的装饰器类只需要新增一个装饰器类。还有一个容易忽略的优势装饰器可以解决“子类行为不同维度变化”的问题。比如一个文本编辑器既要处理“加粗”、“斜体”、“下划线”又要处理“加密”、“压缩”。这两个维度分别有3种和2种方案用继承需要写6个子类但用装饰器只需要5个类1个基础类2个维度各2个装饰器而且后续各自扩展互不影响。当然装饰器模式也不是万能的。如果对象本身非常复杂内部状态很多或者装饰器需要访问对象的内部结构那装饰器模式会让代码变得晦涩难懂。而且装饰器会增加很多小类初看代码时会觉得“怎么这么多类”有一定的理解门槛。这点我在后面“常见问题”部分会详细展开。2. 装饰器模式核心结构与应用场景分析2.1 GoF类图与角色拆解先看看经典的四角色结构这个在面试和考试里是必考的角色名字职责抽象组件Component定义对象的核心接口可以是抽象类或接口具体组件ConcreteComponent被装饰的原始对象实现核心业务逻辑抽象装饰器Decorator持有Component引用实现Component接口把请求转发给被装饰对象具体装饰器ConcreteDecorator在转发请求前后执行额外的逻辑这个结构里最关键的奥妙在Decorator这个抽象类public abstract class Decorator implements Component { protected Component component; public Decorator(Component component) { this.component component; } Override public void operation() { // 转发请求给被包装的组件 component.operation(); } }看到没有Decorator本身也是Component的实现类同时内部又持有Component引用。这就是装饰器模式“既是接口又包装接口”的双重身份。正因为它自己也是一个Component才能实现“嵌套包装”——一个装饰器外面可以再套一个装饰器套多少层都行。ConcreteDecorator做的事情是在operation()方法里“先做事、再转发”或者“先转发、再做事”甚至可以“截断转发”public class ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component component) { super(component); } Override public void operation() { // 装饰逻辑A前置增强 System.out.println(ConcreteDecoratorA: before operation); // 转发给原组件 super.operation(); // 装饰逻辑A后置增强 System.out.println(ConcreteDecoratorA: after operation); } }这种“递归调用”就是装饰器模式执行流程的核心。一个装饰器嵌套一个装饰器最终形成一个调用链最外层装饰器的operation()- 中间层装饰器的operation()- ... - 最内层原始组件的operation()。2.2 装饰器模式适合处理什么现实场景除了奶茶加料装饰器模式的典型应用场景非常广泛Java I/O 家族BufferedInputStream装饰FileInputStreamDataInputStream又装饰BufferedInputStream。你写的new DataInputStream(new BufferedInputStream(new FileInputStream(a.txt)))就是教科书级的装饰器链。权限校验一个Service接口用AuthDecorator包装后在执行核心逻辑之前先校验登录态。日志监控用LogDecorator包装核心服务自动记录方法调用参数、执行时间和异常信息。缓存处理用CacheDecorator包装数据查询服务命中缓存直接返回没命中才转发给原组件。Java集合框架Collections.synchronizedList(new ArrayList())给非线程安全的ArrayList添加同步能力不需要修改ArrayList本身。这个模式的价值在于它把“核心业务”和“边缘功能”解耦了。核心业务只关心自己该做的事日志、缓存、权限、加料这些边缘功能通过装饰器一层层叠加互不干扰而且可插拔。2.3 什么时候不该用装饰器模式聊完了适用场景再说说哪些情况用了反而添乱。对象本身过于复杂时不要用。假如被装饰的对象有几十个方法装饰器需要转发其中大部分方法那这个装饰器类会异常臃肿不如直接用继承或者代理。装饰器的数量不可控时慎用。如果一个接口有十几种装饰器嵌套顺序不同会导致行为差别巨大调试时排查嵌套层级会非常痛苦。这种场景下可以考虑用建造者模式Builder来管理装饰器的组装顺序。工厂模式和装饰器模式经常配合使用。单独丢给客户端一堆装饰器类客户端要自己拼装API会很难看。更好的做法是用工厂方法封装装饰链的创建逻辑。比如public class MilkTeaFactory { public static Component createFullTea() { return new PearlDecorator( new CoconutDecorator( new CheeseDecorator(new PlainMilkTea()))); } }这样客户端只需要调用MilkTeaFactory.createFullTea()不用关心内部怎么装饰。3. 装饰器模式的标准实现与步骤详解3.1 Java实现从零手写一个装饰器为了把流程讲透我用一个“短信发送器”作为例子基础版是直接发送短信装饰器负责加上“防骚扰过滤”和“发送日志”。第1步定义抽象组件接口public interface SmsSender { void send(String phone, String content); }第2步创建具体组件被装饰者public class BasicSmsSender implements SmsSender { Override public void send(String phone, String content) { // 模拟调用短信服务商的API System.out.println([基础短信] 发送到 phone : content); } }第3步创建抽象装饰器public abstract class SmsSenderDecorator implements SmsSender { protected SmsSender wrapperTarget; public SmsSenderDecorator(SmsSender target) { this.wrapperTarget target; } Override public void send(String phone, String content) { // 默认转发子类可以选择在转发前后添加逻辑 wrapperTarget.send(phone, content); } }第4步实现具体装饰器public class FilterDecorator extends SmsSenderDecorator { private ListString blackList Arrays.asList(spam123, fraud456); public FilterDecorator(SmsSender target) { super(target); } Override public void send(String phone, String content) { if (blackList.contains(phone)) { System.out.println([过滤装饰器] 拦截黑名单号码: phone); return; // 注意这里直接短路不转发 } System.out.println([过滤装饰器] 号码检查通过); super.send(phone, content); } } public class LogDecorator extends SmsSenderDecorator { public LogDecorator(SmsSender target) { super(target); } Override public void send(String phone, String content) { System.out.println([日志装饰器] LocalDateTime.now() 准备发送短信); super.send(phone, content); System.out.println([日志装饰器] 短信发送完成); } }第5步客户端组装装饰链public class Client { public static void main(String[] args) { // 基础发送器 SmsSender sender new BasicSmsSender(); // 需要过滤日志就包两层 sender new FilterDecorator(sender); sender new LogDecorator(sender); // 执行操作 sender.send(13800138000, 你好这是一条测试短信); sender.send(spam123, 这是一条垃圾短信); } }执行结果[日志装饰器] 2024-01-01T10:30:00.123 准备发送短信 [过滤装饰器] 号码检查通过 [基础短信] 发送到 13800138000: 你好这是一条测试短信 [日志装饰器] 短信发送完成 [日志装饰器] 2024-01-01T10:30:00.456 准备发送短信 [过滤装饰器] 拦截黑名单号码: spam123 [日志装饰器] 短信发送完成注意观察第二个结果日志装饰器依然打印了“短信发送完成”因为它的转发逻辑是before - target.send() - after而target.send()内部被FilterDecorator截断了但对于外层来说调用已经返回了。这种“洋葱模型”的行为是理解装饰器模式的关键一定要在脑子里把调用栈理清楚。3.2 执行顺序背后的递归逻辑为什么日志装饰器虽然实际没有发出短信但还是打了“发送完成”的日志这是因为装饰器模式的执行本质上是递归调用。当你调用最外层装饰器的send()时LogDecorator.send() // 打印准备发送 - FilterDecorator.send() // 检查黑名单拦截后return - BasicSmsSender.send() // 不会执行 - FilterDecorator.send() 返回 - LogDecorator.send() 打印发送完成FilterDecorator里的return只是终止了它的转发但LogDecorator并不知道内部发生了什么它的after逻辑照常执行。如果希望“一旦过滤拦截连外层日志也标记失败”就需要用特殊的返回值或异常来传递状态。这也是装饰器模式的一个开发经验装饰器之间是紧耦合的关系前后顺序决定了行为的差异内层的异常/短路状态需要通过返回值或异常机制向上传递。3.3 C实现要点与差异分析C写装饰器模式跟Java有个显著差异内存管理。Java有垃圾回收不用操心对象的生命周期C需要手动管理指针尤其是delete谁这个问题稍不注意就是内存泄漏或者悬垂指针。基础接口和具体组件#include iostream #include memory #include string // 抽象组件 class SmsSender { public: virtual ~SmsSender() default; virtual void send(const std::string phone, const std::string content) 0; }; // 具体组件 class BasicSmsSender : public SmsSender { public: void send(const std::string phone, const std::string content) override { std::cout [基础短信] 发送到 phone : content std::endl; } };抽象装饰器class SmsSenderDecorator : public SmsSender { protected: std::shared_ptrSmsSender wrapperTarget; public: explicit SmsSenderDecorator(std::shared_ptrSmsSender target) : wrapperTarget(std::move(target)) {} void send(const std::string phone, const std::string content) override { if (wrapperTarget) { wrapperTarget-send(phone, content); } } };具体装饰器和客户端组装class FilterDecorator : public SmsSenderDecorator { public: explicit FilterDecorator(std::shared_ptrSmsSender target) : SmsSenderDecorator(std::move(target)) {} void send(const std::string phone, const std::string content) override { if (phone spam123) { std::cout [过滤装饰器] 拦截黑名单号码: phone std::endl; return; } std::cout [过滤装饰器] 号码检查通过 std::endl; SmsSenderDecorator::send(phone, content); } }; class LogDecorator : public SmsSenderDecorator { public: explicit LogDecorator(std::shared_ptrSmsSender target) : SmsSenderDecorator(std::move(target)) {} void send(const std::string phone, const std::string content) override { std::cout [日志装饰器] 准备发送短信 std::endl; SmsSenderDecorator::send(phone, content); std::cout [日志装饰器] 短信发送完成 std::endl; } }; int main() { std::shared_ptrSmsSender sender std::make_sharedBasicSmsSender(); sender std::make_sharedFilterDecorator(sender); sender std::make_sharedLogDecorator(sender); sender-send(13800138000, 你好); sender-send(spam123, 垃圾短信); return 0; }这里强烈建议使用std::shared_ptr而不是裸指针。原因有两点避免手动delete的繁琐和风险装饰链可能嵌套多层如果手动delete你要记住每一层指针的释放顺序稍有不慎就泄漏。支持共享所有权如果同一个被装饰对象被多个装饰器共享某些场景下会这样用shared_ptr能正确管理引用计数。还有一个细节析构函数要声明为虚函数。SmsSender的析构函数必须是virtual的否则通过基类指针删除派生类对象时行为是未定义的。这在C多态编程里是老生常谈但确实很多人会漏。3.4 使用lambda和函数式接口简化装饰器Java进阶我自己的实践经验是在Java 8环境里如果装饰器的逻辑很简单可以用lambda和Function接口来简化避免创建一堆类。FunctionString, String base input - input; FunctionString, String withTimestamp base.andThen( msg - [时间戳] LocalDateTime.now() msg); FunctionString, String withSignature withTimestamp.andThen( msg - msg [签名] 来自Java技术博主); FunctionString, String withEncryption withSignature.andThen( msg - new StringBuilder(msg).reverse().toString()); String result withEncryption.apply(你好这是一条测试短信);这种方式没有“类”的概念纯粹用函数组合实现装饰逻辑适合装饰逻辑简单、不需要长时间维护的场景。但如果你需要装饰器有状态比如保存调用次数或者装饰逻辑很复杂还是老老实实写类比较好。4. 装饰器模式在源码中的实际应用解析4.1 JDK中那些“隐藏”的装饰器JDK里用到装饰器模式的地方太多了我挑几个最能说明问题的1. Java I/O 流这是最经典、最常被引用的例子BufferedReader reader new BufferedReader( new InputStreamReader( new FileInputStream(data.txt), StandardCharsets.UTF_8));BufferedReader装饰了InputStreamReaderInputStreamReader又装饰了FileInputStream。BufferedReader增加了缓冲功能InputStreamReader把字节流转成字符流FileInputStream负责读取字节。每一层都是独立的功能模块通过装饰器链组合起来。如果你翻开源码会发现FilterInputStream就是那个抽象装饰器它内部持有InputStream in引用read()方法就是把请求转发给in.read()。而BufferedInputStream、DataInputStream、PushbackInputStream都是具体装饰器。2. Collections.synchronizedListListString list Collections.synchronizedList(new ArrayList());SynchronizedCollection类就是装饰器它内部持有Collection的引用在add、remove等方法上都加了synchronized关键字。你传入一个非线程安全的ArrayList返回一个线程安全的包装对象原ArrayList本身的代码一个字节都没改。我写代码时经常遇到一种情况业务代码里拿到的List是别人传进来的想给它加同步能力但又不确定调用方会不会修改原有的List。用Collections.synchronizedList包装一下最省事但一定要记住包装后所有操作都要通过包装对象不能再直接操作原List否则同步就失效了。3. Servlet API中的HttpServletRequestWrapperHttpServletRequestWrapper实现了HttpServletRequest接口内部持有一个request对象所有方法都转发给它。需要修改请求参数时只需要继承HttpServletRequestWrapper重写相关方法即可不用把HttpServletRequest的所有方法都实现一遍。这就是装饰器模式在大规模企业级框架中的典型应用。4.2 和代理模式的深度对比聊到这里有必要认真讲一下装饰器模式和代理模式的区别这是我见过最容易被混淆的两个模式。两者的类结构长得极像都是持有一个目标对象都在调用目标方法前后做增强。但两者的意图完全不同对比项装饰器模式代理模式核心目的增强功能添加新的职责控制访问管理生命周期对象创建客户端组装可以多层嵌套代理类内部创建或获取目标对象客户端感知客户端知道自己在组装装饰器客户端通常无感知直接面向接口是否改变接口不改变仍然是Component接口不改变仍然是目标接口典型场景IO流、日志、缓存装饰远程调用、延迟加载、访问控制举一个具体的例子缓存装饰器 vs 代理。缓存装饰器的核心逻辑是“如果缓存有直接返回没有调用目标方法并更新缓存”然后在功能上给目标增加了“缓存”这个新能力。代理模式做的是“我要控制对某个对象的访问所以加一层代理在真正访问前做权限判断”。权限判断不是给目标增加功能而是控制谁能访问目标。一句话总结装饰器模式重“功能增强”代理模式重“访问控制”。如果你的目的是加能力用装饰器如果你的目的是控制访问路径用代理。5. 实操过程中的常见问题与排查5.1 装饰器使用中常见的坑与排查方法坑1装饰器顺序理解错误导致行为异常茶杯例子如果在FilterDecorator外部又在LogDecorator里记录“发送完成”就会出现“明明没发出去却记录了已完成”的bug。排查这种问题最有效的方式是画出调用栈图。我的经验是装饰器越“通用”越靠外越“具体”越靠内。比如在短信发送的例子中日志记录是通用的横切逻辑应该放在外层黑名单过滤是具体业务放在内层。顺序反了虽然也能跑但语义会很别扭。坑2装饰器转发遗漏导致功能失效抽象装饰器有几十个方法时子类很容易漏掉转发。比如重写send()时没有调用super.send()整个调用链就断了。这种问题排查时最明显的特征是“部分功能能用部分没反应”。坑3循环装饰装饰器构造函数里传入了自身或者两个装饰器互相引用形成环。这种情况通常不会立即报错但会在调用时出现StackOverflowError。排查时多留意构造参数是否意外引用了自己。坑4过度装饰代码可读性下降你真的见过五六个装饰器叠在一起、每个装饰器代码只有一两行的情况。这种“为模式而模式”的代码后期维护的人会被逼疯。我的建议是如果装饰器的数量超过4个且组装逻辑频繁变化就应该考虑用工厂方法或构建器来统一管理装饰链。坑5C中忘记释放内存针对裸指针实现如果用了裸指针Component*每层装饰器都需要在析构函数里释放持有的指针而且释放顺序要谨慎。我的建议是直接无脑用std::shared_ptr省心省力虽然有一点引用计数的性能开销但换来的是内存安全完全值得。5.2 写课堂作业和Java期末大作业时怎么用装饰器模式如果你现在带着“设计模式大作业”的任务想用装饰器模式当主题我建议你把功能场景做得“丰满但不臃肿”。一个我个人觉得比较出彩的选题是给一个文本处理工具添加多级装饰。具体来说基础对象PlainText实现TextProcessor接口核心方法是String process(String input)。装饰器1UpperDecorator把所有字母转大写。装饰器2TrimDecorator去掉首尾空格。装饰器3ReplaceDecorator把指定敏感词替换成***。装饰器4ReverseDecorator把字符串反转。然后写个测试类演示不同装饰组合的效果比如TextProcessor p1 new ReplaceDecorator(new UpperDecorator(new PlainText())); TextProcessor p2 new ReverseDecorator(new TrimDecorator(new ReplaceDecorator(new PlainText())));报告里可以配一张类图可以用UML工具画把抽象组件、具体组件、抽象装饰器、具体装饰器四类角色标注清楚。重点是在“设计思路”部分写清楚“为什么不用继承”——用一个TextProcessor接口加四个装饰器类就覆盖了至少15种不同的组合如果用继承代码量翻倍都不止。在文档里还可以加一段装饰器链执行顺序的实验数据比如输入 Hello World 经过Upper Replace Trim后的输出是什么、经过Trim Replace Upper后又是什么直观展示顺序对结果的影响。这种细节会让报告很有说服力。5.3 高频面试题与答题框架最后整理几个装饰器模式相关的面试题供大家参考问题1装饰器模式和继承有什么区别答题框架先说继承的痛点类爆炸、静态绑定再说装饰器的解决思路组合替代继承、运行时动态扩展最后提一嘴开闭原则。问题2装饰器模式和代理模式有什么区别答题框架先承认两者结构相似然后从意图、嵌套能力、控制权三个维度对比。核心是“装饰器为了增强代理为了控制”。问题3说几个JDK中使用装饰器模式的例子答题框架Java I/O流BufferedInputStream装饰FileInputStream、Collections.synchronizedList、HttpServletRequestWrapper。问题4装饰器模式有什么缺点答题框架类数量增多需要很多小类、调试复杂多层嵌套难以排查、如果组件接口方法过多装饰器转发代码会很冗长。问题5为什么说装饰器模式遵循开闭原则答题框架新增功能时不需要修改原有代码只需要新增装饰器类同时客户端可以根据需要自由组合。6. 结语与经验之谈装饰器模式是那种“刚开始觉得麻烦用顺了以后真香”的模式。它让我最受益的一点是写业务代码时带着“能不能不改原类就扩展功能”的意识。这不仅仅是代码技巧层面的提升更是设计思维的转变——从“改代码”到“包代码”从“新增子类”到“组合装饰”。我在实战中摸索出几个个人的使用习惯分享给各位参考装饰器的类名一定要能明确表达它的职能比如LogDecorator、CacheDecorator、AuthDecorator不要用什么MyDecorator1、Decorator2这种名字看到名字就能知道它干了什么是重中之重。每层装饰器只做一件事不要试图“顺便”多干点活否则装饰器之间耦合加深组合的意义就没了。优先在项目里使用成熟的第三方装饰器工具比如Java的BufferedInputStream而不是什么都自己造轮子。如果看完这篇还是觉得哪里不太清晰推荐去读读《设计模式可复用面向对象软件的基础》里Decorator那一章GoF的书虽然老但讲得确实透彻。结合本文的代码和梳理多写几个Demo练手很快就能形成自己的理解。学设计模式最忌讳的就是“背定义”一定要动手写、画图推演、对比不同方案。希望这篇文章能帮你把装饰器模式彻底吃透在面试、作业和真实项目中都能游刃有余。