为什么建议多用组合少用继承?
基本是看你对设计原则的理解是不是只停留在背诵。我自己在实践中踩过坑,所以会这么答。
🔹 这句“金句”到底在说什么
“组合”指在一个类里持有另一个类的引用,把活委托它干。“继承”就是 extends,子类直接拥有父类的东西。
说“多用组合少用继承”,核心是 别一上来就想着靠 extends 复用代码,先想想能不能让一个类包含另一个类来解决问题。这不是绝对真理,是权衡后的倾向。
🔹 继承有哪些让人半夜改bug的问题
1️⃣ 白盒复用,破坏封装
继承把父类内部细节全暴露给子类。父类改个方法的实现,子类就可能悄悄崩掉——这种耦合叫“脆弱的基类”。你 override 一个方法,还得猜父类其他方法是不是内部调用了它,牵一发动全身。
2️⃣ 编译时绑定,灵活不了
子类从一出生就跟父类锁死了,运行时换不了行为。比如你写了个“会飞的鸭子”继承 Duck,以后想临时让它不会飞?没法动态拆。而用组合,换个翅膀对象就行。
3️⃣ 类爆炸
用继承覆盖所有组合维度,类数量会指数级爆炸。经典例子:
有颜色(红/蓝)、形状(圆/方)、材质——如果每种搭配都继承,就得写 RedCircleWood、BlueSquareMetal…… 噩梦。组合可以把颜色、形状、材质各自独立,随意拼装。
4️⃣ 只能单继承(Java),机会成本太高
extends 只能有一个父类,如果仅仅为了复用几行代码就占用了这个宝贵名额,以后真正需要继承抽象类的时候就没位置了。
5️⃣ 测试困难
子类依赖父类,写单元测试时常常不得不把父类那一堆初始化也拉进来,很难单独 mock。
🔹 组合强在哪里
✅ 黑盒复用:只依赖对象的公开接口,内部怎么改不影响调用方。
✅ 运行时灵活:可以随时换掉持有的对象,甚至给一个 null 就取消功能。
✅ 职责单一:每个类只干自己的事,然后像乐高一样拼起来。
✅ 更好的测试:依赖注入进来,单元测试轻松 mock 组件。
🔹 一个实际例子说清对比
假设要设计一个日志模块,要求既能打印到控制台,也能发到远程服务器。
用继承你大概会这样:
-
父类 Logger,实现控制台打印
-
子类 RemoteLogger 继承 Logger,覆盖方法加入网络发送
-
某天需要同时打印控制台 且 发远程 → 再新建 ConsoleAndRemoteLogger?后面再来个 Kafka 日志又得加一堆子类,很快失控。
用组合的思路:
-
接口 LogAppender,有
append(msg) -
ConsoleAppender、RemoteAppender、KafkaAppender 各实现
-
Logger 持有
List<LogAppender>,调用时遍历每个 appender 把消息发出去
想增加或关闭某个输出通道,只需配置 List 里的对象就行,Logger 核心逻辑一行不改。这就是组合的威力。