1. 什么是装饰器模式
想象一个最朴素的场景:你写了一个核心类,功能很纯粹。比如一个DataFetcher,它的职责就是从数据库里捞数据。
public class SimpleDataFetcher { public String fetchData() { // 核心逻辑:连接数据库,执行查询,返回结果 return "原始数据"; } }现在,产品经理来了新需求:
- “我们要给这个查询加缓存,提升性能。”
- “哦对了,还得记录每次查询的日志,方便排查问题。”
- “安全部门说了,敏感数据查询要加密。”
- “运营那边希望有个慢查询监控,超过1秒的要报警。”
程序员可能会怎么做?直接冲进SimpleDataFetcher的fetchData()方法里,一顿if-else猛如虎。结果就是,这个类的代码迅速膨胀,变成了一个糅合了数据获取、缓存、日志、加密、监控的“上帝类”。它违反了单一职责原则,变得难以维护、难以测试,任何一个需求的改动都可能引发蝴蝶效应。
而架构师会怎么做?用装饰器模式。
它的核心思想极其简单:不碰核心对象,而是给它“包”上一层又一层的“外壳”(装饰器),每层外壳只干一件事。你可以把它想象成给一个裸机(核心对象)不断加装配件:
- 先加个缓存外壳:有缓存就直接返回,没缓存再调里面的。
- 再加个日志外壳:在调用前后记录一下时间、参数。
- 再加个加密外壳:对结果进行加密处理。
- 最后加个监控外壳:计算耗时,超时就报警。
最终,你调用的是最外面那层壳,它里面套着日志壳,日志壳里面套着缓存壳,缓存壳里面才是那个裸机。裸机(核心功能)的代码一行没改,但功能却得到了极大的增强。
这就是装饰器模式的精髓:动态地、透明地给一个对象添加额外的职责,就增加功能来说,它比生成子类(继承)更为灵活。
2. 什么时候用
当你闻到代码里有下面这些“味道”时,就该考虑装饰器模式了:
2.1 核心稳定,周边易变
这是最典型的场景。你的核心业务逻辑(比如从数据库查数据)是稳定的,但围绕它的横切关注点(Cross-Cutting Concerns)却经常变化或增加。比如缓存策略(今天用本地内存,明天换Redis)、日志格式、权限校验、性能监控、重试机制、熔断降级等等。用装饰器把这些“易变”的、非核心的功能剥离出去,每个装饰器只负责一件事,核心类就干净了。
2.2 排列组合爆炸
如果通过继承来实现功能扩展,你会陷入“类爆炸”的噩梦。比如,一个Beverage(饮料)基类,有Milk(加奶)、Sugar(加糖)、Mocha(加摩卡)等子类。那你要MilkAndSugar、SugarAndMocha、MilkAndMocha、AllInOne等等无数个子类。而用装饰器,你只需要MilkDecorator、SugarDecorator、MochaDecorator,然后在运行时像搭积木一样任意组合:new MochaDecorator(new MilkDecorator(new SimpleCoffee())),想要什么口味就包什么,灵活到飞起。
2.3 功能需要动态装卸
有些功能不是一直需要的。比如,在开发环境需要详细的调试日志,但在生产环境为了性能要关掉。如果功能是硬编码在类里的,你就得改配置或者注释代码。用装饰器,你只需要在组装对象链的时候,决定是否加上LoggingDecorator这一层。在运行时动态地添加或撤销功能,这是继承绝对做不到的。
2.4 实际项目中的高频用例
- Java I/O 流:这是教科书级的例子。
InputStream就是那个“裸机”,FileInputStream是具体实现。BufferedInputStream、GZIPInputStream、DataInputStream这些都是装饰器。你可以new BufferedInputStream(new GZIPInputStream(new FileInputStream(...))),轻松组合出带缓冲的解压缩文件流。 - Web框架中的中间件/过滤器链:比如Servlet Filter、Spring的HandlerInterceptor。一个HTTP请求进来,先经过日志过滤器,再经过权限过滤器,最后到业务控制器。每个过滤器就是一个装饰器,它们层层包装了
HttpServletRequest和HttpServletResponse。 - GUI组件库:给一个基础的文本框组件,动态添加滚动条、边框、阴影等视觉效果。每个视觉效果都是一个独立的装饰器。
- 业务服务的增强:如开头的例子,一个纯粹的数据服务,可以被缓存装饰器、日志装饰器、监控装饰器、限流装饰器、降级装饰器等层层包裹,每个装饰器只关注自己的领域。
3. 怎么实现
理解了思想,我们看看代码骨架。装饰器模式通常有四个角色,结构非常清晰。
3.1 4步框架
- 组件接口 (Component):定规矩。所有“裸机”和“外壳”都得遵守这个规矩。它定义了核心的操作方法,比如
fetchData()。 - 具体组件 (ConcreteComponent):干核心活的“裸机”。它实现了组件接口,是功能最原始、最核心的实现。
- 抽象装饰器 (Decorator):“外壳”的模板。它也实现(或继承)了组件接口,并且持有一个组件接口的引用。这个引用指向被它包装的那个对象(可能是另一个“外壳”,也可能是最终的“裸机”)。它通常会把接口方法委托给持有的那个对象去执行。
- 具体装饰器 (ConcreteDecorator):干具体增强活的“外壳”。它继承自抽象装饰器,在调用被包装对象的方法之前或之后,加入自己的增强逻辑。
3.2 代码实现
// 1. 组件接口(规矩) public interface DataService { String getData(String key); } // 2. 具体组件(裸机) public class DatabaseDataService implements DataService { @Override public String getData(String key) { System.out.println("从数据库查询数据: " + key); // 模拟复杂查询 return "Data from DB for " + key; } } // 3. 抽象装饰器(外壳模板) public abstract class DataServiceDecorator implements DataService { protected DataService wrappee; // 持有一个被包装对象的引用 public DataServiceDecorator(DataService dataService) { this.wrappee = dataService; } @Override public String getData(String key) { // 默认行为就是转发给被包装对象 return wrappee.getData(key); } } // 4. 具体装饰器A - 缓存外壳 public class CachingDecorator extends DataServiceDecorator { private Map<String, String> cache = new HashMap<>(); public CachingDecorator(DataService dataService) { super(dataService); } @Override public String getData(String key) { // 增强逻辑:先查缓存 if (cache.containsKey(key)) { System.out.println("从缓存获取数据: " + key); return cache.get(key); } // 缓存没有,调用被包装的“裸机”或内层“外壳” String data = super.getData(key); // 拿到数据后放入缓存 cache.put(key, data); return data; } } // 5. 具体装饰器B - 日志外壳 public class LoggingDecorator extends DataServiceDecorator { public LoggingDecorator(DataService dataService) { super(dataService); } @Override public String getData(String key) { System.out.println("[INFO] 开始调用getData, 参数: " + key); long start = System.currentTimeMillis(); // 调用内层 String result = super.getData(key); long duration = System.currentTimeMillis() - start; System.out.println("[INFO] 调用结束,耗时: " + duration + "ms, 结果: " + result); return result; } } // 客户端使用:像搭积木一样组合功能 public class Client { public static void main(String[] args) { // 核心裸机 DataService coreService = new DatabaseDataService(); // 根据需要包装:先日志,再缓存(顺序很重要!) DataService decoratedService = new CachingDecorator( new LoggingDecorator(coreService)); // 使用增强后的服务 String data = decoratedService.getData("user_123"); System.out.println("最终结果: " + data); // 第二次调用,应该命中缓存,日志还会记录,但不会调用数据库 data = decoratedService.getData("user_123"); } }运行结果会清晰展示这个“套娃”过程:第一次调用,先经过缓存层(未命中),再经过日志层(记录开始),然后调用数据库,再记录结束,最后存入缓存。第二次调用,缓存层直接命中返回,日志层依然会记录。
4. 优缺点
4.1 优点
- 符合开闭原则的典范:这是它最大的优点。你要加新功能?写个新的装饰器类就行了。原有的核心组件代码和其他的装饰器代码,一行都不用动。系统对扩展开放,对修改关闭。
- 比继承灵活一万倍:继承是静态的,编译时就定死了。装饰是动态的,运行时想怎么组合就怎么组合,可以任意叠加和排序功能。避免了“类爆炸”问题。
- 职责单一,易于维护:每个装饰器只干一件事(缓存、日志、加密)。代码高内聚,低耦合。测试也简单,可以单独测试每个装饰器,也可以测试任意组合。
- 透明性:对使用方(客户端)来说,它拿到的是一个
DataService接口对象。它根本不知道里面套了多少层,是裸机还是豪华套装。接口的一致性得到了保持。
4.2 缺点
- “套娃”链可能很长很复杂:如果装饰层数太多,调试和跟踪会变得困难。你看到一个异常,需要一层层剥开,才能找到是哪个“壳”或者“芯”出了问题。日志必须打好,否则就是噩梦。
- 初始化配置代码可能很丑:就像上面客户端代码里那一长串
new CachingDecorator(new LoggingDecorator(new ...)),如果装饰器很多,这行代码会又长又丑。通常我们会用工厂模式或Spring 这类IoC容器来帮我们优雅地组装这个对象链。 - 装饰器的顺序很重要:比如,你先加密再压缩,和先压缩再加密,结果是完全不同的。在组装装饰链时,必须仔细考虑业务逻辑对顺序的要求。
- “移除”中间某层装饰器比较麻烦:装饰器模式天生是为了“添加”功能,如果你想在运行时动态“移除”某一层特定的装饰器(比如关掉缓存但保留日志),没有直接的支持。通常需要重新构建整个装饰链。
5. 实战建议
- 与其它模式联用:装饰器模式很少单打独斗。结合工厂模式来创建复杂的装饰链;在Spring中,结合
@Autowired和@Qualifier或者自定义BeanPostProcessor来动态注入装饰器,能让代码更整洁。 - 小心性能开销:每一层装饰都意味着一次方法调用转发。如果装饰链非常长,且方法调用非常频繁,可能会带来不可忽视的性能损耗。对于性能临界路径上的代码,要慎用。
- 区分装饰器和代理模式:两者结构很像,都是包装一个对象。但意图不同:代理模式主要为了控制访问(比如延迟加载、权限控制),它通常代表对象本身;装饰器模式主要为了增强功能,它代表对象的“附加物”。代理关系通常在编译时就确定了,而装饰关系可以在运行时动态组合。
- 不要过度设计:如果功能组合非常固定,且未来几乎不会变化,直接用继承或者在类里写几个方法可能更简单直接。设计模式是药,没病别乱吃。
6. 总结
装饰器模式是一种极其强大的结构型模式,它完美体现了“组合优于继承”的设计原则。当你面对一个核心功能需要被多种独立、可选的辅助功能增强时,当你想要避免通过继承导致子类泛滥时,当你需要在运行时灵活地装配对象时,它就是你的不二之选。
它让我们的代码像乐高积木一样,核心模块稳定坚固,增强模块即插即用,组合无限可能。用好它,你的系统架构会变得更加灵活、清晰和健壮。