设计模式之装饰模式
装饰模式(Decorator Pattern)是结构型设计模式里很有代表性的一员,它的核心思想是:在不改变原有对象结构的情况下,动态地给对象添加新的功能。
有点像在奶茶里加珍珠加布丁,不需要重新定义「珍珠奶茶类」「布丁奶茶类」,而是通过一层层“装饰”来实现。
一、为什么需要装饰模式?
假设你有一个 MilkTea
接口:
public interface MilkTea {String getDescription();double cost();
}
如果用户要点「珍珠奶茶」「布丁奶茶」「珍珠布丁奶茶」,直观的做法是写很多子类:
PearlMilkTea
PuddingMilkTea
PearlAndPuddingMilkTea
……
这会导致 类爆炸问题,因为每多一种配料,组合数量就指数级增加。
装饰模式就解决了这个问题:我们不在继承树上不断扩展,而是通过组合(composition)和包装(wrapping)来动态增强对象功能。
二、装饰模式的结构
装饰模式有四个核心角色:
Component(组件接口)
定义对象的抽象接口,比如 MilkTea。ConcreteComponent(具体组件)
实现接口的基本功能,比如SimpleMilkTea
。Decorator(装饰抽象类)
持有一个Component
引用,并且实现相同接口,用来“套娃”。ConcreteDecorator(具体装饰类)
在调用被装饰对象的方法基础上,增加新行为,比如 PearlDecorator
、PuddingDecorator
。
三、代码示例(奶茶)
1. 组件接口
public interface MilkTea {String getDescription();double cost();
}2. 具体组件:基础奶茶
public class SimpleMilkTea implements MilkTea {@Overridepublic String getDescription() {return "原味奶茶";}@Overridepublic double cost() {return 8.0; // 基础价格}
}3. 抽象装饰类
public abstract class MilkTeaDecorator implements MilkTea {protected MilkTea milkTea;public MilkTeaDecorator(MilkTea milkTea) {this.milkTea = milkTea;}@Overridepublic String getDescription() {return milkTea.getDescription();}@Overridepublic double cost() {return milkTea.cost();}
}4. 具体装饰:加珍珠
public class PearlDecorator extends MilkTeaDecorator {public PearlDecorator(MilkTea milkTea) {super(milkTea);}@Overridepublic String getDescription() {return super.getDescription() + ", 珍珠";}@Overridepublic double cost() {return super.cost() + 2.0; // 珍珠加价}
}5. 具体装饰:加布丁
public class PuddingDecorator extends MilkTeaDecorator {public PuddingDecorator(MilkTea milkTea) {super(milkTea);}@Overridepublic String getDescription() {return super.getDescription() + ", 布丁";}@Overridepublic 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;@Overridepublic 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;}@Overridepublic 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];}@Overridepublic 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 组件装饰、日志增强 |
一句话区分:
代理模式:重点是“拦路虎”——先过我这一关,再去找目标对象。
装饰模式:重点是“打补丁”——原本能做的事还照样能做,但我在周围加了点料。
装饰模式本质是 组合优于继承 的典型实践,用“层层套娃”的方式解决继承树爆炸的问题。