结论是对象关系复杂时优先用「组合替代继承」+「接口抽象隔离」,因继承强制 is-a 关系而现实多为 has-a 或 uses-a,深继承链易破坏封装、违反里氏替换原则、导致维护失控,而组合配合接口可实现高内聚、低耦合、易扩展与测试。

直接说结论:对象关系复杂时,优先用「组合替代继承」+「接口抽象隔离」,而不是堆更多继承层级或强行塞进一个巨型类里。
为什么继承链一深就失控
继承表达的是严格的 is-a 关系,但现实模型里多数关系其实是 has-a 或 uses-a。比如 Car 不是 Engine 的一种,而是“有一个”引擎;Logger 也不该继承 FileWriter,它只是“用”文件写入能力。
常见错误现象:
- 修改基类一个
virtual函数,十几个派生类编译不过 -
dynamic_cast到处飞,靠运行时类型判断走不同逻辑 - 构造函数参数膨胀到 8 个以上,还带默认值和重载
根本原因:把职责耦合进了继承结构里。Liskov 替换原则(LSP)被悄悄违反了——子类已不能安全替换父类。
用组合 + 接口把大对象拆成可插拔部件
把“对象拥有什么能力”和“对象是什么”分开。能力抽成接口,由独立对象实现;主类只持有一个或多个接口指针/引用。
实操建议:
- 把每个可变行为(如序列化、校验、日志、策略计算)定义为纯虚接口,例如
ISerializer、IValidator - 主类(如
Order)不继承这些行为,而是持有std::unique_ptr<iserializer></iserializer>成员 - 构造时注入具体实现:
Order(std::make_unique<jsonserializer>())</jsonserializer>,切换格式只需换实现,不动Order本身 - 避免裸指针;用
std::shared_ptr或std::unique_ptr明确所有权
这样做的好处:新增一种序列化方式,只加一个新类 + 注入即可,Order 完全不用改,符合开闭原则(OCP)。
哪些地方最容易误用继承而该换成组合
以下场景几乎总是更适合组合:
- 需要在运行时切换行为:比如
CompressionStrategy(GzipCompressor/ZstdCompressor),用继承就得搞出OrderWithGzip、OrderWithZstd这种爆炸式子类 - 类已有多个父类意图:C++ 不支持多继承接口以外的多重 is-a,硬凑会导致菱形继承或语义混乱
- 测试困难:继承树越深,mock 基类越麻烦;而组合对象可直接传 mock 实现
- 生命周期不一致:比如
DatabaseConnection和ReportGenerator生命周期不同,强行继承会逼你做资源管理妥协
注意:不是禁止继承,而是问一句——这个子类是否真的“是一种”父类?如果不是,别用 public 继承。
接口设计要小而聚焦,别贪大求全
一个接口如果超过 3 个方法,大概率职责过重。比如 IDataProcessor 同时包含 load()、validate()、transform()、save(),就违背了接口隔离原则(ISP)。
更合理的做法:
-
ILoader:只管load() -
IValidator:只管validate() -
ITransformer:只管transform() - 上层协调者(如
Pipeline)按需组合它们
这样每个接口都容易 mock、复用、单独测试;也避免客户代码被迫依赖它根本不用的方法。
最常被忽略的一点:组合不是简单地把一堆 std::unique_ptr 塞进类里就完事——必须明确每个部件的契约(输入/输出/异常)、生命周期归属、线程安全边界。否则只是把混乱从继承层转移到了成员层。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











