优先选组合而非继承,因组合更灵活、可测试性好、语义清晰;继承仅适用于满足“is-a”关系且父类明确为继承设计的情形,如AbstractList;误用如Dog extends DatabaseConnection违背语义,属典型坏味道。
优先选组合,慎用继承。继承容易导致紧耦合、破坏封装,而组合更灵活、可测试性更好、语义更清晰。
什么时候该用继承
仅当满足“
is-a
”关系,且父类是为继承而设计的(比如明确标注了
接口、有文档说明可被子类重写、提供
等钩子)。
Java 的
是为继承设计的,可以安全扩展
但
不是——它没承诺任何继承契约,直接继承它属于误用
如果只是想复用逻辑,却强行让
,就违背了语义,是典型坏味道
组合怎么写才真正有效
组合不是简单地在类里 new 一个对象,关键在于:通过接口依赖、控制生命周期、暴露必要能力。
依赖抽象而非具体类型:用
接口,而不是
类
外部可替换:测试时能轻松注入 mock,生产时可切换 Stripe 实现
不暴露内部细节:比如
持有
,但不把 sender 的 connect() 或 retry() 方法透给上层调用
继承的常见陷阱
看似省代码,实则埋雷:
脆弱基类问题
:父类一改,所有子类行为可能意外变化(比如加了个新 protected 方法,恰好和子类同名,引发覆盖或编译错误)
构造顺序陷阱
:子类构造器中调用被重写的方法,此时父类字段还没初始化完毕,易出 NullPointerException
无法多重复用
:Java 不支持多继承,而一个类常需多种能力(如可缓存 + 可重试 + 可熔断),组合天然支持横向叠加
一个实用判断流程
遇到复用需求时,快速自问三句:
它真的是“是一种”关系吗?(
不是“有一个”或“用一下”
)
父类是否公开声明支持继承?有没有配套的 hook 和文档?
如果明天要换掉这个能力(比如短信通知换成邮件),继承方案要改几处?组合方案改几处?
多数时候,第三问的答案会告诉你:组合更轻、更稳、更可持续。
protectedtemplate methodAbstractListArrayListDog extends DatabaseConnectionPaymentProcessorPayPalProcessorOrderServiceNotificationSender