装饰器模式有哪些角色?调用流程是什么?
简化版
装饰器模式通常包含抽象组件、具体组件、抽象装饰器和具体装饰器四类角色。调用时客户端面向抽象组件,实际请求会一层层经过装饰器,最后到达具体组件,再把结果逐层返回并增强。
详细版
装饰器模式的四个核心角色是:
Component:抽象组件,定义统一接口;ConcreteComponent:具体组件,提供原始功能;Decorator:抽象装饰器,实现同一个接口,并持有Component;ConcreteDecorator:具体装饰器,在调用前后增加功能。
调用链示意:
Client
↓
DecoratorB
↓
DecoratorA
↓
ConcreteComponent
返回时结果再反向经过每层装饰器:
ConcreteComponent 返回结果
↑
DecoratorA 增强结果
↑
DecoratorB 再增强结果
↑
Client 拿到最终结果
示例:
Component component = new ConcreteDecoratorB(
new ConcreteDecoratorA(
new ConcreteComponent()
)
);
component.operation();
每个装饰器只依赖 Component 接口,因此既可以包装真实组件,也可以包装另一个装饰器。这是装饰链能够成立的关键。
完整版教学
一、抽象组件 Component
Component 是装饰器模式的统一入口。
interface DataSource {
String read();
void write(String data);
}
客户端不关心拿到的是原始数据源,还是被压缩、加密、缓存包装过的数据源。只要它实现 DataSource,就可以使用。
这个接口非常关键。如果装饰器和真实对象没有共同接口,就不能透明替换。
二、具体组件 ConcreteComponent
具体组件提供基础能力。
class FileDataSource implements DataSource {
public String read() {
return "file data";
}
public void write(String data) {
System.out.println("write file: " + data);
}
}
它只做核心业务,不关心加密、压缩、日志等附加功能。
这样设计的好处是职责清楚。文件数据源负责读写文件;装饰器负责额外能力。
三、抽象装饰器 Decorator
抽象装饰器一般也实现 Component,并持有一个 Component:
abstract class DataSourceDecorator implements DataSource {
protected final DataSource delegate;
protected DataSourceDecorator(DataSource delegate) {
this.delegate = delegate;
}
public String read() {
return delegate.read();
}
public void write(String data) {
delegate.write(data);
}
}
抽象装饰器的作用是减少重复代码。具体装饰器只重写自己要增强的方法。
四、具体装饰器 ConcreteDecorator
具体装饰器负责真正增强。
class EncryptionDecorator extends DataSourceDecorator {
EncryptionDecorator(DataSource delegate) {
super(delegate);
}
public String read() {
String data = delegate.read();
return decrypt(data);
}
public void write(String data) {
delegate.write(encrypt(data));
}
}
如果再有压缩功能,可以新增:
class CompressionDecorator extends DataSourceDecorator { ... }
然后自由组合:
DataSource source = new CompressionDecorator(
new EncryptionDecorator(
new FileDataSource()
)
);
五、调用流程为什么是一层层的
因为每个装饰器都持有下一个 Component。
执行:
source.write("hello");
如果 source 是 CompressionDecorator,调用先进入压缩装饰器;压缩后调用 EncryptionDecorator;加密后再调用 FileDataSource。
这就是装饰链。
顺序非常重要。压缩后加密,和加密后压缩,结果可能完全不同。面试时如果能主动提到装饰顺序,答案会更有工程味。
六、四类角色如何闭环
基础咖啡 10 元,经牛奶装饰加 3 元、糖装饰加 2 元,最外层 cost() 递归得到 15 元,每层只贡献自己的增量。
Client -> ConcreteDecorator -> Decorator.component -> ConcreteComponent
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 角色名可以变化,判断模式要追踪接口契约和委托链,不能机械数类。 |
| 适用边界 | 抽象 Decorator 可复用委托代码,但不是语法强制;关键是共同接口、对象组合和可递归包装。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:角色名可以变化,判断模式要追踪接口契约和委托链,不能机械数类。
八、常见误区与追问
- 误区:没有抽象 Decorator 类就不算装饰器模式。 可以直接让具体装饰器实现 Component 并持有 Component,抽象基类只是减少重复。
- 误区:装饰器可以任意改变组件接口。 透明装饰依赖共同的 Component 契约;随意增加只能向下转型调用的方法,会破坏可替换性和继续组合的能力。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:最外层对象为什么仍能当作 Component 使用? 每一层都实现同一接口,满足可替换性,因此客户端无需知道内部层数。
- 追问:装饰器链越长,扩展性就一定越好吗? 不一定;链越长,顺序、异常传播和排障成本越高,应由装配层集中管理并限制职责粒度。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
装饰器模式的角色可以记成:接口定规矩,具体组件做基础能力,抽象装饰器保存被包装对象,具体装饰器负责加一层能力。调用是一层层进去,再一层层返回。