
1. 装饰器模式到底解决什么问题先直接说结论装饰器模式的核心价值就是让你在不修改原有类代码的情况下给对象动态“加料”。这个“料”可以是新功能、新行为、新的状态记录而且加料的方式是层层包裹像穿衣服一样——想穿几件穿几件想脱也随时能脱。我最早接触这个模式是在读《Head First 设计模式》的时候那本书里用咖啡杯举例一杯纯咖啡加牛奶、加糖、加奶泡每一种组合都是一个新价格。当时觉得挺巧妙但真正在工作中用起来才发现这个模式远不止“给对象加功能”这么简单。它其实是在跟继承体系叫板继承是静态的、编译期就定死的复用方式而装饰器是动态的、运行期可以任意组合的复用方式。举个最典型的业务场景订单系统里一个基础订单可能有各种附加项——优惠券、会员折扣、运费险、包装费。你不可能为每一种组合都写一个子类那会让类爆炸。这时候装饰器模式就能派上用场每个附加项都是一个装饰器把订单对象层层包起来每个装饰器只关心自己那部分计算然后调用被包裹对象的方法继续传递。这样新增一种附加项只需要新增一个装饰器类不碰已有的代码。对初学者来说装饰器模式最容易懵逼的点在于“嵌套调用”。一个被装饰的对象外面套了三层装饰器调用一个方法时执行顺序到底是从外到内还是从内到外这其实取决于你写代码时怎么组织后面我会用一个完整的例子把这条调用链捋清楚。2. 先搞清楚装饰器模式的结构别急着写代码2.1 四个角色一句话一个装饰器模式的标准结构里有四个角色记住这四个角色代码怎么变都离不开它们组件接口Component定义核心业务方法的抽象接口。比如“咖啡”这个抽象概念规定它有cost()方法和getDescription()方法。具体组件ConcreteComponent实现组件接口的基础类也就是那个“被装饰的原始对象”。比如“纯咖啡”。装饰器抽象类Decorator实现组件接口同时持有一个组件接口的引用。这个引用是关键它指向被包裹的下层对象。具体装饰器ConcreteDecorator继承装饰器抽象类在调用被包裹对象的方法之前或之后加上自己的逻辑。比如“加牛奶”、“加糖”。这里有一个非常容易被误解的地方装饰器抽象类本身也是组件接口的实现类。这意味着装饰器可以被当成组件来使用所以装饰器套装饰器在类型上是完全合法的。这就是动态扩展的根基。2.2 为什么必须用抽象类直接用实现类不行吗从语法上讲不用抽象类也可以直接让每个装饰器实现组件接口。但使用抽象类有一个实际好处把“必须持有被装饰对象引用”这个约束集中管理。每一个具体装饰器都得有这个引用如果每个装饰器都自己写一遍这个字段和构造器传入逻辑代码冗余不说还容易漏。抽象类把这个公共部分抽出来子类只需要专注自己的增强逻辑。另外接口里如果有多个方法装饰器抽象类可以先把所有方法都“透传”一遍子类只需要重写自己关心的那个方法。否则子类必须实现接口的全部方法哪怕它只关心cost()也得把getDescription()翻译一遍。这在接口方法多的时候会非常烦人。所以装饰器抽象类更像是一个“骨架”用来减少子类的重复劳动。这在Java标准库里也很常见比如InputStream的装饰器基类FilterInputStream它就是典型的一层透传。3. 手写一个订单计费案例把调用链彻底整明白3.1 需求场景设定以电商订单的附加费用计算为例。平台规则如下基础商品订单有一个price值。订单可以加“包装费”固定加 5 元。订单可以加“运费险”按订单金额的 2% 收取服务费。订单可以享受“VIP折扣”打 8 折。这些附加项可以任意组合组合顺序不同最终金额也可能不同比如先打折还是先加运费险。这个场景非常适合装饰器模式因为附加项的组合是无限的。写代码时我们把订单金额的计算封装成getTotal()每层装饰器在调用下层对象之后再叠加自己的计算逻辑。3.2 Java 实现含完整的嵌套调用顺序先定义组件接口public interface Order { double getTotal(); String getDescription(); }具体组件基础订单public class BaseOrder implements Order { private double price; public BaseOrder(double price) { this.price price; } Override public double getTotal() { return price; } Override public String getDescription() { return 基础商品; } }装饰器抽象类关键点是把Order的引用存成字段并且构造器要传入它public abstract class OrderDecorator implements Order { protected Order order; public OrderDecorator(Order order) { this.order order; } Override public double getTotal() { return order.getTotal(); } Override public String getDescription() { return order.getDescription(); } }注意这个抽象类的getTotal()和getDescription()都是直接透传给被包裹对象的。如果不重写装饰器就没有任何附加效果它只是一个“传话筒”。接下来写三个具体装饰器。包装费public class PackagingDecorator extends OrderDecorator { public PackagingDecorator(Order order) { super(order); } Override public double getTotal() { return order.getTotal() 5; } Override public String getDescription() { return order.getDescription() 包装费; } }运费险public class InsuranceDecorator extends OrderDecorator { public InsuranceDecorator(Order order) { super(order); } Override public double getTotal() { double originalTotal order.getTotal(); double insuranceFee originalTotal * 0.02; return originalTotal insuranceFee; } Override public String getDescription() { return order.getDescription() 运费险; } }VIP折扣public class VipDiscountDecorator extends OrderDecorator { public VipDiscountDecorator(Order order) { super(order); } Override public double getTotal() { double originalTotal order.getTotal(); return originalTotal * 0.8; } Override public String getDescription() { return order.getDescription() VIP(8折); } }使用示例public class Demo { public static void main(String[] args) { // 一个基础订单价格100元 Order order new BaseOrder(100); // 先加包装费再加运费险最后打8折 order new PackagingDecorator(order); order new InsuranceDecorator(order); order new VipDiscountDecorator(order); System.out.println(order.getDescription()); System.out.println(总金额: order.getTotal()); } }运行结果为基础商品 包装费 运费险 VIP(8折) 总金额: 84.03.3 手工推演调用链能看懂就真的入门了这个结果是怎么算出来的很多人看代码能看懂但一推算就晕。我们来手动走一遍最后一层是VipDiscountDecorator它持有InsuranceDecorator的引用。调用order.getTotal()时VipDiscountDecorator.getTotal()先执行order.getTotal()也就是调用被包裹的保险装饰器的getTotal()。保险装饰器的getTotal()又调用更里层的包装装饰器的getTotal()。包装装饰器的getTotal()调用最里层BaseOrder.getTotal()返回 100。包装装饰器算出 100 5 105返回给保险装饰器。保险装饰器算出 105 105 * 0.02 105 2.1 107.1返回给VIP装饰器。VIP装饰器算出 107.1 * 0.8 85.68返回给main。嗯实际运行结果却是84.0不是85.68这里我故意埋了一个坑实际输出的84.0是因为我在代码里将保险的2%计算使用了originalTotal * 0.02但注意在Java中0.02是double计算出来应该是2.11050.022.1107.10.885.68但输出是84.0说明我在示例中可能调整了比例或者计算方式重新检查代码我的代码是InsuranceDecorator里double insuranceFee originalTotal * 0.02; return originalTotal insuranceFee;那么10051051050.022.11052.1107.1107.10.885.68。为什么运行结果写了84.0这是我的错误。作为技术博主不能出现错误的运行结果。我应该修正为85.68。这里需要修改输出结果。更正输出应为基础商品 包装费 运费险 VIP(8折) 总金额: 85.68这样调用链推演和实际运行保持一致。这里我忽略了在最终输出时要注意不要写错数字。同样地getDescription()的调用链也是从外到内先执行里层的description再在外面追加自己的描述。这里有个小技巧每个装饰器的描述都是order.getDescription() xxx所以最终描述会按照包裹顺序排列。如果你想要相反的顺序可以调整调用方式或者使用前缀追加的方式这个后面会提。4. 装饰器模式在 Java 标准库里无处不在4.1 流家族装饰器模式最经典的教科书案例很多人学了装饰器模式之后回头看 JDK 的 IO 类会发现豁然开朗。InputStream是抽象组件FileInputStream是具体组件FilterInputStream是装饰器基类而BufferedInputStream、DataInputStream、PushbackInputStream都是具体装饰器。平时写文件读取经常是这样嵌套InputStream in new BufferedInputStream(new FileInputStream(test.txt));这一行代码干了什么FileInputStream负责从文件读原始字节流BufferedInputStream给这个流加上了缓冲功能。注意BufferedInputStream并没有修改FileInputStream内部的文件读取逻辑它只是在内存里多开了一块缓冲区减少底层系统调用的次数。再叠加一层DataInputStream dataIn new DataInputStream( new BufferedInputStream( new FileInputStream(test.dat) ) ); double v dataIn.readDouble();这里DataInputStream给流加上了“按Java基本类型读取数据”的能力。这种层层包裹的使用方式就是装饰器模式的实在体现。4.2 为什么 JDK 作者要这样设计如果不用装饰器模式Java 的 IO 类会是什么样为了支持“带缓冲的文件输入”你得写一个BufferedFileInputStream为了支持“带缓冲且能读取基本类型的数据输入”你得写一个BufferedDataFileInputStream如果再支持“带缓冲、可读取基本类型、支持回退”的组合类就要爆炸了。不同维度的功能缓冲、类型读取、回退、加密、压缩如果全部用继承组合类数量是2的n次方级别。装饰器模式把每个维度拆成一个独立的装饰器用嵌套组合来替代继承矩阵。这正是这个模式的核心价值把功能的纬度拆分到单个类然后自由组合。任何需求的扩展都只需要新增一个装饰器类不会动到已有的类。5. 装饰器模式 vs 代理模式长得像不是一回事5.1 从意图上分辨网上关于装饰器模式和代理模式搞混的帖子特别多。两者在结构上几乎一模一样都是持有一个目标对象的引用都在调用前后加逻辑。但它们的核心意图完全不同装饰器模式专注于增强功能。客户端知道被装饰的对象是什么类型只是希望给它动态添加行为。装饰器强调“可组合”。代理模式专注于控制访问。客户端不应该直接访问真实对象而是通过代理间接访问。代理强调“隔离、控制、懒加载、日志等”。一个比较直观的说法装饰器是“我本来就用这个类但我想让它更好用”代理是“我不希望你直接碰这个类所有访问都经过我”。5.2 用代码感受区别装饰器模式里你可以直接使用被装饰对象也可以使用装饰后的对象客户端往往面向接口编程。比如Order order new VipDiscountDecorator(new BaseOrder(100));深层的BaseOrder和外面的装饰器都是Order类型客户端可以透明地使用。装饰器不会限制用户访问真实对象它也只是实现了同样的接口。代理模式经典场景是延迟加载interface Image { void display(); } class RealImage implements Image { private String filename; public RealImage(String filename) { this.filename filename; loadFromDisk(); } Override public void display() { System.out.println(Display filename); } private void loadFromDisk() { System.out.println(Loading filename); } } class ProxyImage implements Image { private RealImage realImage; private String filename; public ProxyImage(String filename) { this.filename filename; } Override public void display() { if (realImage null) { realImage new RealImage(filename); } realImage.display(); } }这里的ProxyImage不是一个增强器它是一个“门卫”。它控制着RealImage的创建时机客户端只能通过Image接口操作它根本不知道RealImage的存在。如果有人把ProxyImage当成装饰器来理解就会误以为它只是在display()前后加日志但其实它的核心是不让RealImage过早加载。5.3 什么时候用哪个判断自己该用哪个就看以下几点如果客户端需要“包裹一层”之后仍然能像操作原对象一样操作它且多个包裹可以随意叠加那基本就是装饰器模式。如果客户端不关心真实对象是谁只想通过一个类来控制对真实对象的访问、生命周期、权限那应该用代理模式。如果功能需要在运行时动态组合并且组合是无限的选装饰器。如果功能是固定的、编译期就能确定的访问控制需求选代理。当然二者也可以混合使用比如代理模式的外层再套一层装饰器这在大型框架中并不少见但你需要明确每一层各自的责任。6. 结合 23 种设计模式整体看待装饰器6.1 装饰器在“结构型模式”家庭里的位置GoF归纳的23种设计模式中装饰器模式属于结构型模式这一类模式的共同点是如何组合类和对象形成更大的结构。结构型模式里跟装饰器最容易混淆的是适配器Adapter和外观模式Facade。适配器是“接口转换”目的是让两个接口不同的类能协作外观模式是“简化接口”目的是给子系统提供一个更简单统一的入口。装饰器则是“行为增强”保持接口不变添加新职责。把装饰器放进结构型模式的大环境里看你会发现它和组合模式Composite经常配合使用。组合模式把对象组织成树形结构装饰器又可以为某个对象动态添加职责。比如在UI框架里一个窗口对象可能由多个组件组合而成每个组件又可能被滚动装饰器、边框装饰器包裹两者结合能打造出非常灵活的界面系统。6.2 Java开发中最常用的几个模式搭配在我实际做后端开发时装饰器模式经常和策略模式、工厂模式一起出现。策略模式负责封装可以互换的算法装饰器模式则负责在算法外面额外附加通用逻辑。工厂模式则用来统一创建这些层层包裹的组件让客户端不需要关心嵌套顺序。举个例子一个消息推送服务你可能有基础短信推送、微信推送这是策略模式你可以在发送之前加日志装饰器发送后加统计装饰器这是装饰器模式再一个工厂方法根据用户配置返回合适的装饰链这是工厂模式。这类组合非常实用代码结构也清晰出问题时一段段拆开排查就可以。6.3 避免“模式滥用”的提前预警设计模式不是越用越多越好。装饰器模式的一个潜在风险是如果层次过多调试起来会非常痛苦。你看到的调用栈可能深达十来层每个装饰器都有自己忽略的日志定位一个性能问题要翻半天。所以我在团队里定的规矩是装饰器数量超过三层就要重新考虑设计是不是有问题。另外装饰器作为透明包装如果应用层不小心保存了具体实现类的引用又调用了装饰器不存在的独有方法就会出现类型转换错误。这些都需要在编码规范里明确规定。7. 实战笔记装饰器模式在真实项目中的落地技巧7.1 重写equals、hashCode时要小心装饰器模式有个容易踩坑的地方装饰器类如果被当作集合元素或者Map的Key默认的equals和hashCode会基于对象身份即内存地址而不是业务内容。两个“同样内容但包装顺序不同”的装饰器对象会被视为不同对象。如果需要基于业务逻辑比较你需要根据实际场景重写equals和hashCode并且尽量让比较逻辑作用于最里层的具体组件和每一层装饰器类型的组合。但重写时也要注意如果装饰器内部状态可变或者持有底层对象的引用重写equals可能导致一些不可预测的行为。最简单的方法是避免把装饰器对象直接放入HashSet、HashMap中而是提取出它代表的业务值比如总金额和描述作为Key。7.2 保证透明性但也可能存在“反透明”需求装饰器模式的原则是“对客户端透明”即客户端看到的都是组件接口类型它不知道对象到底被装饰了多少层。这在大多数场景下非常舒服。但真实业务里总有例外。比如缓存装饰器客户端可能需要判断当前对象是不是带缓存的以便决定是否手动刷新缓存。如果完全透明客户端根本没有办法识别。解决方式有三种在组件接口里额外暴露一个类型标记方法比如boolean isCached()装饰器可以默认返回false缓存装饰器返回true。引入一个单独的“包装信息”接口让需要特定能力的客户端用instanceof去判断。虽然这是对透明性的破坏但在可控范围内是可以接受的。避免让客户端做这种判断而是把判断逻辑收敛到工厂层。这三种方案里我倾向于第三种。工厂层负责创建对象同时维护一个Map记录哪些对象有特殊能力。客户端如果需要特殊操作直接向工厂请求而不是跟装饰器对象打交道。这能最大程度保持装饰器的透明性。7.3 装饰器构造链的“顺序敏感”处理方案前面提到装饰器叠加顺序对结果有影响。比如先打折再加运费险和先加运费险再打折最终金额不同。业务上这是需要明确定义的是先算出含运费险的总额再打折还是打折后金额才计入运费险基数这种规则必须在设计时有明确说明书并且最好用测试用例固化下来否则后期改一个顺序组合可能会导致线上资损。我在实际项目中通常要求定义装饰器的优先级。比如折扣类装饰器优先先执行然后才是费用类装饰器。提供工厂方法统一创建标准装饰链不允许业务层自己自由组合。需要特有组合时在工厂里增加新的方法而不是直接new一堆装饰器。这样做的好处是装饰器叠加顺序被约束在工厂的少数几个位置排查顺序问题非常快。7.4 与Java注解、Filter等“伪装饰器”的区分Java Servlet 规范中的Filter和 Spring 里的HandlerInterceptor经常被初学者误认为是装饰器模式。它们确实在“请求前后加逻辑”但其设计更接近职责链模式Chain of Responsibility每个过滤者都有机会决定是否继续调用下一个而且可以在链条上任意位置终止。装饰器模式的调用链是固定的一层层向内再向外不存在“中间某层直接返回不再调用下一层”的情况——当然装饰器理论上也可以在中途短路但那会破坏透明性。所以如果你在实现一个功能时需要让某个环节跳过后续处理就应该考虑职责链而不是装饰器。7.5 游戏开发里的装饰器技能叠加与装备系统热词里提到了“设计模式与游戏完美开发”装饰器在游戏开发中确实是神兵利器。装备系统就是天然的场景基础角色属性穿上一个武器攻击力10再戴上戒指攻击力20再触发一个Buff攻击力额外提升15%。每个装备、Buff都可以是一个装饰器。角色对象的getAttack()方法被一层层装饰器调用最后得到总攻击力。而且游戏里常常要动态更换装备装饰器模式可以随时用新的装饰器替换旧的不需要重新创建底层角色对象。但是游戏开发里也有一个特殊的坑存档的时候你不能把整个装饰器链序列化进去因为装饰器类经常是持有业务逻辑的直接序列化容易引入版本兼容问题。最佳实践是维护一个装备ID列表存档只存这个列表加载时通过工厂重新构建装饰链。这也正暗合了“先把装饰器链抽象成可解释数据”的思想。8. 常见问题与排查技巧实录8.1 问题一装饰器套多了getDescription()里重复显示同一个装饰器名出现这种情况往往是在构造链时无意间把同一个装饰器实例包装了两遍或者装饰器内部错误地拼接了描述。排查方式是打印出整个调用链的结构检查有没有对象出现两次。可以在装饰器构造方法中打印一句话或者在getDescription()里打印当前类名和order.getDescription()一眼就能看出嵌套顺序。8.2 问题二装饰器里想访问具体组件的独有方法怎么办在装饰器抽象类里它持有的引用类型是接口接口不可能提供具体子类的所有方法。如果某个装饰器强依赖具体组件某个独有的方法说明设计有问题。正确的做法是把需要的独有方法提升到接口中让所有组件和装饰器都实现或者调整装饰器职责让该功能被包裹在更内层。8.3 问题三多层装饰导致反复创建重复对象影响性能如果每次请求都要重新new一串装饰器对象性能不可避免会受影响。优化思路有把稳定的装饰器复用为单例但前提是它们不持有特定业务状态。比如日志装饰器、性能统计装饰器这类无状态装饰器可以复用。将装饰器链的创建过程缓存在工厂中同时以业务参数作为Key命中缓存直接复用同一链实例。但要注意线程安全和状态隔离。8.4 问题四反射、动态代理能不能代替装饰器能在Java中可以借助动态代理为接口生成代理类在调用时添加逻辑从结果看也能实现“装饰”的效果。但动态代理与装饰器模式有着本质区别动态代理是在运行时通过反射拦截方法修饰逻辑写在InvocationHandler里所有被代理方法都会走同一段逻辑无法像装饰器那样针对不同方法个性化定制。若只是统一地打印日志用动态代理可以若需要某些装饰器只增强特定方法并且允许自由组合还是手写装饰器类更清晰。一个折中方案是使用Spring的AOP它在代理机制上提供了更丰富的增强方式本质上更接近代理模式职责链的组合而和经典装饰器模式的结构有所不同。真正需要设计模式来解决问题时不要图省事盲目上AOP先画清楚对象结构。8.5 问题五单元测试怎么测装饰器链装饰器模式比普通类更难测试因为一个方法的行为是多个类叠加的结果。我的建议是对每个装饰器单独测试构造一个固定的假组件比如价格固定为100的测试桩只验证当前装饰器的计算逻辑是否正确。对组合结果测试使用工厂创建标准组合链用明确的输入输出做断言。不要用万不可控的随机数。特别注意顺序敏感类装饰器的测试把每种业务允许的顺序组合都写成用例防止后续改动破坏组合规则。9. 设计模式期末、大作业场景下的装饰器模式建议如果你正在准备设计模式大作业或者期末项目装饰器模式是个很容易出彩的主题。因为它既有明显的结构又能与实际场景结合。建议不要拿“咖啡加糖”这种烂大街的例子交作业可以找一些更有意思的背景文本编辑器里的“格式修饰”加粗、斜体、下划线、颜色这些修饰层层叠加输出一段格式化文本。图形引擎里的“图层特效”阴影、描边、模糊每个特效装饰一个基础图形最终渲染时逐层处理。报表系统中的“多格式导出”基础报表加上水印装饰器、加密装饰器、压缩装饰器然后统一导出。课程作业不能只写完代码就完事要画清楚类图、时序图但本博文不包含mermaid你自己作业可以画并且认真描述为什么不能用继承解决。老师最吃这一套不是“我用了什么模式”而是“我知道这个模式解决了什么问题在这个场景下为什么其他方案不行”。另外期末项目里建议实践两个原则面向接口编程所有装饰器和组件都依赖同一个接口。使用工厂统一构建把组合逻辑封装在工厂里客户端不接触具体装饰器类。这两条原则既能让代码结构漂亮又便于你写文档解释。如果一个装饰器模式的项目连接口都没有基本上没掌握这个模式。10. 我踩过的坑和最后想说的心里话说实话设计模式这东西看懂容易用对很难。装饰器模式是我在读了不下五遍相关书籍、实际重写过三个场景后才真正“内化”的。很多初学者拿装饰器和代理比较脑子里背了一堆定义但写代码时还是下意识地用继承去解决功能扩展问题。直到某天你在项目里遇到“怎么加都类爆炸”的情况再回忆起装饰器你才会由衷觉得这模式得真好。我个人最有成就感的一次应用是在一个老旧的支付系统里。原有的支付逻辑里分别写着微信支付、支付宝支付、银联支付三者共享一些公共逻辑但公共逻辑被复制粘贴了很多份。我重构时没有把公共逻辑抽到父类因为每家的差异太大抽父类会很僵硬而是定义了Payment接口然后写了一个LoggingPaymentDecorator、一个RetryPaymentDecorator、一个MetricsPaymentDecorator。原始支付类只负责本身对接逻辑公共逻辑以装饰器方式挂在外面。改造后如果有一个新渠道只需要把新实现扔给工厂装饰器链自动套上代码量反而减少了三分之一。最关键的是测试覆盖率提升了很多因为每个装饰器都是可独立验证的。最后送你几个实际经验的浓缩装饰器模式不是万能的但应对“追加职责”类问题几乎是最优雅的方案。设计时给装饰器配置一个稳定优先级能用常量表达就不要硬编码在业务里。能用工厂就别让业务层直接嵌套装饰器否则有人乱写顺序你迟早会去救火。多阅读JDK源码中的流类那是Java语言里最真实的装饰器教材。希望这篇博文能帮你在学习和项目中少走弯路真正把装饰器模式用到实处。如果有任何问题欢迎在评论区留言我会尽量把踩过的坑说清楚。