#装饰器与适配器模式
#一个日志打印引发的血案
2022年,我们团队的监控系统出了个问题:日志打印耗时从 5ms 飙到 500ms。
排查发现,罪魁祸首是一段"增强版"的日志代码:
public class LoggingOutputStream extends OutputStream {
private final OutputStream delegate;
private final Logger logger;
public LoggingOutputStream(OutputStream delegate, Logger logger) {
this.delegate = delegate;
this.logger = logger;
}
@Override
public void write(int b) throws IOException {
logger.debug("Writing byte: " + b); // 每次写一个字节都打日志!
delegate.write(b);
}
}开发同学想给 OutputStream 加日志功能,结果在 write(int b) 里加日志——每个字节都打一次,500ms 全花在日志上了。
但这段代码本身是装饰器模式的正确用法。问题是:装饰器模式适合加什么功能?加到哪一层?
今天这篇文章,把装饰器模式和适配器模式放在一起讲——因为这两个模式在实现上几乎一模一样,区别只在语义上。
#一、装饰器模式核心结构🔴
#1.1 什么是装饰器
装饰器模式:动态地给对象添加一些额外的职责。就增加功能来说,装饰器模式比继承更灵活。
组件接口
│
├── ConcreteComponent(被装饰的对象)
│
└── Decorator(装饰器)
│
└── ConcreteDecoratorA
└── ConcreteDecoratorB
└── ConcreteDecoratorC#1.2 标准写法
// 组件接口
interface DataSource {
void write(String data);
String read();
}
// 被装饰对象
class FileDataSource implements DataSource {
private final String filename;
public FileDataSource(String filename) {
this.filename = filename;
}
@Override
public void write(String data) {
// 写入文件
}
@Override
public String read() {
// 读取文件
}
}
// 装饰器基类
class DataSourceDecorator implements DataSource {
protected final DataSource wrappee; // 被装饰对象
public DataSourceDecorator(DataSource wrappee) {
this.wrappee = wrappee;
}
@Override
public void write(String data) {
wrappee.write(data);
}
@Override
public String read() {
return wrappee.read();
}
}
// 具体装饰器:数据压缩
class CompressionDecorator extends DataSourceDecorator {
public CompressionDecorator(DataSource wrappee) {
super(wrappee);
}
@Override
public void write(String data) {
super.write(compress(data));
}
@Override
public String read() {
return decompress(super.read());
}
private String compress(String data) {
// 压缩算法
}
private String decompress(String data) {
// 解压算法
}
}
// 具体装饰器:数据加密
class EncryptionDecorator extends DataSourceDecorator {
public EncryptionDecorator(DataSource wrappee) {
super(wrappee);
}
@Override
public void write(String data) {
super.write(encrypt(data));
}
@Override
public String read() {
return decrypt(super.read());
}
private String encrypt(String data) { /* ... */ }
private String decrypt(String data) { /* ... */ }
}#1.3 装饰器的组合使用
// 装饰器可以层层叠加
DataSource source = new FileDataSource("data.txt");
// 先加密,再压缩
DataSource decorated =
new CompressionDecorator(
new EncryptionDecorator(source)
);
decorated.write("Hello"); // 压缩 -> 加密 -> 写入文件顺序不同,效果不同:
// 方式一:压缩后加密
decorated = new EncryptionDecorator(new CompressionDecorator(source));
// 数据流:原始数据 -> 压缩 -> 加密 -> 存储
// 方式二:加密后压缩
decorated = new CompressionDecorator(new EncryptionDecorator(source));
// 数据流:原始数据 -> 加密 -> 压缩 -> 存储【架构权衡】 装饰器的顺序很重要。一般原则:先处理后存储的操作(如压缩、加密),后处理先读取的操作。如果顺序错了,解压和解密就会失败。
#二、Java I/O 流:装饰器的集大成者🟡
#2.1 经典的装饰器设计
Java I/O 库是装饰器模式最经典的应用:
InputStream (Component)
│
├── FileInputStream (ConcreteComponent)
├── ByteArrayInputStream
├── SocketInputStream
│
└── FilterInputStream (Decorator)
│
├── BufferedInputStream
├── DataInputStream
├── LineNumberInputStream
└── PushbackInputStream// 基础用法
FileInputStream fis = new FileInputStream("data.txt");
BufferedInputStream bis = new BufferedInputStream(fis); // 加缓冲装饰器
DataInputStream dis = new DataInputStream(bis); // 加数据解析装饰器
// 层层叠加
BufferedInputStream buffered =
new BufferedInputStream(
new DataInputStream(
new FileInputStream("data.txt")
)
);#2.2 为什么 BufferedInputStream 能提升性能
// BufferedInputStream 装饰器
public class BufferedInputStream extends FilterInputStream {
private final byte[] buffer; // 8KB 缓冲区
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE); // 8192 字节
}
@Override
public synchronized int read() throws IOException {
if (pos >= count) {
fill(); // 一次性读 8KB 到缓冲区
if (pos >= count) return -1;
}
return buffer[pos++];
}
}关键洞察:FileInputStream 每次 read() 都触发一次系统调用(磁盘 IO),BufferedInputStream 把多次单字节读合并成一次批量读。
不用缓冲:10000 次系统调用(磁盘 IO 10000 次)
使用缓冲:10000 次内存读取 + 2 次系统调用(磁盘 IO 2 次)性能差异:10-100 倍。
⚠️
BufferedInputStream 加在正确的位置才能提升性能。如果你的数据源本身已经是缓冲的(如 ByteArrayInputStream),再套 BufferedInputStream 就没有意义了。
#三、适配器模式核心结构🔴
#3.1 什么是适配器
适配器模式:将一个类的接口转换成客户端所期望的另一种接口,使原本不兼容的类可以合作。
客户端期望的接口 (Target)
↓
Adapter ← Adaptee(需要适配的接口)#3.2 类适配器 vs 对象适配器
// 被适配者:现有的类
class Adaptee {
public void specificRequest() {
System.out.println("Adaptee: specific request");
}
}
// 目标接口
interface Target {
void request();
}
// 方式一:类适配器(通过继承)
class ClassAdapter extends Adaptee implements Target {
@Override
public void request() {
specificRequest(); // 调用父类方法
}
}
// 方式二:对象适配器(通过组合)
class ObjectAdapter implements Target {
private final Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void request() {
adaptee.specificRequest(); // 组合方式
}
}#3.3 什么时候用类适配器 vs 对象适配器
| 维度 | 类适配器 | 对象适配器 |
|---|---|---|
| 实现方式 | 继承 | 组合 |
| 灵活性 | 低(受限于单继承) | 高 |
| 可以重写 | 可以调用父类方法 | 需要额外处理 |
| 能适配 | Adaptee 的所有子类 | Adaptee 及其所有子类 |
| Java 推荐 | ❌(Java 多继承受限) | ✅ |
Java 中几乎总是用对象适配器,因为 Java 不支持多继承。
#四、装饰器 vs 适配器:关键区别🟡
#4.1 本质区别
| 维度 | 装饰器模式 | 适配器模式 |
|---|---|---|
| 目的 | 增加功能 | 接口转换 |
| 包装方式 | 层层叠加 | 一次包装 |
| 接口 | 实现相同接口 | 实现不同接口 |
| 调用关系 | 转发给被包装对象 | 调用被适配对象 |
| 组合顺序 | 可以任意组合 | 通常一次 |
| 使用时机 | 编译时确定 | 编译时确定 |
#4.2 语义上的区别
// 装饰器:层层叠加,接口不变
DataSource ds = new CompressionDecorator(
new EncryptionDecorator(
new FileDataSource("data.txt")
)
);
// ds 的类型仍然是 DataSource
// 可以继续用 DataSource 接口操作
// 适配器:一次转换,接口变化
class LegacyDataAdapter implements DataSource {
private final LegacyDataSystem legacy; // 不兼容的接口
public void request() { // DataSource 接口
legacy.specificRequest(); // LegacyDataSystem 接口
}
}
// 适配器暴露 DataSource 接口,但内部调用 LegacyDataSystem#4.3 Spring 中的装饰器和适配器
// Spring MVC 的适配器模式
// HandlerAdapter:把 Controller 适配到统一的调用接口
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler);
}
// 适配到 @Controller
public class RequestMappingHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof Controller;
}
@Override
public ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
return ((Controller) handler).handleRequest(req, resp);
}
}
// Spring Cache 的装饰器模式
// Cache:抽象缓存接口
// ConcurrentMapCache:装饰了 HashMap 的缓存实现
// EhCache:装饰了 EhCache 的缓存实现#五、生产实战🟡
#5.1 REST API 响应包装
// 装饰器:为所有响应加统一包装
class ApiResponseDecorator implements ResponseWrapper {
private final HttpServletResponse response;
public ApiResponseDecorator(HttpServletResponse response) {
this.response = response;
}
@Override
public void write(String data) throws IOException {
ApiResponse<?> wrapped = ApiResponse.success(data);
response.getWriter().write(toJson(wrapped));
}
}#5.2 多数据源适配
// 适配器:把不同的数据源适配到统一接口
interface UnifiedDataSource {
User findUser(String id);
Order findOrder(String id);
}
// 适配 MySQL
class MySQLDataSource implements UnifiedDataSource {
@Override
public User findUser(String id) {
return mysqlTemplate.queryForObject("SELECT * FROM user WHERE id = ?", id);
}
}
// 适配 MongoDB
class MongoDataSource implements UnifiedDataSource {
@Override
public User findUser(String id) {
return mongoTemplate.findById(id, User.class);
}
}#六、面试总结
#6.1 核心追问
- "装饰器模式和继承相比有什么优势?" —— 灵活组合、避免类膨胀
- "Java I/O 流用了什么设计模式?" —— 装饰器模式
- "装饰器模式和适配器模式的本质区别是什么?" —— 增加功能 vs 接口转换
- "什么时候用类适配器,什么时候用对象适配器?" —— Java 中基本都用对象适配器
#6.2 级别差异
| 级别 | 期望回答 |
|---|---|
| P5 | 能区分装饰器和适配器,能写出基本结构 |
| P6 | 能从 Java I/O 源码分析装饰器,能说出对象适配器优于类适配器 |
| P7 | 能结合 Spring 框架源码分析适配器/装饰器,能正确选型 |