装饰器模式:给对象“穿衣服”的艺术
说实话,我第一次接触装饰器模式是在看Java I/O源码的时候。当时我盯着new BufferedInputStream(new FileInputStream("test.txt"))这行代码,心里想的是:“这嵌套得也太夸张了吧?”后来我才明白,这种层层包裹的设计,恰恰是装饰器模式的精髓所在。
装饰器模式,说白了就是动态地给一个对象添加额外的职责。它比继承更灵活,因为你可以自由组合各种装饰,而不需要为每一种组合都创建一个子类。
模式结构:四个核心角色
装饰器模式有四个参与者,我习惯把它们比作“装修房子”的过程:
| 角色 | 类比 | 职责 |
|---|---|---|
| 抽象构件(Component) | 毛坯房的标准 | 定义对象的核心接口 |
| 具体构件(ConcreteComponent) | 毛坯房本身 | 实现核心功能 |
| 抽象装饰(Decorator) | 装修公司的模板 | 持有构件引用,实现统一接口 |
| 具体装饰(ConcreteDecorator) | 刷墙、铺地板 | 添加具体的新功能 |
SVG结构图:装饰器模式的核心逻辑
Java代码示例:一个简单的咖啡订单系统
我记得有一次帮朋友做一个小型咖啡店的点单系统。需求很简单:顾客点一杯咖啡,可以加牛奶、加糖、加奶油,每种添加都要算钱。如果用继承,你得写MilkCoffee、SugarCoffee、MilkSugarCoffee……组合一多就爆炸了。装饰器模式正好解决这个问题。
// 1. 抽象构件:饮料
public interface Beverage {
String getDescription();
double cost();
}
// 2. 具体构件:浓缩咖啡
public class Espresso implements Beverage {
@Override
public String getDescription() {
return "浓缩咖啡";
}
@Override
public double cost() {
return 15.0;
}
}
// 3. 抽象装饰:调料装饰器
public abstract class CondimentDecorator implements Beverage {
protected Beverage beverage;
public CondimentDecorator(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
return beverage.getDescription();
}
@Override
public double cost() {
return beverage.cost();
}
}
// 4. 具体装饰:牛奶
public class Milk extends CondimentDecorator {
public Milk(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return beverage.getDescription() + " + 牛奶";
}
@Override
public double cost() {
return beverage.cost() + 3.0;
}
}
// 5. 具体装饰:糖
public class Sugar extends CondimentDecorator {
public Sugar(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return beverage.getDescription() + " + 糖";
}
@Override
public double cost() {
return beverage.cost() + 1.5;
}
}
// 6. 使用示例
public class CoffeeShop {
public static void main(String[] args) {
Beverage espresso = new Espresso();
System.out.println(espresso.getDescription() + " ¥" + espresso.cost());
// 加牛奶
Beverage milkEspresso = new Milk(espresso);
System.out.println(milkEspresso.getDescription() + " ¥" + milkEspresso.cost());
// 加牛奶再加糖
Beverage milkSugarEspresso = new Sugar(milkEspresso);
System.out.println(milkSugarEspresso.getDescription() + " ¥" + milkSugarEspresso.cost());
}
}
浓缩咖啡 ¥15.0
浓缩咖啡 + 牛奶 ¥18.0
浓缩咖啡 + 牛奶 + 糖 ¥19.5
Java I/O流中的装饰器应用
你想想看,Java I/O库为什么设计得那么“嵌套”?其实它就是一个大型的装饰器模式实战现场。InputStream是抽象构件,FileInputStream、ByteArrayInputStream是具体构件,而FilterInputStream就是抽象装饰,BufferedInputStream、DataInputStream、PushbackInputStream都是具体装饰。
// 经典嵌套:带缓冲的文件读取
InputStream in = new BufferedInputStream(
new FileInputStream("data.txt")
);
// 再加一层:读取基本数据类型
DataInputStream dataIn = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.bin")
)
);
// 再加一层:解压缩
InputStream zipIn = new GZIPInputStream(
new BufferedInputStream(
new FileInputStream("data.gz")
)
);
我曾经在做一个日志分析系统时,需要从压缩文件中读取数据,还要做缓冲和行号统计。当时我直接用了三层装饰:LineNumberReader装饰BufferedReader,再装饰InputStreamReader,最后套上GZIPInputStream。代码写出来非常干净,每一层只负责一件事。
operation()方法。结果装饰器只执行了自己的逻辑,原始对象的功能完全丢失了。记住:装饰器一定要调用super.operation()或component.operation(),否则就是“只装修不盖房”。
C++代码示例:同样的咖啡系统
#include <iostream>
#include <memory>
#include <string>
// 抽象构件
class Beverage {
public:
virtual std::string getDescription() const = 0;
virtual double cost() const = 0;
virtual ~Beverage() = default;
};
// 具体构件
class Espresso : public Beverage {
public:
std::string getDescription() const override {
return "浓缩咖啡";
}
double cost() const override {
return 15.0;
}
};
// 抽象装饰
class CondimentDecorator : public Beverage {
protected:
std::unique_ptr<Beverage> beverage;
public:
CondimentDecorator(std::unique_ptr<Beverage> b)
: beverage(std::move(b)) {}
};
// 具体装饰:牛奶
class Milk : public CondimentDecorator {
public:
using CondimentDecorator::CondimentDecorator;
std::string getDescription() const override {
return beverage->getDescription() + " + 牛奶";
}
double cost() const override {
return beverage->cost() + 3.0;
}
};
// 具体装饰:糖
class Sugar : public CondimentDecorator {
public:
using CondimentDecorator::CondimentDecorator;
std::string getDescription() const override {
return beverage->getDescription() + " + 糖";
}
double cost() const override {
return beverage->cost() + 1.5;
}
};
int main() {
auto espresso = std::make_unique<Espresso>();
std::cout << espresso->getDescription()
<< " ¥" << espresso->cost() << std::endl;
auto milkEspresso = std::make_unique<Milk>(std::move(espresso));
std::cout << milkEspresso->getDescription()
<< " ¥" << milkEspresso->cost() << std::endl;
auto milkSugarEspresso = std::make_unique<Sugar>(std::move(milkEspresso));
std::cout << milkSugarEspresso->getDescription()
<< " ¥" << milkSugarEspresso->cost() << std::endl;
return 0;
}
std::unique_ptr管理装饰器链中的对象生命周期,避免内存泄漏。移动语义(std::move)确保所有权正确转移。
装饰器模式 vs 继承:什么时候用哪个?
我个人习惯这样判断:
- 用继承:如果功能扩展是静态的、固定的,比如“所有咖啡都必须有热水”,那就用继承。
- 用装饰器:如果功能组合是动态的、可选的,比如“今天加牛奶,明天加糖,后天两个都要”,那就用装饰器。
举个例子:如果你要做一个“加密+压缩+日志”的数据传输管道,用装饰器模式可以自由组合:new LoggingDecorator(new CompressionDecorator(new EncryptionDecorator(new FileDataSource())))。如果用继承,你得写EncryptedCompressedLoggedFileDataSource这种又臭又长的类名,而且每增加一种组合就要新建一个类。
• 需要给对象动态添加功能,且功能可以撤销
• 功能组合数量巨大,无法用继承穷举
• 不希望修改原有类的代码(开闭原则)
嗯,装饰器模式就讲到这里。它不像单例模式那么“网红”,但绝对是实用派。下次你看到Java I/O里那些层层嵌套的代码,应该就能会心一笑了——原来都是装饰器在“穿衣服”啊。
公众号:蓝海资料掘金营,微信deep3321