组合 vs 继承:“不要使用继承”这句话的真正含义
学习面向对象时,一定会遇到这样一句话:“优先使用组合,而不是继承(Favor composition over inheritance)。”
先说结论,这句话并不是“绝对不要使用继承”,而是“不要为了复用代码而滥用继承”。继承仍然是有效的工具,但在需要复用的场景中,大多数时候组合是更安全的选择。
本文会结合我的经验,说明两者在实际代码中的区别,以及什么时候该选择哪一种。
继承和组合有什么区别?
我们先用非常简单的方式区分一下。
继承是“~是~(is-a)”关系。比如,狗是动物。子类会直接继承父类的功能。
组合是“~拥有~(has-a)”关系,比如汽车拥有发动机。它把具备所需功能的对象放在内部,再让该对象完成工作。
结合代码会更容易理解。下面是通过继承获得功能的方式。
// 继承: Stack会 NSMutableArray继承所有方法
class Stack: NSMutableArray {
func push(_ o: Any) { add(o) }
func pop() -> Any {
let o = lastObject!
removeLastObject()
return o
}
}
问题是,这样一来,Stack 连不希望对外暴露的 insert(_:at:)、removeObject(at:) 等方法也会全部暴露出去。
下面是将同样的实现改为组合后的样子。
// 组合:只选择必要的功能并进行委托
class Stack {
private var list: [Any] = []
func push(_ o: Any) { list.append(o) }
func pop() -> Any { list.removeLast() }
}
我们把数组放在内部,只对外开放必要的操作。Stack 只保留真正需要完成的事情。
“不要使用继承”这句话的真正含义
这句话的背景,是继承存在两个典型缺点。
第一,封装会被破坏。子类会依赖父类的内部实现。父类代码一旦发生变化,原本运行正常的子类也可能突然出问题。
第二,耦合度会变得过高。父类和子类在编译时被紧密绑定,之后很难改变两者的关系。
继承不是复用代码的工具,而是定义类型的工具。
这句话是关键。只因为想复用代码就使用继承时,关系就开始变得复杂。
正方形和矩形就是一个著名例子。从数学上看,正方形是矩形,因此似乎可以使用继承。但一旦继承了矩形“分别修改宽度和高度”的功能,正方形就不再是正方形了。
即使看起来是 is-a 关系,只要连行为都无法完全替代,继承就会成为陷阱。
那么,什么时候可以使用继承?
继承并非绝对不好。如果同时满足以下所有条件,使用继承反而更简洁。
- 是真正的 is-a 关系吗:子类始终是父类的一种吗?
- 遵守里氏替换原则吗:把子类放到父类的位置,是否完全没有问题?
- 父类是否以继承为前提进行设计:是否有文档说明,并且为扩展保持开放?
如果三项都符合,使用继承也没问题。扩展 UIViewController 这类基于框架的类,就是典型案例。
反过来,只要有一项不明确,就先考虑组合。
我用表格做了一个简单比较。
| 场景 | 建议 |
|---|---|
| 纯粹的 is-a 关系,且可以完全替换 | 继承 |
| 只想复用代码时 | 组合 |
| 想在运行时改变行为时 | 组合 |
| 需要组合多项功能时 | 组合 |
可以看到,实际工作中遇到的大多数场景都倾向于组合。“favor composition”这个建议并非无缘无故。
我在实际工作中会这样判断
创建新类时,我习惯先问自己:“这是父类的一种,还是只是想借用父类的功能?”
如果只是想借用功能,我几乎总会选择组合。
观察设计模式也能看出明确的方向。策略模式、装饰器模式等,全部都以组合为基础。
尤其是策略模式,它会把行为拆分成对象,并在运行时替换。这种灵活性很难用继承模拟。
当然,你需要接受代码会稍微变长,因为必须手动编写委托方法。不过,之后修改结构会方便得多。
“不要使用继承”并不是禁止继承,而是提醒你不要为了复用而滥用继承。is-a 关系明确,就使用继承;只是想借用功能,就使用组合。只要掌握这一个标准,代码就会稳固许多。祝你今天也能做好设计!

