为什么组合优于继承
继承在编译期绑定,子类被迫接受父类的全部实现细节,父类一改就可能波及所有子类;组合在运行期可替换,只依赖被组合对象的接口,行为复用不必暴露内部结构。层级越深的继承树,改动的影响面越难评估。
什么时候仍用继承
当类型关系稳定且确实存在 is-a 语义时,继承仍然合适,例如领域模型中明确的分类。判断标准有两条:子类能否替换父类而不破坏语义,也就是里氏替换;父类的全部行为对子类是否都有意义。只要出现为了复用代码而继承,通常就该改成组合加委托。
两者对比
| 维度 | 继承 | 组合 |
|---|---|---|
| 绑定时机 | 编译期确定 | 运行期可替换 |
| 封装性 | 子类依赖父类实现细节 | 只依赖被组合对象的接口 |
| 变化影响 | 父类改动波及全部子类 | 替换实现即可,影响局部 |
| 适用场景 | 稳定且真实的 is-a | 行为复用与策略变化 |
| 测试难度 | 父类依赖链难隔离 | 可注入替身单独测试 |
常见替代手法
| 策略模式 | 把算法族抽成接口,运行期选择实现 |
|---|---|
| 装饰器模式 | 在委托链上叠加功能,不修改原类 |
| 模板方法 | 用组合的步骤对象替代抽象基类的钩子 |
| 默认方法 | 在接口上提供默认实现,避免为复用而建基类 |
典型使用场景
- 多种计价或折扣规则:用策略组合替代子类枚举
- 日志与埋点增强:用装饰器叠加而不改原实现
- 跨平台实现:按平台组合不同适配器
- 框架回调:用组合的钩子对象替代强制继承基类
设计模式经典著作中即建议优先使用对象组合:策略、装饰器、桥接等模式都是这条原则的具体体现,它们把变化点从类型层级搬到对象之间的委托关系上。