什么是装饰器模式?它解决什么问题?
简化版
装饰器模式是在不修改原对象代码的前提下,通过包装对象的方式动态给对象增加新功能。它主要解决继承扩展不灵活、功能组合爆炸、运行时需要按需叠加能力的问题。
详细版
装饰器模式的核心思想是“用组合扩展能力,而不是用继承堆子类”。
典型结构包括:
Component:抽象组件,定义统一接口;ConcreteComponent:具体组件,提供基础能力;Decorator:抽象装饰器,持有一个Component;ConcreteDecorator:具体装饰器,在调用前后增加功能。
示例:
interface Coffee {
String description();
int cost();
}
class PlainCoffee implements Coffee {
public String description() {
return "咖啡";
}
public int cost() {
return 10;
}
}
abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
protected CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
class MilkDecorator extends CoffeeDecorator {
MilkDecorator(Coffee coffee) {
super(coffee);
}
public String description() {
return coffee.description() + " + 牛奶";
}
public int cost() {
return coffee.cost() + 3;
}
}
调用:
Coffee coffee = new MilkDecorator(new PlainCoffee());
System.out.println(coffee.description()); // 咖啡 + 牛奶
System.out.println(coffee.cost()); // 13
如果还要加糖、加奶泡、加冰,可以继续包一层:
Coffee coffee = new SugarDecorator(
new MilkDecorator(
new PlainCoffee()
)
);
装饰器模式适合功能可以按需叠加、组合很多、但不想为每种组合写一个子类的场景。
完整版教学
一、为什么需要装饰器模式
假设你在做咖啡系统。最基础的是普通咖啡,后来用户可以加牛奶、加糖、加奶泡、加冰。
如果用继承,可能会出现这些类:
Coffee
MilkCoffee
SugarCoffee
MilkSugarCoffee
MilkFoamCoffee
IceMilkSugarCoffee
...
功能一多,组合就会爆炸。4 种配料理论上就有很多组合,更多配料时类数量会失控。
装饰器模式不为每个组合写一个类,而是为每个“独立增强能力”写一个装饰器,然后运行时自由组合:
Coffee coffee = new IceDecorator(
new SugarDecorator(
new MilkDecorator(new PlainCoffee())
)
);
这就是装饰器模式最核心的价值:把“能力组合”从继承层级里释放出来。
二、装饰器模式的本质是组合
装饰器和被装饰对象实现同一个接口:
class MilkDecorator implements Coffee
class PlainCoffee implements Coffee
因此装饰器本身也可以被继续装饰。
装饰器内部持有一个 Coffee:
private final Coffee coffee;
它先调用被包装对象,再叠加自己的能力:
return coffee.description() + " + 牛奶";
这就是“递归包装”的结构。每一层装饰器只负责一个小增强,最后组合成完整功能。
三、装饰器模式和开闭原则
开闭原则要求对扩展开放、对修改关闭。
如果要新增“加椰奶”功能,继承方案可能要修改或新增很多组合类。装饰器方案只需要新增一个装饰器:
class CoconutMilkDecorator extends CoffeeDecorator {
public String description() {
return coffee.description() + " + 椰奶";
}
public int cost() {
return coffee.cost() + 4;
}
}
原来的 PlainCoffee、MilkDecorator、SugarDecorator 都不用改。
调用方需要时组合:
Coffee coffee = new CoconutMilkDecorator(new PlainCoffee());
这就是通过组合实现扩展。
四、装饰器模式适合什么场景
装饰器适合这些场景:
- 功能可以拆成多个独立增强;
- 增强可以按需组合;
- 不想用继承产生大量子类;
- 希望运行时决定叠加哪些能力;
- 每个增强都能遵循同一个接口。
典型例子包括:
- Java IO 流;
- Web 请求过滤和包装;
- 日志、缓存、压缩、加密组合;
- UI 组件动态加边框、滚动条;
- 业务对象按需增加校验、格式化、统计能力。
五、装饰器模式的边界
装饰器不是万能增强器。它要求增强逻辑能围绕同一个接口展开。
如果每个功能都改变接口,或者每个增强之间强依赖复杂顺序,装饰器会变得难读。
另外,装饰器层数太多时,调试调用链会比较费劲。比如:
new ADecorator(new BDecorator(new CDecorator(new DDecorator(target))))
这时要考虑是否需要工厂、配置化装配,或者换成责任链、管道等结构。
六、组合数量为什么会爆炸
有 4 种可选配料时理论组合数是 2^4=16;增加到 10 种后变成 2^10=1024,靠继承为每种组合建类不可维护。
Plain -> Milk -> Sugar -> Ice; each layer implements Component
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 装饰器减少的是组合子类,不会消除每种独立职责本身的实现成本。 |
| 适用边界 | 装饰器适合彼此可组合的正交职责;若步骤有复杂跳转、共享状态或严格编排,管道、责任链或显式工作流更清晰。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:装饰器减少的是组合子类,不会消除每种独立职责本身的实现成本。
八、常见误区与追问
- 误区:装饰器会让类数量变成零。 每种独立增强通常仍需要一个类;它避免的是为增强组合建立大量子类。
- 误区:装饰器可以任意改变组件接口。 透明装饰依赖共同的 Component 契约;随意增加只能向下转型调用的方法,会破坏可替换性和继续组合的能力。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:如何证明一个包装结构是装饰器? 包装者与组件遵守同一契约,并把调用委托给被包装对象后叠加职责,且可继续嵌套。
- 追问:装饰器链越长,扩展性就一定越好吗? 不一定;链越长,顺序、异常传播和排障成本越高,应由装配层集中管理并限制职责粒度。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
装饰器模式的记忆锚点是:基础对象负责核心能力,装饰器负责一层一层叠加附加能力。它用组合替代继承,让功能可以在运行时自由拼装,避免组合类爆炸。