设计模式之装饰模式

装饰模式(Decorator Pattern)是结构型设计模式里很有代表性的一员,它的核心思想是:在不改变原有对象结构的情况下,动态地给对象添加新的功能

有点像在奶茶里加珍珠加布丁,不需要重新定义「珍珠奶茶类」「布丁奶茶类」,而是通过一层层"装饰"来实现。


一、为什么需要装饰模式?

假设你有一个 MilkTea 接口:

复制代码
public interface MilkTea {
    String getDescription();
    double cost();
}

如果用户要点「珍珠奶茶」「布丁奶茶」「珍珠布丁奶茶」,直观的做法是写很多子类:

  • PearlMilkTea

  • PuddingMilkTea

  • PearlAndPuddingMilkTea

    ......

这会导致 类爆炸问题,因为每多一种配料,组合数量就指数级增加。

装饰模式就解决了这个问题:我们不在继承树上不断扩展,而是通过组合(composition)和包装(wrapping)来动态增强对象功能


二、装饰模式的结构

装饰模式有四个核心角色:

  1. Component(组件接口)

    定义对象的抽象接口,比如 MilkTea。

  2. ConcreteComponent(具体组件)

    实现接口的基本功能,比如 SimpleMilkTea

  3. Decorator(装饰抽象类)

    持有一个 Component 引用,并且实现相同接口,用来"套娃"。

  4. ConcreteDecorator(具体装饰类)

    在调用被装饰对象的方法基础上,增加新行为,比如 PearlDecorator、PuddingDecorator


三、代码示例(奶茶)

复制代码
1. 组件接口
public interface MilkTea {
    String getDescription();
    double cost();
}

2. 具体组件:基础奶茶
public class SimpleMilkTea implements MilkTea {
    @Override
    public String getDescription() {
        return "原味奶茶";
    }

    @Override
    public double cost() {
        return 8.0; // 基础价格
    }
}

3. 抽象装饰类
public abstract class MilkTeaDecorator implements MilkTea {
    protected MilkTea milkTea;

    public MilkTeaDecorator(MilkTea milkTea) {
        this.milkTea = milkTea;
    }

    @Override
    public String getDescription() {
        return milkTea.getDescription();
    }

    @Override
    public double cost() {
        return milkTea.cost();
    }
}

4. 具体装饰:加珍珠
public class PearlDecorator extends MilkTeaDecorator {
    public PearlDecorator(MilkTea milkTea) {
        super(milkTea);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 珍珠";
    }

    @Override
    public double cost() {
        return super.cost() + 2.0; // 珍珠加价
    }
}

5. 具体装饰:加布丁
public class PuddingDecorator extends MilkTeaDecorator {
    public PuddingDecorator(MilkTea milkTea) {
        super(milkTea);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 布丁";
    }

    @Override
    public double cost() {
        return super.cost() + 1.0; // 布丁加价
    }
}

6. 使用示例
public class Main {
    public static void main(String[] args) {
        MilkTea baseTea = new SimpleMilkTea();
        System.out.println(baseTea.getDescription() + " => ¥" + baseTea.cost());

        // 加珍珠
        MilkTea pearlTea = new PearlDecorator(baseTea);
        System.out.println(pearlTea.getDescription() + " => ¥" + pearlTea.cost());

        // 再加布丁
        MilkTea pearlPuddingTea = new PuddingDecorator(pearlTea);
        System.out.println(pearlPuddingTea.getDescription() + " => ¥" + pearlPuddingTea.cost());
    }
}

输出:

原味奶茶 => ¥8.0

原味奶茶, 珍珠 => ¥10.0

原味奶茶, 珍珠, 布丁 => $11.0

小结

SimpleMilkTea → 基础奶茶

PearlDecorator、PuddingDecorator → 装饰器,可以自由叠加

组合灵活,避免写出大量继承类(如 珍珠布丁奶茶、双珍珠奶茶)


四、装饰模式的特点

优点:

  • 灵活:运行时可动态组合功能,而非编译时固定继承。

  • 遵循开闭原则(OCP):不修改原有类,就能增强功能。

  • 可无限层叠组合,比如「奶茶 + 双份珍珠 + 双份布丁」。

缺点:

  • 层数过多时,调试、排查比较麻烦。

  • 对象包装链过长时,可能影响性能。


五、实际应用场景: Java IO 库

InputStreamBufferedInputStreamDataInputStream 就是典型的装饰模式。每一层包装为原始流提供新功能。

1. 结构回顾:装饰模式骨架

  • Component(抽象组件)InputStream 抽象类

  • ConcreteComponent(具体组件)FileInputStreamByteArrayInputStream ...

  • Decorator(抽象装饰类)FilterInputStream

  • ConcreteDecorator(具体装饰类)BufferedInputStreamDataInputStreamPushbackInputStream ...


2. 源码入口:InputStream

