装饰器与适配器模式

一个日志打印引发的血案

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 核心追问

  1. "装饰器模式和继承相比有什么优势?" —— 灵活组合、避免类膨胀
  2. "Java I/O 流用了什么设计模式?" —— 装饰器模式
  3. "装饰器模式和适配器模式的本质区别是什么?" —— 增加功能 vs 接口转换
  4. "什么时候用类适配器,什么时候用对象适配器?" —— Java 中基本都用对象适配器

6.2 级别差异

级别期望回答
P5能区分装饰器和适配器,能写出基本结构
P6能从 Java I/O 源码分析装饰器,能说出对象适配器优于类适配器
P7能结合 Spring 框架源码分析适配器/装饰器,能正确选型