装饰器模式:给对象“穿衣服”的艺术
说实话,我第一次接触装饰器模式时,脑子里冒出的画面就是俄罗斯套娃。一层套一层,每一层都加点新东西。后来在项目中用多了,我发现它更像给对象“穿衣服”——你穿了一件T恤,外面再套件卫衣,冷了还能加个羽绒服。每件衣服都扩展了你的功能,但你还是你。
好,咱们今天就来聊聊这个模式。我会从结构讲起,再跟继承做个对比,最后看看Java I/O流里它是怎么大显身手的。
装饰器模式的结构
装饰器模式的核心思想很简单:不修改原有类,而是通过包装来扩展功能。它由四个角色组成:
- Component(抽象组件):定义对象的接口,可以是抽象类或接口。
- ConcreteComponent(具体组件):被装饰的原始对象。
- Decorator(抽象装饰器):持有Component引用,并实现其接口。
- ConcreteDecorator(具体装饰器):给组件添加额外职责。
我画了张图,你看一眼就明白了:
嗯,这里要注意:装饰器和被装饰对象实现同一个接口。这是关键点。因为这样,装饰器才能“冒充”原始对象,调用方根本不知道自己在跟装饰器打交道。
核心要点:装饰器模式是“组合优于继承”的经典体现。它通过组合的方式,在运行时动态地给对象添加行为。
与继承的对比
咱们来做个对比。假设你要给咖啡加各种配料——牛奶、糖、奶泡。用继承怎么做?
// 继承方式——类爆炸
class Coffee { }
class CoffeeWithMilk extends Coffee { }
class CoffeeWithSugar extends Coffee { }
class CoffeeWithMilkAndSugar extends CoffeeWithMilk { } // 继承链越来越长
class CoffeeWithMilkAndFoam extends CoffeeWithMilk { }
// ... 你想想看,如果有10种配料,得有多少组合?
我当年在一个电商项目里就遇到过类似问题。要给订单计算各种折扣——会员折扣、节日折扣、满减优惠。如果用继承,每个组合都得写一个新类。项目还没上线,类文件已经堆成山了。
用装饰器模式呢?
// 装饰器方式——灵活组合
interface Coffee {
double cost();
String description();
}
class SimpleCoffee implements Coffee {
public double cost() { return 5.0; }
public String description() { return "咖啡"; }
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 1.5; }
public String description() { return coffee.description() + " + 牛奶"; }
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 0.5; }
public String description() { return coffee.description() + " + 糖"; }
}
// 使用
Coffee myCoffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
System.out.println(myCoffee.description()); // 咖啡 + 牛奶 + 糖
System.out.println(myCoffee.cost()); // 7.0
看出区别了吗?
| 对比维度 | 继承 | 装饰器模式 |
|---|---|---|
| 扩展方式 | 编译时静态扩展 | 运行时动态扩展 |
| 类数量 | 组合爆炸(2^n) | 线性增长(n+1) |
| 灵活性 | 低,组合固定 | 高,可任意排列组合 |
| 对原有代码影响 | 需要修改父类 | 无需修改任何现有类 |
我的建议:当你的功能扩展点可能有很多种组合时,优先考虑装饰器模式。但如果你确定扩展点很少(比如只有两三种),继承反而更简单直接。
装饰器模式在Java I/O流中的应用
说到这个,我不得不感叹:Java的I/O流设计者绝对是装饰器模式的忠实粉丝。你看整个java.io包,几乎就是装饰器模式的教科书式实现。
咱们来看个最常见的例子——读取文件内容:
// 原始方式:直接读文件
FileInputStream file = new FileInputStream("data.txt");
// 加一层缓冲,提高性能
BufferedInputStream buffered = new BufferedInputStream(file);
// 再加一层,把字节转成字符
InputStreamReader reader = new InputStreamReader(buffered);
// 再加一层,按行读取
BufferedReader br = new BufferedReader(reader);
// 最终使用
String line = br.readLine();
你看,每一层装饰器都只做一件事:FileInputStream负责读文件,BufferedInputStream负责加缓冲,InputStreamReader负责编码转换,BufferedReader负责按行读取。各司其职,互不干扰。
我曾经在做一个日志分析系统时,需要从压缩文件中读取数据并解密。当时我就是这么干的:
// 真实项目中的组合
InputStream in = new FileInputStream("log.gz");
in = new GZIPInputStream(in); // 解压
in = new BufferedInputStream(in); // 缓冲
in = new CipherInputStream(in, cipher); // 解密
Reader reader = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(reader);
我曾经踩过的坑:装饰器的顺序很重要!比如上面这个例子,必须先解压再解密。如果顺序搞反了,解密出来的就是乱码。另外,记得在最外层关闭流——Java的装饰器会递归调用内层流的close()方法。
Java I/O流中的装饰器模式,说白了就是一套“管道”系统。数据从最内层流进来,每经过一层装饰器就被加工一次,最后变成你想要的格式。这种设计让I/O操作变得极其灵活——你想加缓冲就套一层BufferedInputStream,想加加密就套一层CipherInputStream,完全不用修改现有代码。
我个人觉得,装饰器模式最妙的地方在于:它让“开闭原则”变得触手可及。对扩展开放,对修改关闭——你不需要动任何现有类,只需要写一个新的装饰器类,就能给系统添加新功能。
好,关于装饰器模式就聊到这儿。记住它的核心:组合优于继承,动态扩展,各层解耦。下次你遇到需要给对象层层叠加功能时,不妨想想这个模式。