复制代码
public abstract class InputStream implements Closeable {
    public abstract int read() throws IOException;
    // 还有一些 read(byte[])、skip() 等默认实现
}

这里定义了数据读取的抽象接口,所有输入流都得实现。


3. 被装饰的具体组件:FileInputStream

复制代码
public class FileInputStream extends InputStream {
    private final FileDescriptor fd;
    
    @Override
    public int read() throws IOException {
        return read0();
    }

    private native int read0() throws IOException;
}

这是最基础的流,直接从文件里读取字节。

功能很"原始",没有缓冲、没有数据类型转换。


4. 装饰抽象类:FilterInputStream

复制代码
public class FilterInputStream extends InputStream {
    protected volatile InputStream in;

    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

    @Override
    public int read() throws IOException {
        return in.read();  // 委托给被装饰对象
    }
}

关键点:

  • 它持有一个 InputStream in

  • 所有方法都是转发调用(即套娃)。

  • 它本身不加功能,只是"抽象层",为子类装饰器铺路。


5. 具体装饰:BufferedInputStream

复制代码
public class BufferedInputStream extends FilterInputStream {
    private static int DEFAULT_BUFFER_SIZE = 8192;
    protected volatile byte buf[];
    protected int count;
    protected int pos;

    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }

    public BufferedInputStream(InputStream in, int size) {
        super(in);
        buf = new byte[size];
    }

    @Override
    public int read() throws IOException {
        if (pos >= count) {
            fill();  // 从底层 InputStream 批量读取
            if (pos >= count) return -1;
        }
        return buf[pos++] & 0xff;  // 从缓冲区读
    }
}

这里的增强逻辑是 "缓冲"

  • 底层 FileInputStream 一次只能读一个字节 → 效率低。

  • BufferedInputStream 会一次性把数据读到内存缓冲区,再一个个返回 → 减少系统调用,大幅提升性能。


6. 另一个具体装饰:DataInputStream

复制代码
public class DataInputStream extends FilterInputStream implements DataInput {
    public DataInputStream(InputStream in) {
        super(in);
    }

    public final int readInt() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        int ch3 = in.read();
        int ch4 = in.read();
        if ((ch1 | ch2 | ch3 | ch4) < 0)
            throw new EOFException();
        return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4));
    }
}

这里的增强逻辑是 "数据类型解析"

  • 底层 InputStream 只会提供原始字节

  • DataInputStream 能把字节组合成 intlongUTF 字符串 等高级数据类型。


7. 使用示例

复制代码
InputStream in = new FileInputStream("data.bin");

// 加缓冲
InputStream buffered = new BufferedInputStream(in);

// 再加数据类型解析
DataInputStream dataIn = new DataInputStream(buffered);

int magic = dataIn.readInt();   // 直接读 int
String msg = dataIn.readUTF(); // 直接读 UTF 字符串

调用链路:
FileInputStream → BufferedInputStream → DataInputStream

这就是装饰模式的典型应用:层层包装,动态增强


8. 总结:Java IO 与装饰模式

  1. 继承树避免爆炸 :如果要在所有流都支持"缓冲+数据解析",继承会爆炸(BufferedFileDataInputStream 之类),装饰模式完美解决。

  2. 灵活组合 :你可以只用 BufferedInputStream,也可以只用 DataInputStream,也可以两者结合。

  3. 职责分离:每个装饰器只关心自己的增强逻辑,保持单一职责。


六、代理模式 vs 装饰模式

维度 代理模式(Proxy) 装饰模式(Decorator)
设计意图 控制对对象的访问,隔离真实对象 动态地给对象添加新功能
客户端视角 客户端以为直接在用目标对象,实际经过代理 客户端以为直接在用目标对象,实际经过装饰
关注点 "能不能访问、如何访问" "功能增强、行为叠加"
典型职责 远程代理、虚拟代理、保护代理(如权限检查、延迟加载、远程调用) 在不修改类的情况下增强功能(如日志、缓存、加密、IO 缓冲)
实现方式 代理对象持有真实对象的引用,并在方法调用时控制调用过程 装饰对象持有被装饰对象的引用,并在方法调用前后添加逻辑
行为变化 方法结果通常保持一致,只是访问路径受控 方法结果通常增强或变化,功能比原来更多
类结构相似度 与装饰模式几乎一致 与代理模式几乎一致
典型案例 Spring AOP(JDK Proxy / CGLIB)、RPC 桩、MyBatis Mapper 动态代理 Java IO (BufferedInputStreamDataInputStream)、GUI 组件装饰、日志增强

一句话区分:

  • 代理模式:重点是"拦路虎"------先过我这一关,再去找目标对象。

  • 装饰模式:重点是"打补丁"------原本能做的事还照样能做,但我在周围加了点料。


装饰模式本质是 组合优于继承 的典型实践,用"层层套娃"的方式解决继承树爆炸的问题。