优先选择组合而非继承,因为多数复用场景是“拥有”关系而非“属于”关系;继承仅适用于稳定明确的is-a关系,而组合更灵活、易测试、易替换且利于隔离变化。

优先选择组合,不是因为继承不好,而是因为大多数复用场景本质上是“拥有”而非“属于”。当你想让一个类具备某种能力、行为或职责时,组合更自然、更可控、也更贴近真实业务逻辑。
看关系本质:是不是“一种”,而不是“能不能用”
继承只适用于明确且稳定的 is-a 关系。比如 Dog 是 Animal,ArrayList 是 List 的一种实现——这种类型归属清晰、语义牢固,且子类能安全替代父类(里氏替换原则成立)。
如果只是想复用某段逻辑,比如“订单要计算运费”“用户要发短信”,这些都不是“订单是一种运费计算器”,而是“订单 有 一个运费计算器”“用户 有 一个通知服务”。强行用继承,会把职责错位、接口污染、测试变重。
看变化频率:哪个部分更容易改?
业务规则、算法策略、外部依赖(如支付、物流、存储)往往高频变动。把这些逻辑塞进继承链里,等于把变化钉死在编译期。
- 用继承:改一次运费规则,可能要动基类、测所有子类、发布整条继承树
- 用组合:定义
ShippingCalculator接口,新增FreeForVIPCalculator类,注入即可,原有代码完全不动
组合把可变部分封装成独立组件,变化被隔离,系统更稳定。
看测试与替换:能不能轻松 mock 或换实现?
继承结构下,子类常依赖父类构造器、初始化顺序、protected 成员甚至内部状态。单元测试容易变成集成测试,启动慢、失败难定位。
组合天然支持依赖注入:
- 测试时直接传入
MockPaymentProcessor - 生产环境切换为
AlipayProcessor或WechatProcessor - 甚至运行时根据用户等级动态选策略
这种灵活性,继承做不到。
看设计意图:父类是否真的为你而生?
不是所有类都适合被继承。JDK 中 String、LocalDateTime 都是 final;Vector 虽可继承,但它的 public 方法(如 addElement())并不适合作为栈的接口——这正是早期 Stack extends Vector 被废弃的根本原因。
真正可继承的父类,通常满足:
- JavaDoc 明确写有 “This class is designed for inheritance”
- 提供 protected 钩子方法、空参构造器、清晰的扩展契约
- 没有隐藏的不变量依赖或 fragile 的 protected 字段
不符合这些条件,就别硬套继承。











