Java设计模式之装饰器模式
文章目录
- Java设计模式之装饰器模式
-
- 前言
- 一、装饰器模式核心思想
- [二、装饰器模式 vs 代理模式](#二、装饰器模式 vs 代理模式)
- [三、Java IO流中的装饰器模式](#三、Java IO流中的装饰器模式)
- 四、装饰器模式在真实项目中的应用
- 总结
- [✅ 亮点总结](#✅ 亮点总结)
- 适用场景
- 扩展方向
前言
在咖啡店里,你点了一杯浓缩咖啡,可以加牛奶、加摩卡、加奶泡,每种配料都在基础价格上叠加费用,而最终顾客喝到的仍是一杯"咖啡"。这种"不改变原始对象,通过层层包装来扩展功能"的思想,就是装饰器模式(Decorator Pattern)。它与代理模式外形相似但目的截然不同,本文将从辨析入手,深入剖析装饰器在Java IO流中的经典应用。
装饰器 vs 继承 :如果不用装饰器模式,你会怎么实现"浓缩咖啡+牛奶+摩卡"?一种做法是创建EspressoWithMilk、EspressoWithMocha、EspressoWithMilkAndMocha等子类------这就是"类爆炸"问题,3种配料就要2^3=8个子类,4种配料就要16个------完全没有可维护性。装饰器模式用组合替代继承,将每种配料设计为独立的装饰器类,可以任意组合叠加。这也是《Design Patterns》中"组合优于继承"原则的经典体现。
一、装饰器模式核心思想
装饰器模式的核心是:动态地给一个对象添加额外的职责,同时不改变其接口。
java
// 组件接口
interface Coffee {
String getDescription();
double cost();
}
// 具体组件(被装饰的主体)
class Espresso implements Coffee {
@Override
public String getDescription() {
return "浓缩咖啡";
}
@Override
public double cost() {
return 15.0;
}
}
class HouseBlend implements Coffee {
@Override
public String getDescription() {
return "混合咖啡";
}
@Override
public double cost() {
return 12.0;
}
}
// 抽象装饰器 - 持有组件的引用
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee; // 被装饰的对象
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double cost() {
return coffee.cost();
}
}
// 具体装饰器:牛奶
class Milk extends CoffeeDecorator {
public Milk(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + 牛奶";
}
@Override
public double cost() {
return coffee.cost() + 3.0;
}
}
// 具体装饰器:摩卡
class Mocha extends CoffeeDecorator {
public Mocha(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + 摩卡";
}
@Override
public double cost() {
return coffee.cost() + 5.0;
}
}
// 具体装饰器:奶泡
class Whip extends CoffeeDecorator {
public Whip(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + 奶泡";
}
@Override
public double cost() {
return coffee.cost() + 4.0;
}
}
使用装饰器模式构建一杯"浓缩咖啡+牛奶+摩卡+奶泡":
java
public class CoffeeShop {
public static void main(String[] args) {
// 基础咖啡
Coffee coffee = new Espresso();
System.out.println(coffee.getDescription() + ": ¥" + coffee.cost());
// 浓缩咖啡: ¥15.0
// 层层装饰
coffee = new Milk(coffee);
coffee = new Mocha(coffee);
coffee = new Whip(coffee);
System.out.println(coffee.getDescription() + ": ¥" + coffee.cost());
// 浓缩咖啡 + 牛奶 + 摩卡 + 奶泡: ¥27.0
}
}
这种设计的精妙之处在于:咖啡配料可以任意组合,新增配料只需添加一个装饰器类,完全符合开闭原则。
顺序重要吗? 在装饰器模式中,装饰器的叠加顺序有时会影响结果。比如先加密再压缩和先压缩再加密,最终得到的数据完全不同。虽然装饰器模式本身不强制顺序,但在实际使用中,你需要根据业务逻辑确定装饰器的调用链。Java IO流中也有类似情况:你应该先包装BufferedInputStream再包DataInputStream,而不是反过来,因为DataInputStream会做小批量读取,需要底层的缓冲来提升性能。
二、装饰器模式 vs 代理模式
这两个模式在结构上非常相似------都持有目标对象的引用,都实现相同的接口。但它们的目的截然不同:
| 维度 | 装饰器模式 | 代理模式 |
|---|---|---|
| 核心目的 | 增强功能/添加职责 | 控制访问/屏蔽细节 |
| 增强内容 | 使用者知道增强了什么(加牛奶) | 使用者不知道代理做了什么(日志) |
| 关注点 | 扩展对象的能力 | 控制对象的访问 |
| 关系 | 运行时动态组合(可多层) | 一般一对一静态绑定 |
| 典型例子 | Java IO流 | Spring AOP |
一句话总结:装饰器是"锦上添花",代理是"偷梁换柱"。
如何记忆这个区别? 一个简单的口诀------"装饰器是透明的,代理是不透明的"。装饰器的使用者在代码中清醒地知道自己在包装什么(new Milk(new Espresso())),知道加了牛奶;而代理的使用者通常拿到的是一个已经创建好的代理对象,不知道背后有代理的存在。在Spring AOP中,你注入的Service对象其实是个代理,但代码中完全感知不到------这就是"不透明"。
三、Java IO流中的装饰器模式
Java的java.io包是装饰器模式最经典的应用,几乎所有流都通过装饰器扩展功能:
java
import java.io.*;
// 基础组件:文件输入流(只能按字节读取)
InputStream fileInput = new FileInputStream("data.txt");
// 装饰器1:缓冲(添加缓冲区,提升性能)
BufferedInputStream bufferedInput = new BufferedInputStream(fileInput);
// 装饰器2:数据读取(添加按基本类型读取的能力)
DataInputStream dataInput = new DataInputStream(bufferedInput);
// 链式调用写法
DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.txt")));
再看一个实际的文件读写示例,体会IO流中装饰器的嵌套之美:
java
import java.io.*;
public class IODecoratorDemo {
public static void main(String[] args) {
String filePath = "demo.txt";
// 写入文件(装饰器嵌套)
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(filePath), "UTF-8"))) {
writer.write("Hello 装饰器模式");
writer.newLine();
writer.write("Java IO流是装饰器模式的经典应用");
} catch (IOException e) {
e.printStackTrace();
}
// 读取文件(装饰器嵌套)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream(filePath), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
来分析这个写入链new BufferedWriter(new OutputStreamWriter(new FileOutputStream(p), "UTF-8"))中每层装饰器的职责:
- FileOutputStream:基础组件,将字节写入文件
- OutputStreamWriter:装饰器,将字符流转换为字节流,指定UTF-8编码
- BufferedWriter:装饰器,添加缓冲功能,批量写入减少IO次数
每一层只负责一件单一功能,通过层层包装组合出所需的能力。这比创建一个"带缓冲带编码的文件写入器"更加灵活、可复用。
IO流的装饰器嵌套有一个问题:关闭顺序 。幸运的是,Java IO流的装饰器设计考虑了这一点------只需关闭最外层的装饰器流,它会自动调用内层流的close()。比如new BufferedWriter(new OutputStreamWriter(new FileOutputStream(p))),关闭BufferedWriter就会级联关闭OutputStreamWriter和FileOutputStream。所以在try-with-resources中,只需将最外层流声明在try括号里即可。但注意,如果你手动调用多个流的close(),千万不要先关内层再关外层------外层的flush()和close()依赖内层流,先关内层会导致IOException: Stream Closed。
四、装饰器模式在真实项目中的应用
以Web开发中的数据加密传输为例:
java
// 基础数据处理器
interface DataProcessor {
String process(String data);
}
class PlainDataProcessor implements DataProcessor {
@Override
public String process(String data) {
return data;
}
}
// 装饰器:压缩
class CompressDecorator extends AbstractDecorator {
public CompressDecorator(DataProcessor processor) {
super(processor);
}
@Override
public String process(String data) {
String processed = super.process(data);
return "[压缩]" + processed + "[/压缩]";
}
}
// 装饰器:加密
class EncryptDecorator extends AbstractDecorator {
public EncryptDecorator(DataProcessor processor) {
super(processor);
}
@Override
public String process(String data) {
String processed = super.process(data);
return "[加密]" + processed + "[/加密]";
}
}
// 抽象装饰器基类
abstract class AbstractDecorator implements DataProcessor {
protected DataProcessor processor;
public AbstractDecorator(DataProcessor processor) {
this.processor = processor;
}
@Override
public String process(String data) {
return processor.process(data);
}
}
// 测试
public class DataPipelineDemo {
public static void main(String[] args) {
// 构建处理链:加密 → 压缩
DataProcessor pipeline = new EncryptDecorator(
new CompressDecorator(
new PlainDataProcessor()));
String result = pipeline.process("用户敏感数据123");
System.out.println(result);
// 输出: [加密][压缩]用户敏感数据123[/压缩][/加密]
}
}
总结
装饰器模式的核心优势在于组合优于继承 。通过将功能拆分为独立的装饰器类,可以像搭积木一样灵活组合出所需的功能。Java IO流是装饰器模式的教科书级应用,理解了IO流的设计,也就真正掌握了装饰器模式。它与代理模式形似而神异:装饰器强调功能增强 ,代理强调访问控制。在实际开发中,当需要为对象动态叠加多种能力时,装饰器模式是你的首选方案。
一点补充:装饰器模式也有它的"阿喀琉斯之踵"------装饰器类和被装饰对象必须实现相同的接口,这意味着如果原始接口设计得不好(方法过多或不合理),装饰器也必须实现所有这些方法。此外,如果想用装饰器移除某个功能(而不是增强),它也无能为力------装饰器只能"做加法",不能"做减法"。在这些场景下,代理模式或适配器模式可能是更好的选择。
✅ 亮点总结
- 装饰器模式完美诠释"组合优于继承"------功能叠加用组合,避免子类爆炸
- Java IO流的装饰器体系剖析:
BufferedInputStream(new FileInputStream(path))就是层层装饰 - 数据管道实战:加密 → 压缩 → 基础处理,三个装饰器自由组合出6种处理链
- 抽象装饰器基类(
AbstractDecorator)的设计,减少具体装饰器的重复代码 - 装饰器 vs 代理模式的核心区别------功能增强 vs 访问控制,面试重点辨析
适用场景
- 数据处理管道------日志脱敏 + 加密 + 压缩,多步处理自由编排顺序
- 中间件拦截链------按需叠加缓存、限流、鉴权等功能到服务调用上
- UI组件扩展------基础文本框 + 滚动条装饰器 + 边框装饰器 + 阴影装饰器
扩展方向
- Java IO源码阅读 :从
InputStream→FilterInputStream→BufferedInputStream的继承链理解装饰器模式 - 责任链模式对比:另一个支持功能叠加的模式,与装饰器的职责差异和各自适用场景
- Spring中的装饰器应用 :
TransactionAwareCacheDecorator、ServerHttpRequestDecorator等Spring内部装饰器(推荐阅读上一篇:Java设计模式之观察者模式)