架构风格与模式58 条

组合优于继承

优先通过对象组合与委托复用行为,而非继承:继承在编译期绑定、破坏封装且层级僵化;当类型关系稳定且确实存在 is-a 语义时才考虑继承。

为什么组合优于继承

继承在编译期绑定,子类被迫接受父类的全部实现细节,父类一改就可能波及所有子类;组合在运行期可替换,只依赖被组合对象的接口,行为复用不必暴露内部结构。层级越深的继承树,改动的影响面越难评估。

什么时候仍用继承

当类型关系稳定且确实存在 is-a 语义时,继承仍然合适,例如领域模型中明确的分类。判断标准有两条:子类能否替换父类而不破坏语义,也就是里氏替换;父类的全部行为对子类是否都有意义。只要出现为了复用代码而继承,通常就该改成组合加委托。

两者对比

维度继承组合
绑定时机编译期确定运行期可替换
封装性子类依赖父类实现细节只依赖被组合对象的接口
变化影响父类改动波及全部子类替换实现即可,影响局部
适用场景稳定且真实的 is-a行为复用与策略变化
测试难度父类依赖链难隔离可注入替身单独测试

常见替代手法

策略模式把算法族抽成接口,运行期选择实现
装饰器模式在委托链上叠加功能,不修改原类
模板方法用组合的步骤对象替代抽象基类的钩子
默认方法在接口上提供默认实现,避免为复用而建基类

典型使用场景

  • 多种计价或折扣规则:用策略组合替代子类枚举
  • 日志与埋点增强:用装饰器叠加而不改原实现
  • 跨平台实现:按平台组合不同适配器
  • 框架回调:用组合的钩子对象替代强制继承基类
设计模式经典著作中即建议优先使用对象组合:策略、装饰器、桥接等模式都是这条原则的具体体现,它们把变化点从类型层级搬到对象之间的委托关系上。

易错点

  • 继承层级超过三层后,改动一个父类方法的影响面难以评估
  • 为复用代码而继承而非表达语义关系,子类继承了大量不适用的父类方法
  • 用继承实现能力开关,导致某些子类必须抛出异常来屏蔽父类行为
  • 组合对象的生命周期无人管理,出现共享实例被意外修改的问题

数据来源与参考

  • Gamma et al.《Design Patterns》(1994)
  • 《The Pragmatic Programmer》
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