
装饰器模式这东西我最早接触的时候也觉得就那样无非是包装一下对象嘛。直到后来在项目里被继承结构逼到墙角才真正体会到这个模式的精妙之处。如果你也在为“怎么优雅地给类加功能”发愁或者准备面试被问到设计模式这篇文章可以帮你把它彻底吃透。我尽量用大白话加上完整可跑的代码把装饰器模式讲明白。1. 装饰器模式到底解决什么问题先别急着看定义咱们先想一个特别常见的场景你写了一个类核心功能已经稳定了现在产品经理过来说要加个新功能你怎么办多数人第一反应是加个子类重写一下方法再调一下super。这个思路本身没问题但一旦功能多了你就会发现事情开始不对劲。1.1 继承扩展的尴尬与痛点假设你有一个Coffee类里面有getCost()和getDescription()两个方法。现在要支持加牛奶、加糖、加奶泡用继承去做就是CoffeeWithMilkCoffeeWithSugarCoffeeWithMilkAndSugarCoffeeWithMilkAndFoam...这还只是3个调料到了4个、5个的时候类的数量会指数级膨胀。你想一下加牛奶和加糖的顺序不同是不是还得搞两个类这种组合爆炸的问题用继承基本无解。更麻烦的是这些子类之间大量重复代码改动一个公共逻辑可能得同步改七八个文件。1.2 装饰器模式的核心思想装饰器模式的核心就一句话用组合代替继承把功能一层层包上去。它不改变原有对象的内部结构而是在外面套一层“壳”这层壳既能保留原对象的能力又能附加新的行为。你随时可以决定包几层、包什么、按什么顺序包。这个思路放到生活里特别像外卖包装核心的餐品不变外面可以套保温袋保温袋外面还能套防水袋再外面套个纸袋。每个袋子都有自己的功能但又不会改变餐品本身的配方。装饰器模式就是这种“套娃”思想在代码层面的落地。1.3 开闭原则的最佳实践设计模式的初衷大部分都指向一个原则对扩展开放对修改关闭。装饰器模式是把这个原则体现得最淋漓尽致的一种。你不需要动原有的类不需要动调用方的代码只需要新增一个装饰器类就能完成功能扩展。这意味着什么意味着你可以在不破坏现有代码稳定性的前提下持续给系统加新功能。这对线上系统的价值非常大——你不太可能为了加一个日志功能把线上正在跑的类改一遍风险太高了。而装饰器模式新增一个类就够了老代码一行不动。2. 装饰器模式的四个角色缺一不可先把这个模式的结构看清楚后面写代码才不会乱。整个模式一共有四个角色每个角色都有明确的职责分工。2.1 组件接口Component这是整个模式的“锚点”定义了一组稳定的业务方法。不管是原始对象还是装饰器都实现这个接口。它的存在保证了“所有对象都能被一视同仁地对待”这也是装饰器能层层嵌套的基础。2.2 具体组件ConcreteComponent这是最核心的原始对象实现了组件接口提供了最基础、最原始的功能。它就是那个被一层层包装的“餐品本体”。在装饰器模式里具体组件不需要知道装饰器的存在它只管做好自己分内的事。2.3 抽象装饰器Decorator这个角色很容易被忽略但它恰恰是整个模式的灵魂。它是一个抽象类实现了组件接口并且在内部持有组件接口的引用。这个引用就是被包装的对象。构造的时候传入谁它包装谁。这个类的核心工作只有两个把请求转发给持有的组件对象以及为子类提供“加料”的入口。2.4 具体装饰器ConcreteDecorator每个具体装饰器都继承抽象装饰器重写需要增强的方法。在调用父类转发结果的同时加入自己的额外逻辑。比如“加牛奶”是一个具体装饰器“加糖”是另一个具体装饰器它们互不干扰可以自由组合。这四个角色配合起来就形成了一条“责任链”。每个装饰器只关心自己的附加逻辑其他事情一律丢给持有对象去处理。3. 实战从零搭建一个咖啡订单系统纸上谈兵没什么意思直接看代码。我以一个咖啡订单系统为例把装饰器模式从头到尾写一遍。这个例子非常经典而且特别直观。3.1 先定义组件接口和基础实现首先是组件接口也就是所有咖啡的公共抽象public interface Coffee { double getCost(); String getDescription(); }接口很简单一个算价格一个描述。接下来是具体组件也就是最基础的咖啡public class Espresso implements Coffee { Override public double getCost() { return 6.0; } Override public String getDescription() { return 浓缩咖啡; } } public class Americano implements Coffee { Override public double getCost() { return 5.0; } Override public String getDescription() { return 美式咖啡; } }到这一步为止一切都很正常。系统里已经有两种基础咖啡了。现在需求来了要支持给咖啡加各种配料。3.2 抽象装饰器关键中的关键按照惯例先创建一个抽象装饰器类。这里最核心的代码就是那个持有Coffee接口引用的构造方法public abstract class CoffeeDecorator implements Coffee { protected Coffee coffee; public CoffeeDecorator(Coffee coffee) { this.coffee coffee; } Override public double getCost() { return coffee.getCost(); } Override public String getDescription() { return coffee.getDescription(); } }这里有两个设计细节值得注意。第一CoffeeDecorator实现了Coffee接口所以在任何需要Coffee的地方它都能被当作Coffee用。第二默认的getCost()和getDescription()直接转发给内部的coffee对象这保证了不重写也不会破坏原有行为。3.3 具体装饰器给咖啡加料有了抽象装饰器具体装饰器写起来就非常轻松了。比如加牛奶public class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee coffee) { super(coffee); } Override public double getCost() { return super.getCost() 2.0; } Override public String getDescription() { return super.getDescription() 加牛奶; } }加糖public class SugarDecorator extends CoffeeDecorator { public SugarDecorator(Coffee coffee) { super(coffee); } Override public double getCost() { return super.getCost() 1.0; } Override public String getDescription() { return super.getDescription() 加糖; } }你没看错就这么简单。加奶泡、加焦糖、加榛果全都是同一个套路。这就是装饰器模式最让人舒服的地方每种配料对应一个类类之间完全独立不需要知道彼此的存在。3.4 组合使用效果拉满现在看看实际使用的效果这才是重点public class CoffeeShop { public static void main(String[] args) { Coffee coffee new Espresso(); System.out.println(基础款 coffee.getDescription() 价格 coffee.getCost()); coffee new MilkDecorator(coffee); System.out.println(加牛奶后 coffee.getDescription() 价格 coffee.getCost()); coffee new SugarDecorator(coffee); System.out.println(再加糖后 coffee.getDescription() 价格 coffee.getCost()); coffee new FoamDecorator(coffee); System.out.println(最后加奶泡 coffee.getDescription() 价格 coffee.getCost()); // 也可以一次性从基础咖啡开始包装 Coffee another new SugarDecorator(new MilkDecorator(new Americano())); System.out.println(另一杯 another.getDescription() 价格 another.getCost()); } }运行结果基础款浓缩咖啡价格6.0 加牛奶后浓缩咖啡加牛奶价格8.0 再加糖后浓缩咖啡加牛奶加糖价格9.0 最后加奶泡浓缩咖啡加牛奶加糖加奶泡价格12.0 另一杯美式咖啡加牛奶加糖价格8.0看到没每次包装之后返回给我们的依然是一个Coffee对象。这就是装饰器模式“套娃”的精髓——包装完的对象还能继续被包装想包几层就包几层想怎么组合就怎么组合。3.5 为什么说装饰器比继承更优秀咱们把这个例子和开头的继承方案对比一下。同样的功能用继承实现需要多少类不加调料是两种基础咖啡3种调料下全组合就是8种5种调料就是32种。而装饰器模式只需要2个基础咖啡类 1个抽象装饰器 N个调料装饰器类完全线性增长。更重要的是装饰器模式在运行期可以动态组合。今天想喝“美式牛奶糖”明天想喝“浓缩奶泡”不需要预先定义好这种组合临时拼装就行。这种灵活性继承给不了。4. 框架与源码中的装饰器模式其实你早就在用装饰器模式了只是可能没意识到。很多框架和JDK源码里都有装饰器模式的影子只是换了一层马甲。4.1 JDK里的IO流最经典的示范Java IO类库是整个JDK中装饰器模式应用得最彻底的地方。看一下这个常见写法BufferedReader reader new BufferedReader(new InputStreamReader(new FileInputStream(data.txt)));这个嵌套结构从里往外一层层看FileInputStream是具体组件负责读文件字节InputStreamReader是装饰器负责把字节流转换成字符流BufferedReader是装饰器负责提供缓冲功能每一层都扩展了上一层的能力但没有改变文件读取这个最核心的行为。你换一个StringReader给它包装BufferedReader照样能用——这就是面向接口编程的好处。还有DataOutputStream、BufferedInputStream、PushbackInputStream全都是这个套路。学IO流的时候如果能意识到这是装饰器模式理解起来会顺很多。4.2 JDK集合类里的低调应用java.util.Collections里有两个方法核心类都应用了装饰器思想ListString synchronizedList Collections.synchronizedList(new ArrayList()); ListString unmodifiableList Collections.unmodifiableList(new ArrayList());这两个方法返回的其实是Collections内部定义的静态内部类它们实现了List接口内部持有原始集合的引用。SynchronizedCollection给集合的所有方法都加上了synchronized锁UnmodifiableCollection把所有修改方法都改成了抛异常。这就是典型的具体装饰器——原始ArrayList的代码完全没动但能力被包装成了线程安全的、只读的。注意区分下Collections.synchronizedList这里通过静态工厂方法返回装饰器没有显式命名为Decorator但包装的机制和装饰器模式完全一致。4.3 Spring等框架中的实际应用Spring框架里也有很多装饰器模式的影子。比如TransactionAwareCacheDecorator它包装了一个Cache对象在操作缓存之前自动检查Spring事务确保事务存在时把缓存操作延迟到事务提交后才执行。还有HttpServletRequestWrapper和HttpServletResponseWrapper这俩更是装饰器模式的教科书级应用。为什么要包装因为Servlet规范里请求和响应对象都是接口你没法直接修改实现类的代码。这时候就搞了个HttpServletRequestWrapper内部持有原始请求对象默认全部转发。你想加个自定义参数继承这个Wrapper重写getParameter()就行。public class MyRequestWrapper extends HttpServletRequestWrapper { private MapString, String extraParams new HashMap(); public MyRequestWrapper(HttpServletRequest request) { super(request); extraParams.put(userId, 12345); } Override public String getParameter(String name) { String value super.getParameter(name); return value ! null ? value : extraParams.get(name); } }这里的核心价值是过滤器链里传的这个包装对象对业务代码来说是透明的——它们看到的还是一个HttpServletRequest但行为已经被悄悄改写了。这个例子特别适合用来理解装饰器“透明增强”的特性。4.4 开源项目里的装饰器运用再看几个大家熟知的中间件一旦看懂它们的包装机制你就能理解它们为什么这么设计。MyBatis的Cache接口执行器里的二级缓存用了大量的装饰器设计。SynchronizedCache、LoggingCache、SerializedCache、LruCache它们全都实现了同一个Cache接口然后一层层互相包装从基础的PerpetualCache开始按配置组合出各种带缓存策略的Cache对象。Dubbo的Protocol、Cluster、Invoker职责链上层层封装每个装饰器只负责一项能力比如失败重试、负载均衡、超时控制。当请求进来先从最外层经过各种逻辑最后到达真正干活的实现。Netty的ChannelHandler虽然不叫装饰器但管道中每个handler都是对前一个handler的进一步增强或处理这种链式处理模型和装饰器“逐层附加职责”的思路一脉相承。5. 这些坑我替你先踩过了装饰器模式虽然好用但真用到生产环境里有几个坑非常容易踩。下面都是我在实际开发和Code Review时总结出来的血泪经验。5.1 不要用instanceof去判断具体类型因为装饰器包装的是接口运行期的真实类型是装饰器类不是原始的具体类。如果你代码里有这种判断if (coffee instanceof Espresso) { // 做一些特殊处理 }一旦coffee被MilkDecorator包了一层这个判断就永远为false了。这是个非常隐蔽的bug排查的时候让人头大。我的建议是在使用装饰器模式的情况下尽量避免依赖具体类型判断。如果你真的需要根据原始类型走不同逻辑最好在组件接口里加一个类型标识的方法或者用别的方式绕过这个问题。5.2 别在装饰器里包this和方法回调这一点相当诡异但真实存在。假设有个上报统计的框架你在装饰器里给方法加了统计逻辑public class MetricDecorator implements Service { private Service target; public MetricDecorator(Service target) { this.target target; } Override public void execute() { long start System.currentTimeMillis(); target.execute(); System.out.println(耗时: (System.currentTimeMillis() - start)); } }问题出在如果target在执行过程中内部调用了自己的另一个方法这个调用走的是target内部的方法不会经过你的装饰器。如果你以为“包装了就万事大吉所有方法都被增强了”那就大错特错了。装饰器只对“外部通过引用调用”的方法生效对对象内部自调用完全无感。这在Spring AOP里面也有对应问题叫“自调用失效”。理解了装饰器的这个特性你就能理解为什么Spring要引入AopContext和ExposeProxy来处理内部调用的场景。5.3 装饰器类别搞太深三层四层是上限理论上装饰器可以无限嵌套但实际上嵌套太多会带来一连串问题调试的时候调用栈深得吓人排查一个异常要翻十几层包装反射和序列化在某些框架下会出幺蛾子代码可读性急剧下降新人根本看不懂那一长串构造函数。我的个人经验是装饰器嵌套尽量控制在两三层以内。如果发现“加这个能力”“加那个能力”已经超过三层了应该停下来考虑下是不是该换一种设计思路比如策略模式、责任链模式或者干脆把这些功能内聚到一个大的实现里面。5.4 装饰器要保证“透明性”官方给装饰器模式的定义里有个关键词叫transparent翻译过来是透明性。意思是装饰器对外呈现的类型必须和原始对象一致调用方感知不到装饰器的存在。这里有一个注意点如果装饰器类新增了额外的方法而调用方强转成装饰器类型去调用这些方法那代码就变得不“透明”了整个体系的自由度就会大打折扣。这相当于在装饰器内部留下了对具体实现的依赖后面换装饰器的时候调用方代码也得跟着改。我一般会在装饰器里尽量不新增公开方法只增强接口里已有的方法。实在要新增也要想清楚是不是用别的方式更合理。5.5 equals、hashCode等基础方法要慎重处理装饰器包装后equals()和hashCode()的行为也会变。默认情况下MilkDecorator包装的对象和一个独立构造的Espresso虽然内部依赖同一个对象但它们的equals()结果是false因为类都不同。这在集合场景下特别容易出问题把一个装饰器对象放到HashSet里再放一个原始对象进去你可能期望它们是相等的但实际会出现重复元素。如果业务上对相等性有要求建议在装饰器里显式委托给持有对象的equals()和hashCode()避免这种边界情况。6. 面试必问装饰器模式 vs 代理模式只要你面的是后端或者Java相关岗位面试官十有八九会问这个问题装饰器模式和代理模式有什么区别这两个模式的结构极其相似都是“包装一个对象”很多人分不清。6.1 从目的上区分这两个模式的根本区别是意图不同装饰器模式的目标是增强功能。你使用装饰器是为了给对象添加新的行为比如给咖啡加糖、给IO加缓冲。代理模式的目标是控制访问。你使用代理是为了控制对原始对象的访问权比如延迟加载、访问控制、日志记录。代理不关心也不改变原始对象的功能逻辑。一句话概括装饰器是“加东西”代理是“控制入口”。6.2 从类型透明度上区分装饰器对客户端是透明的。客户端完全可以把装饰后的对象当成原始对象来用它只关心接口方法的能力是不是够用不在乎谁在内部执行。代理模式则相反。虽然代理也实现了和目标对象相同的接口但从客户端的角度看它面对的是一个“受控的访问点”有时候代理类还会新增一些管理类的方法比如检查权限、监控运行状态。对客户端而言代理更像一个“把关者”而不是一个“增强版”。6.3 从创建时机上区分装饰器通常在需要增强时动态组装对象已经存在了然后一层层包上去。代理类的创建则往往发生在原始对象创建之前或者在访问原始对象的那一刻由代理决定要创建什么、暴露什么。6.4 一张表看清所有区别对比维度装饰器模式代理模式设计意图增强、附加职责控制访问、保护目标客户端感知感知不到装饰器透明使用常能感知代理层存在对象关系递归嵌套层层包装一对一的控制关系生命周期原始对象已存在动态包装可以控制原始对象的创建时机典型应用IO流、HttpServletRequestWrapper懒加载代理、Spring AOP、RPC代理扩展方式通过增加新装饰器扩展能力通过增加新代理策略扩展控制逻辑这个表记熟了面试的时候能稳很多。但要记住回答的时候别只背表要结合具体例子展开证明你是真的懂而不是背概念。7. 什么时候用装饰器什么时候别用任何设计模式都有它的适用边界装饰器模式也不例外。用得好是利器用不好是负担。7.1 适合使用装饰器模式的场景总结下来下面四种情况特别适合用装饰器模式需要给一组兄弟类动态添加功能。功能种类多且组合灵活用继承会类爆炸。功能可以叠加且顺序可自由组合。就像咖啡加糖加奶顺序不同体验不同但也不需要为每种顺序建一个类。希望增强功能但不想修改原始类代码。原始类是公共组件或第三方库改不了源码那就包装它。需要严格遵守开闭原则的场景。系统核心类已经稳定不希望在加功能时动到核心代码。7.2 不适合使用装饰器模式的场景同时这几种情况我建议你绕道功能种类固定且完全不会有变化。没必要为了一个固定功能建一堆类直接用普通继承或者组合就行。装饰层数可能特别多场景。超过三层后调试和理解成本会大幅上升。对对象类型判断敏感的系统。如依赖instanceof做逻辑分支、序列化/反序列化要求类型严格准确的场景装饰器会给你带来不必要麻烦。并发环境下的共享可变对象。多个线程同时调用同一个装饰器对象装饰器本身是无状态的还好一旦在装饰器里放入状态字段可能会出现并发问题。这种场景优先考虑能不能用无状态策略或者用责任链替代。7.3 可以和别的模式配合使用工厂模式和装饰器模式简直是天作之合。实际项目中我不会在业务代码里到处new装饰器而是把“拼装装饰器”的逻辑收敛到工厂类里。客户端只需要给一个配置项工厂负责把装饰器一层层组装好返回。public class CoffeeFactory { public static Coffee createCoffee(String base, String... toppings) { Coffee coffee; switch (base) { case espresso: coffee new Espresso(); break; case americano: coffee new Americano(); break; default: throw new IllegalArgumentException(未知咖啡类型: base); } for (String topping : toppings) { switch (topping) { case milk: coffee new MilkDecorator(coffee); break; case sugar: coffee new SugarDecorator(coffee); break; case foam: coffee new FoamDecorator(coffee); break; default: throw new IllegalArgumentException(未知配料: topping); } } return coffee; } }这样设计之后客户端只需要一句话Coffee coffee CoffeeFactory.createCoffee(americano, milk, sugar);装饰器模式的灵活性和工厂模式的分层装配结合起来既体现了组合的优势又降低了使用成本。这也是我们平时项目中比较推荐的组合方式。类似地如果装饰器的创建过程很复杂也可以用建造者模式去辅助构建或者用策略模式动态选择装饰器类型顶层设计上可以很灵活。7.4 我的选型判断标准说实话我平时写业务代码时第一个想到的方案通常还不是装饰器模式而是先考虑这个功能到底要不要抽象。很多时候几个if-else就能搞定的事情根本不需要上升到模式层面。但是一旦出现了两个信号我就会立刻考虑装饰器模式第一同样类型的增强后续还会不断出现第二增强的组合方式是灵活且不可预测的。满足这两个条件时用装饰器模式几乎不会后悔。8. 写在最后的几点心得用了这么多年的设计模式我最大的体会是不要为了用模式而用模式模式是工具不是目的。装饰器模式看着简单但真正用好它需要考验你对系统边界的理解、对扩展性的预判、以及对代码可读性的把握。我自己在项目里最常用到装饰器模式的地方一个是给第三方SDK做能力增强另一个是在不动核心接口的情况下增加横切逻辑。前者因为改不了源码只能包装后者因为核心接口被很多地方引用不想因为加一个功能就波及所有调用方。这两种场景下装饰器模式都是性价比极高的方案。最后再分享一个小技巧如果你拿不准自己的设计算不算真正的装饰器模式就检查一下这个类能不能继续被同样类型的类包装。如果你自己都说不准那大概率还不是一个合格的装饰器。多写、多看、多琢磨模式这种东西用着用着就内化了。