原型继承表达“是什么”,组合表达“有什么”;判断依据是“一种”还是“有一个”关系,需结合语义、运行时灵活性、可继承性及长期维护成本综合选择。

原型继承和组合模式不是“二选一”的语法技巧,而是两种不同语义关系的表达方式——一个讲的是“是什么”,另一个讲的是“有什么”。用错场景,代码会越来越难改、难测、难理解。
看关系本质:是“一种”还是“有一个”
这是最直接的判断起点。
- 如果 A 是 B 的一种具体形式(比如 Penguin 是 Bird),且业务上需要统一处理、多态调用(如所有鸟类都可能调用
.fly(),哪怕企鹅重写为报错),那原型继承(class Penguin extends Bird)语义成立 - 如果 A 用到了 B 的能力,但 B 并不构成 A 的类型身份(比如 OrderService 使用 PaymentProcessor),那就该用组合:把
paymentProcessor作为字段持有,通过委托调用,不产生类型层级 - 注意:“Bird 有羽毛”是组合,“Bird 是 Animal”才是继承候选;混淆这两者,容易把本该封装的细节暴露成继承契约
看运行时灵活性:能否换实现
组合天然支持替换,原型继承则在实例创建时就锁死了行为来源。
- 测试时要注入 mock(比如用
MockPaymentProcessor替换真实支付器)→ 必须组合 - 环境配置决定依赖(开发用内存缓存,生产连 Redis)→ 组合 + 明确接口定义更稳妥
- 如果所有逻辑完全固定、从不更换、也不需隔离测试,原型继承写起来确实更少代码,但代价是失去扩展弹性
看被复用对象是否“设计为可继承”
不是所有类都适合当父类。强行继承普通工具类或业务类,常踩坑。
- 检查它是否有文档说明“可子类化”,是否提供
protected钩子方法,是否定义了隐式契约(比如必须先调init()) - 父类字段是
public或默认访问?继承会把内部状态直接暴露给子类;组合可通过私有字段 + 清晰接口隐藏实现细节 - 经典反例:
Stack extends Vector——Stack 只需 push/pop,却被迫继承整个 Vector 的扩容逻辑和全部 API,违背单一职责
看长期维护成本:改一处,崩多少
继承链越深,牵一发而动全身;组合结构扁平,新增或调整行为更安全。
- 加一个“会游泳但不会飞的鸟”,继承体系得重构抽象层(拆出
Flyable/Swimmable),改多个子类;组合只需新写SwimBehavior,赋给对应实例即可 - 父类删掉一个非
final方法,所有子类可能立刻失效;组合只依赖公开方法签名,只要接口不变,实现怎么换都稳 - 类型层级一旦铺开,后期想转成组合往往要重写大量构造逻辑;而从组合起步,必要时再抽基类也更可控











