组合优于继承的关键在于厘清“is-a”与“has-a”关系;非天然继承关系(如订单与商品)应使用组合,配合接口、构造器注入、委托控制和策略切换实现灵活可维护的设计。

直接用组合替代继承,关键不是“换个写法”,而是重新思考类之间的关系——是“是一种”还是“有一个”。业务代码里多数复用场景其实属于后者。
判断要不要用组合:先问是不是“is-a”
如果两个类之间不是天然的“是一种”关系,比如订单和商品、用户和地址、支付服务和微信SDK,就别用继承。强行 extends 会把语义搞混,后续改起来特别容易出错。
- 正确示例:AdminUser 是 User 的一种 → 可以继承(但也要看父类是否设计为可扩展)
- 错误示例:Order 有多个 Product → Order 不是 Product 的一种,该用组合
- 再比如:Car 有 Engine,但 Car 不是 Engine,所以 Car 持有 Engine 实例,而不是 extends Engine
组合怎么写才安全:private final + 构造器注入
别在类里 new 具体实现,也别暴露内部对象。用 final 字段 + 构造器传入,既保证不可变,又方便测试时替换 mock。
- 字段声明写成 private final PaymentProcessor processor;
- 构造器里加校验:Objects.requireNonNull(processor, "processor must not be null");
- 不要提供 public setProcessor(),除非真需要运行时切换策略(如灰度发布)
- 避免写 public PaymentProcessor getProcessor(),否则外部可能绕过你的业务逻辑直接调用
委托方法别偷懒,要真正控制行为
组合不是简单把字段塞进去就完事,重点是通过委托把调用权收回来,才能加日志、校验、重试或降级逻辑。
- 比如 OrderService 调用 processor.pay() 前,可以统一加风控检查
- pay() 失败时,可以封装成业务异常,而不是把底层 SDK 异常直接抛给上层
- 不要写 return processor.pay(...) 就完事,中间留好扩展点
配合接口实现多态和策略切换
继承靠 extends 实现多态,组合靠接口 + 注入实现。谁决定用哪个实现?不是类自己 new,而是上层(Spring 容器、工厂、配置中心)来决定。
- 定义接口:interface PaymentStrategy { void execute(Order order); }
- 不同渠道实现:WechatPayStrategy、AlipayStrategy
- 业务类只依赖接口:private final PaymentStrategy strategy;
- 运行时换策略:Spring Profile 切换 Bean,或根据 order.channel 动态选择
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











