装饰器模式:给代码穿上“可拆卸”的外衣
说实话,我第一次接触装饰器模式,是在看 Java I/O 源码的时候。当时我盯着 new BufferedInputStream(new FileInputStream("test.txt")) 这行代码,心里想的是:这嵌套得也太深了吧?后来我才明白,这恰恰是装饰器模式的精髓——一层一层地给核心功能“加料”。
今天我们就来聊聊这个模式。我会结合一个真实的业务场景:短信发送功能。你想想看,一个基础的短信发送,可能只需要把文本发出去。但实际项目中,我们往往需要给它加上签名、加密、日志记录……这些附加功能。如果用继承来做,那得有多少子类?签名短信、加密短信、签名+加密短信……光想想就头大。
装饰器模式就是来解决这个问题的。它允许你动态地给一个对象添加额外的职责。说白了,就是你可以像搭积木一样,自由组合各种功能。
模式核心:三个角色
要理解装饰器,先记住这三个角色:
- 抽象组件(Component):定义核心接口。比如短信发送的接口。
- 具体组件(ConcreteComponent):实现核心功能。比如最基础的短信发送。
- 装饰器(Decorator):持有组件引用,并实现相同接口。它负责“包装”核心组件。
嗯,这里要注意:装饰器本身也是组件。这意味着你可以用装饰器去包装另一个装饰器。这就是嵌套调用的由来。
业务场景:短信发送的“豪华套餐”
假设我们有一个基础的短信发送功能:
// 抽象组件
public interface SmsSender {
void send(String message);
}
// 具体组件:基础短信发送
public class BasicSmsSender implements SmsSender {
@Override
public void send(String message) {
System.out.println("发送短信:" + message);
}
}
现在,我们需要给短信加上签名。我个人的习惯是,先写一个抽象的装饰器基类:
// 抽象装饰器
public abstract class SmsDecorator implements SmsSender {
protected SmsSender wrapped;
public SmsDecorator(SmsSender wrapped) {
this.wrapped = wrapped;
}
@Override
public void send(String message) {
wrapped.send(message);
}
}
然后,具体的装饰器就可以继承这个基类:
// 签名装饰器
public class SignatureDecorator extends SmsDecorator {
private String signature;
public SignatureDecorator(SmsSender wrapped, String signature) {
super(wrapped);
this.signature = signature;
}
@Override
public void send(String message) {
// 在发送前追加签名
String signedMessage = message + " 【" + signature + "】";
super.send(signedMessage);
}
}
// 加密装饰器
public class EncryptDecorator extends SmsDecorator {
public EncryptDecorator(SmsSender wrapped) {
super(wrapped);
}
@Override
public void send(String message) {
// 模拟加密
String encrypted = "ENC(" + message + ")";
super.send(encrypted);
}
}
// 日志装饰器
public class LogDecorator extends SmsDecorator {
public LogDecorator(SmsSender wrapped) {
super(wrapped);
}
@Override
public void send(String message) {
System.out.println("[日志] 开始发送短信...");
super.send(message);
System.out.println("[日志] 短信发送完成。");
}
}
你看,每个装饰器只负责一件事。它们可以自由组合:
public class Client {
public static void main(String[] args) {
// 基础短信
SmsSender sender = new BasicSmsSender();
// 加上签名
sender = new SignatureDecorator(sender, "蓝海资料掘金营");
// 再加上加密
sender = new EncryptDecorator(sender);
// 最后加上日志
sender = new LogDecorator(sender);
// 发送
sender.send("今晚8点有直播课");
}
}
运行结果:
[日志] 开始发送短信...
发送短信:ENC(今晚8点有直播课 【蓝海资料掘金营】)
[日志] 短信发送完成。
是不是很灵活?想加什么功能,就包一层。想去掉,直接拆掉那层就行。
Java I/O 中的经典应用
其实 Java 的 I/O 流就是装饰器模式最经典的例子。我记得刚学的时候,被 BufferedInputStream、DataInputStream、ObjectInputStream 这些类搞得晕头转向。后来理解了装饰器模式,一下子就通了。
你看:
// 基础组件:FileInputStream 从文件读取字节
InputStream in = new FileInputStream("data.bin");
// 装饰器1:BufferedInputStream 提供缓冲功能
in = new BufferedInputStream(in);
// 装饰器2:DataInputStream 提供读取基本类型的功能
in = new DataInputStream(in);
// 现在你可以用 DataInputStream 的方法了
DataInputStream dis = (DataInputStream) in;
int value = dis.readInt();
每个装饰器都只增加一种能力。缓冲、数据类型读取、对象序列化……它们可以任意组合。这就是为什么 Java I/O 类库看起来那么庞大,但核心逻辑其实很简洁。
避坑指南:我曾经踩过的坑
我曾经在一个项目中,用装饰器模式给 HTTP 请求添加了重试、超时、日志等功能。看起来很美,但后来发现了一个问题:装饰器链的顺序很重要。
比如,如果你把日志装饰器放在最外层,它记录的是整个请求的耗时。但如果放在内层,它记录的只是核心请求的耗时,不包括重试的时间。这两种结果完全不同。
所以,我的建议是:在设计装饰器时,一定要明确每个装饰器的职责边界,并且通过文档或命名来约定顺序。比如,我习惯把日志装饰器放在最外层,因为它应该记录整个调用链的完整信息。
什么时候用装饰器?
我总结了几条经验:
- 当你需要给一个对象动态添加功能,而且这些功能可以自由组合时
- 当使用继承会导致子类爆炸时(比如短信的签名、加密、日志组合有 2^3=8 种,用继承你得写 8 个类)
- 当功能需要运行时动态决定时(比如根据配置决定是否加密)
但也要注意,装饰器模式不适合所有场景。如果功能组合是固定的,或者只有一两种变体,直接用继承或策略模式可能更简单。
知识体系图
下面这张图展示了装饰器模式的核心结构。我特意用短信发送的例子来画,方便你对照理解。
与 Java I/O 的对应关系
为了让你更清楚,我把短信示例和 Java I/O 做个对照:
| 角色 | 短信示例 | Java I/O 示例 |
|---|---|---|
| 抽象组件 | SmsSender | InputStream |
| 具体组件 | BasicSmsSender | FileInputStream |
| 抽象装饰器 | SmsDecorator | FilterInputStream |
| 具体装饰器 | SignatureDecorator | BufferedInputStream |
| 具体装饰器 | EncryptDecorator | DataInputStream |
| 具体装饰器 | LogDecorator | PushbackInputStream |
核心要点:装饰器模式的核心是“组合优于继承”。它通过对象组合的方式,实现了功能的动态扩展。每个装饰器只关注自己的职责,通过链式调用来叠加效果。
个人小技巧:我在设计装饰器时,会先画一个“功能组合矩阵”。比如短信有签名、加密、日志三个维度,矩阵的每个单元格就是一种组合。然后检查这些组合是否都能通过装饰器链实现。如果发现某个组合需要特殊处理,那可能就是设计有问题。
注意:装饰器模式虽然灵活,但也会带来一些问题。比如调试时,调用栈会变得很深,难以追踪。另外,如果装饰器链过长,性能也会受影响。我建议装饰器数量控制在 3-5 层以内,超过这个数,就要考虑是否该用其他模式了。
好了,关于装饰器模式,今天就聊到这里。记住它的核心思想:动态地给对象添加职责,而且这些职责可以自由组合。下次你看到 Java I/O 的嵌套调用,或者要给某个功能加“料”时,不妨想想装饰器模式。