组合优于继承的核心是降低系统脆弱性,通过将易变部分(如虚方法、protected字段、默认行为、构造依赖)抽为接口并组合使用,保留继承于稳定is-a关系、纯数据类或框架强制基类,运行时动态切换策略更抗压。

组合优于继承不是一句空话,核心作用之一就是降低系统变量的脆弱性——特别是当基类稍有改动,大量子类就意外崩坏时。关键不在于“不用继承”,而在于把容易变的部分抽出来,用组合隔离变化。
识别哪些变量最容易引发脆弱性
这些变量一旦修改,极易波及整个继承链:
- 被子类重写的虚方法(尤其是流程控制类方法,比如 Process、Validate)
- protected 字段或属性(子类直接读写,等于暴露了内部状态)
- 基类中新增的默认行为(如加一个自动日志、默认拦截逻辑)
- 构造函数中隐式依赖的初始化顺序
用组合替换“可变基类”的典型做法
不靠父类承载行为,而是让主类持有行为对象,通过接口通信:
- 把订单处理逻辑拆成 IOrderValidator、IOrderTaxCalculator、IOrderNotifier 等接口
- OrderProcessor 类只持有一组策略对象,不继承任何业务基类
- 不同场景下传入不同实现:比如测试时用 StubValidator,海外订单用 VATCalculator
- 新增“在线且免税”类型?只需新增一个 TaxCalculator 实现,无需动 OrderProcessor 或建新子类
保留继承的合理位置
继承不该消失,但应退守到真正稳定、语义清晰的层次:
- 领域内明确的 is-a 关系,且不会随业务演进改变(例如:AdminUser 是 User,这个事实不会变)
- 纯数据容器类,仅含字段和简单 getter/setter(如 BaseEntity
带 Id 和 CreatedAt) - 框架强制要求的基类(如 ASP.NET Core 的 ControllerBase),此时组合用于填充业务逻辑,而非替代它
运行时切换比编译时继承更抗压
组合天然支持动态替换,这是继承做不到的:
- 同一个 OrderProcessor 实例,可在下单前 setTaxCalculator(new ZeroTax()),支付后 setNotifier(new SmsNotifier())
- 通过 DI 容器按环境注入不同实现(开发用内存队列,生产用 Kafka)
- 故障降级时,临时替换成 FailFastNotifier,不影响主流程结构










