装饰模式(Decorator Pattern)是结构型设计模式里很有代表性的一员,它的核心思想是:在不改变原有对象结构的情况下,动态地给对象添加新的功能。
有点像在奶茶里加珍珠加布丁,不需要重新定义「珍珠奶茶类」「布丁奶茶类」,而是通过一层层"装饰"来实现。
一、为什么需要装饰模式?
假设你有一个 MilkTea
接口:
public interface MilkTea {
String getDescription();
double cost();
}
如果用户要点「珍珠奶茶」「布丁奶茶」「珍珠布丁奶茶」,直观的做法是写很多子类:
-
PearlMilkTea
-
PuddingMilkTea
-
PearlAndPuddingMilkTea
......
这会导致 类爆炸问题,因为每多一种配料,组合数量就指数级增加。
装饰模式就解决了这个问题:我们不在继承树上不断扩展,而是通过组合(composition)和包装(wrapping)来动态增强对象功能。
二、装饰模式的结构
装饰模式有四个核心角色:
-
Component(组件接口)
定义对象的抽象接口,比如 MilkTea。
-
ConcreteComponent(具体组件)
实现接口的基本功能,比如
SimpleMilkTea
。 -
Decorator(装饰抽象类)
持有一个
Component
引用,并且实现相同接口,用来"套娃"。 -
ConcreteDecorator(具体装饰类)
在调用被装饰对象的方法基础上,增加新行为,比如 Pearl
Decorator
、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 库
InputStream
、BufferedInputStream
、DataInputStream
就是典型的装饰模式。每一层包装为原始流提供新功能。
1. 结构回顾:装饰模式骨架
-
Component(抽象组件) →
InputStream
抽象类 -
ConcreteComponent(具体组件) →
FileInputStream
、ByteArrayInputStream
... -
Decorator(抽象装饰类) →
FilterInputStream
-
ConcreteDecorator(具体装饰类) →
BufferedInputStream
、DataInputStream
、PushbackInputStream
...
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
能把字节组合成int
、long
、UTF 字符串
等高级数据类型。
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 与装饰模式
-
继承树避免爆炸 :如果要在所有流都支持"缓冲+数据解析",继承会爆炸(
BufferedFileDataInputStream
之类),装饰模式完美解决。 -
灵活组合 :你可以只用
BufferedInputStream
,也可以只用DataInputStream
,也可以两者结合。 -
职责分离:每个装饰器只关心自己的增强逻辑,保持单一职责。
六、代理模式 vs 装饰模式
维度 | 代理模式(Proxy) | 装饰模式(Decorator) |
---|---|---|
设计意图 | 控制对对象的访问,隔离真实对象 | 动态地给对象添加新功能 |
客户端视角 | 客户端以为直接在用目标对象,实际经过代理 | 客户端以为直接在用目标对象,实际经过装饰 |
关注点 | "能不能访问、如何访问" | "功能增强、行为叠加" |
典型职责 | 远程代理、虚拟代理、保护代理(如权限检查、延迟加载、远程调用) | 在不修改类的情况下增强功能(如日志、缓存、加密、IO 缓冲) |
实现方式 | 代理对象持有真实对象的引用,并在方法调用时控制调用过程 | 装饰对象持有被装饰对象的引用,并在方法调用前后添加逻辑 |
行为变化 | 方法结果通常保持一致,只是访问路径受控 | 方法结果通常增强或变化,功能比原来更多 |
类结构相似度 | 与装饰模式几乎一致 | 与代理模式几乎一致 |
典型案例 | Spring AOP(JDK Proxy / CGLIB)、RPC 桩、MyBatis Mapper 动态代理 | Java IO (BufferedInputStream 、DataInputStream )、GUI 组件装饰、日志增强 |
一句话区分:
-
代理模式:重点是"拦路虎"------先过我这一关,再去找目标对象。
-
装饰模式:重点是"打补丁"------原本能做的事还照样能做,但我在周围加了点料。
装饰模式本质是 组合优于继承 的典型实践,用"层层套娃"的方式解决继承树爆炸的问题。