装饰器模式:给对象“穿衣服”的艺术
说实话,我第一次接触装饰器模式时,觉得它有点绕。但后来在项目中用顺手了,才发现它真是个好东西。你想想看,如果我想给一个对象加功能,最直接的办法是什么?继承呗。但继承有个问题——它是静态的,编译时就定死了。而且每加一个功能就得新建一个子类,类多了简直要命。
装饰器模式就不一样了。它像给对象“穿衣服”一样,一层一层往上套。每件“衣服”都增加一点新功能,而且穿脱自由。嗯,这就是它的核心思想:动态地给一个对象添加额外的职责,比生成子类更灵活。
为什么需要装饰器模式?
我在项目中遇到过这样一个场景:一个消息推送系统,基础功能是发短信。后来产品经理说,要加邮件通知。再后来,又要加微信通知、加日志记录、加失败重试……如果每个组合都建一个子类,那得有多少?短信+邮件、短信+微信、短信+邮件+微信……这还不算完,还要考虑顺序。
用继承的话,类数量会爆炸。用装饰器模式呢?每个功能就是一个装饰器,想怎么组合就怎么组合。说白了,这就是“组合优于继承”的典型应用。
核心要点:装饰器模式让你在不修改原有类的情况下,动态地扩展对象的功能。它遵循开闭原则——对扩展开放,对修改关闭。
装饰器模式的结构
装饰器模式有四个角色,我习惯这样记:
- 组件接口(Component):定义对象的公共接口,所有组件和装饰器都要实现它。
- 具体组件(ConcreteComponent):被装饰的原始对象,实现基础功能。
- 装饰器基类(Decorator):实现组件接口,并持有组件对象的引用。它的主要作用是“传递调用”。
- 具体装饰器(ConcreteDecorator):在调用组件方法前后,添加自己的额外逻辑。
下面这张图能帮你理清关系:
代码示例:消息推送系统
咱们用代码说话。先定义组件接口:
// 组件接口
public interface Notifier {
void send(String message);
}
然后实现基础组件——短信通知:
// 具体组件
public class SmsNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("发送短信: " + message);
}
}
接下来是装饰器基类。它负责“传递调用”:
// 装饰器基类
public abstract class NotifierDecorator implements Notifier {
protected Notifier wrapped;
public NotifierDecorator(Notifier notifier) {
this.wrapped = notifier;
}
@Override
public void send(String message) {
wrapped.send(message); // 转发给被装饰的对象
}
}
现在可以写具体装饰器了。比如加一个邮件通知:
// 具体装饰器A:邮件通知
public class EmailDecorator extends NotifierDecorator {
public EmailDecorator(Notifier notifier) {
super(notifier);
}
@Override
public void send(String message) {
super.send(message); // 先执行原有逻辑
System.out.println("发送邮件: " + message); // 再添加新功能
}
}
再加一个微信通知:
// 具体装饰器B:微信通知
public class WeChatDecorator extends NotifierDecorator {
public WeChatDecorator(Notifier notifier) {
super(notifier);
}
@Override
public void send(String message) {
super.send(message);
System.out.println("发送微信: " + message);
}
}
客户端怎么用呢?就像叠衣服一样,一层套一层:
public class Client {
public static void main(String[] args) {
// 基础功能:只发短信
Notifier notifier = new SmsNotifier();
// 加个邮件功能
notifier = new EmailDecorator(notifier);
// 再加个微信功能
notifier = new WeChatDecorator(notifier);
// 调用时,三个功能都会执行
notifier.send("系统故障,请立即处理!");
// 输出:
// 发送短信: 系统故障,请立即处理!
// 发送邮件: 系统故障,请立即处理!
// 发送微信: 系统故障,请立即处理!
}
}
小提示:装饰器的执行顺序取决于你包装的顺序。上面的例子是“短信→邮件→微信”,如果你想先发微信再发邮件,调换一下装饰器的包装顺序就行。
装饰器模式 vs 继承
我整理了一张对比表,方便你理解两者的区别:
| 对比维度 | 装饰器模式 | 继承 |
|---|---|---|
| 扩展方式 | 动态组合,运行时添加功能 | 静态绑定,编译时确定 |
| 灵活性 | 高,可以自由组合、任意顺序 | 低,每个组合都需要新类 |
| 类数量 | 少,n个功能只需n个装饰器 | 多,n个功能需要2ⁿ个组合类 |
| 对原有类的修改 | 不需要修改 | 可能需要修改父类 |
| 适用场景 | 功能组合多、变化频繁 | 功能稳定、层次清晰 |
实际项目中的避坑指南
我曾经犯过一个错误:在装饰器里修改了被装饰对象的状态。结果多个装饰器共享同一个对象时,状态互相干扰,查了半天才定位到问题。记住,装饰器应该只添加行为,不要修改内部状态。
还有一点要注意:装饰器模式不适合处理“装饰顺序敏感”的场景。比如加密后再压缩,和压缩后再加密,结果完全不同。这种情况下,装饰器模式虽然能用,但容易出错。我建议用策略模式或者管道模式来处理。
⚠️ 注意事项:
- 装饰器不要修改被装饰对象的接口,否则会破坏透明性
- 避免装饰器链过长,超过5层就会影响可读性
- 如果装饰器需要访问被装饰对象的特定方法,说明设计可能有问题
Java I/O 中的装饰器模式
你想想看,Java 的 I/O 流是不是很眼熟?BufferedInputStream 装饰 FileInputStream,DataInputStream 再装饰 BufferedInputStream……这就是典型的装饰器模式。
// Java I/O 中的装饰器
InputStream in = new FileInputStream("data.txt");
in = new BufferedInputStream(in); // 添加缓冲功能
in = new DataInputStream(in); // 添加基本类型读取功能
我个人习惯用这个例子来向团队解释装饰器模式。因为大家都用过,一说就懂。
什么时候用装饰器模式?
我总结了三个适用场景:
- 功能组合多:比如消息通知、日志记录、权限校验,这些功能经常需要组合使用。
- 功能变化频繁:产品经理今天加一个功能,明天减一个功能,用装饰器模式改起来快。
- 不想影响其他对象:装饰器只影响被装饰的那个实例,不会污染整个类。
反过来,如果功能组合固定、变化很少,直接用继承反而更简单。别为了用模式而用模式,这是新手容易犯的毛病。
一句话总结:装饰器模式就是“组合的艺术”。它用“包装”代替“继承”,让你像搭积木一样灵活地扩展对象功能。记住四个字——动态组合。
公众号:蓝海资料掘金营,微信deep3321