装饰器模式相比继承有什么优势?
简化版
装饰器模式相比继承更灵活,因为它可以在运行时按需组合多个功能,而继承通常在编译期固定类层级。装饰器用组合替代继承,能减少子类爆炸,也更符合开闭原则。
详细版
继承适合表达稳定的“是什么”关系,例如 Dog extends Animal。但如果用继承表达很多可组合功能,很容易产生类爆炸。
例如一个文本处理器支持:
- 加密;
- 压缩;
- 日志;
- 缓存;
- 统计。
用继承可能出现:
EncryptedTextProcessor
CompressedTextProcessor
EncryptedCompressedTextProcessor
CachedEncryptedCompressedTextProcessor
...
装饰器模式可以把每种增强拆成独立装饰器:
TextProcessor processor = new CacheDecorator(
new CompressDecorator(
new EncryptDecorator(
new PlainTextProcessor()
)
)
);
优势:
- 每个增强独立,职责清楚;
- 能在运行时决定组合;
- 新增增强时不改原类;
- 避免为每种组合创建子类;
- 更容易复用单个增强能力。
但装饰器也有成本:对象层数更多,调用链更长,调试时要理解组合顺序。所以不是所有继承都要替换成装饰器,只有“功能组合变化多”时才更适合。
完整版教学
一、继承的问题不是继承本身
继承不是坏东西。它适合表达稳定的抽象层级。
例如:
class Dog extends Animal
class Cat extends Animal
这种关系比较自然。
问题出在用继承表达“可选功能组合”。比如饮料加料:
Coffee
MilkCoffee
SugarCoffee
MilkSugarCoffee
IceMilkSugarCoffee
这里 MilkSugarCoffee 并不是一种非常稳定的领域类型,它只是“咖啡 + 牛奶 + 糖”的组合。
当你用继承表达组合时,子类数量会随着功能数量快速增长。
二、组合为什么更灵活
组合的思路是把每个功能做成一个对象,然后运行时拼起来。
Coffee coffee = new SugarDecorator(
new MilkDecorator(
new PlainCoffee()
)
);
这里不需要 MilkSugarCoffee 类。牛奶和糖各自是一个装饰器,它们可以组合,也可以单独使用。
新增“椰奶”时,只需要新增:
class CoconutDecorator extends CoffeeDecorator { ... }
不用修改已有类,也不用新增所有带椰奶组合的子类。
三、装饰器如何体现开闭原则
开闭原则强调:新增功能尽量通过扩展,而不是修改已有代码。
用继承组合功能时,新增一个功能可能要影响很多组合类。
用装饰器时,新增一个功能只新增一个装饰器:
class LogDecorator extends ProcessorDecorator {
public String process(String input) {
long start = System.currentTimeMillis();
try {
return delegate.process(input);
} finally {
System.out.println("cost=" + (System.currentTimeMillis() - start));
}
}
}
原来的处理器不用改,其他装饰器也不用改。
这就是对扩展开放、对修改关闭。
四、继承和装饰器的选择边界
如果对象关系稳定,继承更简单。
比如:
abstract class Shape
class Circle extends Shape
class Rectangle extends Shape
这不一定需要装饰器。
如果变化点是功能叠加,装饰器更合适。
比如:
- 输入流叠加缓冲、解压、解密;
- 文本处理叠加过滤、格式化、统计;
- HTTP 客户端叠加重试、日志、签名;
- UI 组件叠加滚动条、边框、阴影。
判断关键是:这是稳定分类,还是可选能力组合?
五、装饰器也不是没有代价
装饰器的代价包括:
- 对象层数变多;
- 调用链不如单个类直观;
- 顺序错误会导致行为错误;
- 过度装饰会让代码难读;
- 调试时需要展开每一层包装。
因此,当功能组合不复杂时,简单继承或直接实现可能更好。
好的设计不是“永远用组合”,而是知道什么时候继承简单,什么时候组合灵活。
六、组合与继承的扩展维度
3 种主体乘 4 种增强,继承组合最坏要 3×2^4=48 个组合类;装饰方案约需 3 个主体类加 4 个装饰类。
inheritance: fixed subclass tree; decorator: runtime object graph
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | “组合优于继承”不是禁用继承,而是避免用继承表达会独立变化的多个维度。 |
| 适用边界 | 稳定且天然是 is-a 的核心差异可用继承;需要按实例、按运行时自由组合的附加职责更适合装饰。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:“组合优于继承”不是禁用继承,而是避免用继承表达会独立变化的多个维度。
八、常见误区与追问
- 误区:继承在设计模式中一定是不好的。 继承适合稳定分类和模板骨架;问题在于用它承载大量正交功能组合。
- 误区:装饰器可以任意改变组件接口。 透明装饰依赖共同的 Component 契约;随意增加只能向下转型调用的方法,会破坏可替换性和继续组合的能力。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:装饰器是否一定比继承更简单? 组合种类多时更灵活,但对象链、装配和调试也会增加复杂度。
- 追问:装饰器链越长,扩展性就一定越好吗? 不一定;链越长,顺序、异常传播和排障成本越高,应由装配层集中管理并限制职责粒度。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
继承适合稳定分类,装饰器适合可选能力组合。只要你发现为了不同功能组合不断创建子类,就该想到装饰器;它用一层层包装替代一堆组合子类。