装饰器模式:定义、结构、实现、与继承的区别、优缺点、应用场景
装饰器模式,说实话,是我个人非常喜欢的一个模式。为什么?因为它太实用了。你想想看,我们写代码的时候,经常遇到一个场景:要给一个已有的对象增加新的功能,但又不想修改它的代码。这时候,装饰器模式就派上用场了。
我记得有一次,我在做一个日志系统。核心的日志记录器已经写好了,但客户突然要求给日志加上时间戳、线程ID、甚至加密功能。如果改核心代码,那风险太大了。后来我用装饰器模式,一层层包装,完美解决了问题。
什么是装饰器模式?
说白了,装饰器模式就是动态地给一个对象添加一些额外的职责。就跟你给手机套个壳、贴个膜一样——手机还是那个手机,但功能(保护、美观)增加了。
从定义上看,装饰器模式属于结构型模式。它提供了一种比继承更灵活的扩展方式,无需修改原有代码,就能给对象添加新功能。
模式结构
装饰器模式的结构其实不复杂,我画个图你就明白了。
结构中的几个角色,我简单说一下:
- Component:抽象组件,定义了对象的接口。可以是接口或抽象类。
- ConcreteComponent:具体组件,实现了核心业务逻辑。这是我们要装饰的对象。
- Decorator:抽象装饰类,持有Component的引用,并实现其接口。
- ConcreteDecorator:具体装饰类,给组件添加新的功能。
代码实现
光说不练假把式。咱们直接上代码,用Java实现一个简单的咖啡订单系统。嗯,这里要注意,咖啡本身是核心,加料就是装饰。
// 1. 抽象组件
public interface Coffee {
double cost();
String description();
}
// 2. 具体组件
public class Espresso implements Coffee {
@Override
public double cost() {
return 5.0;
}
@Override
public String description() {
return "浓缩咖啡";
}
}
// 3. 抽象装饰类
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double cost() {
return coffee.cost();
}
@Override
public String description() {
return coffee.description();
}
}
// 4. 具体装饰类 - 加牛奶
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 2.0;
}
@Override
public String description() {
return super.description() + " + 牛奶";
}
}
// 5. 具体装饰类 - 加糖
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 1.5;
}
@Override
public String description() {
return super.description() + " + 糖";
}
}
// 6. 客户端使用
public class Client {
public static void main(String[] args) {
Coffee coffee = new Espresso();
System.out.println(coffee.description() + " 价格: " + coffee.cost());
// 加牛奶
coffee = new MilkDecorator(coffee);
// 再加糖
coffee = new SugarDecorator(coffee);
System.out.println(coffee.description() + " 价格: " + coffee.cost());
// 输出: 浓缩咖啡 + 牛奶 + 糖 价格: 8.5
}
}
💡 我的小技巧:在实际项目中,我习惯把装饰器的构造方法写成链式调用风格。这样客户端代码看起来更清爽,就像
new SugarDecorator(new MilkDecorator(new Espresso()))。
装饰器 vs 继承
很多人会问:我直接用继承不也能扩展功能吗?为什么非要用装饰器?
这个问题问得好。我举个例子你就明白了。
| 对比维度 | 继承 | 装饰器 |
|---|---|---|
| 扩展方式 | 静态编译期决定 | 动态运行时组合 |
| 灵活性 | 低,子类数量爆炸 | 高,自由组合 |
| 职责单一 | 子类承担多种职责 | 每个装饰类只做一件事 |
| 对原有代码影响 | 需要修改父类 | 完全不需要修改 |
| 适用场景 | 明确稳定的层次结构 | 需要动态添加功能 |
你想想看,如果咖啡有10种配料,用继承你得写多少子类?2的10次方,1024个!而用装饰器,只需要10个装饰类,自由组合就行。
⚠️ 我曾经踩过的坑:有一次我过度使用装饰器,给一个对象套了七八层。结果调试的时候,调用栈深得吓人,排查问题花了整整一天。后来我给自己定了个规矩:装饰层数不超过3层,超过就考虑重构。
优缺点分析
任何模式都不是银弹,装饰器也一样。
优点
- 开闭原则:对扩展开放,对修改关闭。不用改原有代码就能加功能。
- 灵活组合:运行时动态组合,想加就加,想减就减。
- 职责单一:每个装饰类只负责一个额外功能,符合单一职责原则。
- 避免类爆炸:比继承的类数量少得多。
缺点
- 增加复杂度:多了很多小类,新手看了容易懵。
- 调试困难:多层包装后,调用链变长,排查问题费劲。
- 类型问题:如果客户端依赖具体类型,装饰器会破坏类型信息。
应用场景
我个人在实际项目中,主要在以下几个场景用装饰器模式:
- IO流处理:Java的BufferedInputStream、DataInputStream等,就是典型的装饰器。
- 日志增强:给核心业务方法加上日志、性能监控、权限校验等。
- 缓存系统:给数据访问层加上缓存装饰,不影响原有查询逻辑。
- UI组件:给按钮、文本框等组件添加边框、滚动条、阴影等效果。
- 中间件架构:像Express、Koa的中间件机制,本质上就是装饰器思想。
核心要点总结:
- 装饰器模式的核心是组合优于继承
- 每个装饰类只做一件事,保持单一职责
- 装饰层数不宜过多,建议不超过3层
- 适合需要动态、灵活扩展功能的场景
好了,装饰器模式就讲到这里。说白了,它就是给对象「穿衣服」的艺术。你学会了吗?