装饰器模式:给代码穿上“可脱卸”的外衣
说实话,我最早接触装饰器模式,是在读Java IO源码的时候。当时看到new BufferedInputStream(new FileInputStream("test.txt"))这种写法,第一反应是——这代码写得也太绕了吧?后来真正理解了装饰器,才明白这种“套娃”式的设计有多优雅。
装饰器模式,说白了就是:在不修改原有类的情况下,动态地给对象添加新功能。你想想看,如果每次想加个缓存、加个加密,都要去改原来的类,那代码迟早变成一团乱麻。
为什么需要装饰器?
我遇到过这样一个场景:一个消息推送服务,最开始只支持短信发送。后来产品经理说,要加邮件通知。再后来,又要加微信通知、加日志记录、加失败重试……
如果用继承来解决,你会得到这样的类结构:
// 继承方式的噩梦
class SmsSender {}
class EmailSender extends SmsSender {}
class WechatSender extends SmsSender {}
class SmsWithLogSender extends SmsSender {}
class EmailWithLogSender extends EmailSender {}
// ... 组合爆炸!
每增加一个功能,类的数量就翻倍。这就是典型的“类爆炸”问题。装饰器模式就是专门解决这个痛点的。
装饰器模式的核心结构
先看一张图,理解装饰器的骨架:
这个模式就四个角色:
- Component:抽象接口,定义核心行为
- ConcreteComponent:具体实现,提供基础功能
- Decorator:抽象装饰器,持有Component引用
- ConcreteDecorator:具体装饰器,添加额外功能
Java实现:消息推送的装饰器
回到我前面说的消息推送场景。用装饰器模式,代码会清爽很多:
// 1. 组件接口
interface Notifier {
void send(String message);
}
// 2. 基础实现
class SmsNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("发送短信: " + message);
}
}
// 3. 抽象装饰器
abstract class NotifierDecorator implements Notifier {
protected Notifier wrapped;
public NotifierDecorator(Notifier notifier) {
this.wrapped = notifier;
}
@Override
public void send(String message) {
wrapped.send(message);
}
}
// 4. 具体装饰器 - 加日志
class LogDecorator extends NotifierDecorator {
public LogDecorator(Notifier notifier) {
super(notifier);
}
@Override
public void send(String message) {
System.out.println("[日志] 开始发送消息");
super.send(message);
System.out.println("[日志] 发送完成");
}
}
// 5. 具体装饰器 - 加加密
class EncryptDecorator extends NotifierDecorator {
public EncryptDecorator(Notifier notifier) {
super(notifier);
}
@Override
public void send(String message) {
String encrypted = encrypt(message);
super.send(encrypted);
}
private String encrypt(String msg) {
return "加密{" + msg + "}";
}
}
// 6. 使用
public class Demo {
public static void main(String[] args) {
Notifier notifier = new SmsNotifier();
notifier = new LogDecorator(notifier);
notifier = new EncryptDecorator(notifier);
notifier.send("你好,世界");
// 输出:
// [日志] 开始发送消息
// 发送短信: 加密{你好,世界}
// [日志] 发送完成
}
}
你看,想加什么功能,就包一层装饰器。顺序也可以自由调整。这就是装饰器的精髓——运行时动态组合。
C++实现:同样的模式,不同的风味
C++的实现思路完全一样,但要注意内存管理。我个人习惯用智能指针来避免内存泄漏:
#include <iostream>
#include <memory>
#include <string>
// 组件接口
class Notifier {
public:
virtual ~Notifier() = default;
virtual void send(const std::string& message) = 0;
};
// 基础实现
class SmsNotifier : public Notifier {
public:
void send(const std::string& message) override {
std::cout << "发送短信: " << message << std::endl;
}
};
// 抽象装饰器
class NotifierDecorator : public Notifier {
protected:
std::shared_ptr<Notifier> wrapped;
public:
NotifierDecorator(std::shared_ptr<Notifier> notifier)
: wrapped(notifier) {}
void send(const std::string& message) override {
wrapped->send(message);
}
};
// 具体装饰器 - 日志
class LogDecorator : public NotifierDecorator {
public:
using NotifierDecorator::NotifierDecorator;
void send(const std::string& message) override {
std::cout << "[日志] 开始发送消息" << std::endl;
NotifierDecorator::send(message);
std::cout << "[日志] 发送完成" << std::endl;
}
};
// 具体装饰器 - 加密
class EncryptDecorator : public NotifierDecorator {
public:
using NotifierDecorator::NotifierDecorator;
void send(const std::string& message) override {
std::string encrypted = "{加密}" + message;
NotifierDecorator::send(encrypted);
}
};
int main() {
auto notifier = std::make_shared<SmsNotifier>();
auto logDecorator = std::make_shared<LogDecorator>(notifier);
auto encryptDecorator = std::make_shared<EncryptDecorator>(logDecorator);
encryptDecorator->send("你好,世界");
return 0;
}
Java IO流中的装饰器——最经典的案例
Java的IO库是装饰器模式最典型的应用。你看这个:
// 基础:从文件读取字节
FileInputStream fis = new FileInputStream("data.txt");
// 装饰:加缓冲
BufferedInputStream bis = new BufferedInputStream(fis);
// 再装饰:加解压缩
GZIPInputStream gzis = new GZIPInputStream(bis);
// 再装饰:加对象反序列化
ObjectInputStream ois = new ObjectInputStream(gzis);
// 最终:读取一个对象
Object obj = ois.readObject();
每一层装饰器只做一件事,组合起来就能完成复杂功能。这就是组合优于继承的绝佳证明。
| 装饰器类 | 功能 | 装饰对象 |
|---|---|---|
| BufferedInputStream | 提供缓冲,减少磁盘IO | InputStream |
| DataInputStream | 读取基本数据类型 | InputStream |
| GZIPInputStream | 解压GZIP格式数据 | InputStream |
| ObjectInputStream | 反序列化Java对象 | InputStream |
| LineNumberReader | 读取文本并记录行号 | Reader |
Java与C++实现对比
| 对比维度 | Java | C++ |
|---|---|---|
| 接口定义 | interface 关键字 | 纯虚类(virtual ... = 0) |
| 内存管理 | 自动GC,无需操心 | 智能指针或手动管理 |
| 装饰器持有方式 | 直接持有接口引用 | 通常用 shared_ptr |
| 典型应用 | java.io 包、Servlet Filter | 标准库 stream、boost |
| 代码量 | 相对简洁 | 略多(需要处理构造/析构) |
避坑指南
我曾经在一个项目中,用装饰器给每个请求都加了权限校验、日志记录、性能统计、参数校验……结果发现请求延迟增加了30%。后来才意识到,装饰器虽然方便,但每一层都有性能开销。对于高频调用的场景,要慎重使用多层装饰。
另外,装饰器模式有一个“小陷阱”:装饰器与具体组件的类型不同。你不能把装饰后的对象当作具体组件来用。比如:
// 这样不行
SmsNotifier sn = new LogDecorator(new SmsNotifier());
// 必须这样
Notifier n = new LogDecorator(new SmsNotifier());
如果你需要用到具体组件的特有方法,装饰器模式就不太合适了。这时候可以考虑适配器模式或者代理模式。
什么时候用装饰器?
- 需要给对象动态添加功能,且功能可以自由组合
- 不想通过继承导致类爆炸
- 功能需要运行时决定,而不是编译时
- 功能之间相互独立,没有依赖关系
装饰器模式,说白了就是“用组合代替继承,用包装代替修改”。它让代码更灵活,但也要求你更克制。记住:好的设计模式,是让代码更容易理解和维护,而不是为了炫技。
核心要点回顾:
- 装饰器模式动态扩展功能,不修改原有代码
- 核心是组合而非继承,避免类爆炸
- Java IO 是教科书级的应用案例
- C++ 实现注意智能指针管理生命周期
- 嵌套层数不宜过多,注意性能开销