组合优于继承是提醒优先用“拥有”而非“成为”,关键在语义准确(has-a/is-a)、耦合可控、变化可隔离;Car has an Engine用组合,Car is a Vehicle才用继承;推荐private final字段+构造器注入+接口抽象,避免空指针与循环依赖。

组合优于继承不是让继承退出历史舞台,而是提醒你:当复用功能、扩展行为时,优先考虑“拥有”而非“成为”。关键不在语法怎么写,而在语义是否准确、耦合是否可控、变化是否可隔离。
明确“has-a”还是“is-a”关系
这是判断起点。如果类A需要B的能力,但A本质上不是B的一种,就别继承——哪怕看起来很像。
- Car has an Engine → 用组合;Car is a Vehicle → 继承才合理
- Stack is not a Vector(它只是用了列表存储),所以JDK后来重写了Stack,内部组合List,只暴露push/pop
- OrderService has a PaymentProcessor,不是PaymentProcessor的一种,必须组合
用private final字段 + 构造器注入
这是最稳妥的组合落地方式。它把依赖关系显式化、不可变、可校验。
- 字段声明为
private final PaymentProcessor processor,杜绝外部篡改和空状态 - 构造器中强制传入,并用
Objects.requireNonNull(processor)提前拦截null - 避免在类里写
new PaymentProcessorImpl()——这会锁死实现,无法替换或mock - Spring等框架中,默认启用构造器注入,天然支持该模式
抽离接口,面向协议协作
组合不是简单塞个具体类,而是通过抽象接口解耦。接口粒度要准,命名要反映业务意图。
- 定义
PaymentStrategy接口,而不是IPaymentHandler——前者聚焦策略选择,后者易泛化成万能工具 - Car类不持有
EngineImpl,而是Engine接口;后续可无缝换成ElectricEngine或HybridEngine - 不要让组合类转发所有底层方法。比如
OrderService暴露processPayment()即可,不必透出strategy.validate()或formatter.format()
警惕组合中的常见陷阱
组合虽好,用错一样出问题。
-
空指针风险:可选组件(如GPS)用
Optional<GPS>包装,或明确文档说明“未安装则跳过导航” - 循环依赖:A持有B,B又持有A → 启动失败。应引入第三方协调者,或改用事件/回调解耦
- 生命周期失控:组合对象由宿主管理,但若它自身需初始化(如连接池start()),要在宿主构造后统一触发
- 过度拆分:不是每个小逻辑都要独立成类。判断标准是:这个能力是否可能被多个类复用?是否可能单独演进或替换?

















