软件设计

组合(Composition)vs 继承(Inheritance):“不要使用继承”这句话的真正含义

学习面向对象时,一定会遇到这样一句话:“优先使用组合,而不是继承(Favor composition over inheritance)。”

4 分钟阅读
组合(Composition)vs 继承(Inheritance):“不要使用继承”这句话的真正含义 封面图

组合 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 关系,只要连行为都无法完全替代,继承就会成为陷阱。


那么,什么时候可以使用继承?

继承并非绝对不好。如果同时满足以下所有条件,使用继承反而更简洁。

  1. 是真正的 is-a 关系吗:子类始终是父类的一种吗?
  2. 遵守里氏替换原则吗:把子类放到父类的位置,是否完全没有问题?
  3. 父类是否以继承为前提进行设计:是否有文档说明,并且为扩展保持开放?

如果三项都符合,使用继承也没问题。扩展 UIViewController 这类基于框架的类,就是典型案例。

反过来,只要有一项不明确,就先考虑组合。

我用表格做了一个简单比较。

场景 建议
纯粹的 is-a 关系,且可以完全替换 继承
只想复用代码时 组合
想在运行时改变行为时 组合
需要组合多项功能时 组合

可以看到,实际工作中遇到的大多数场景都倾向于组合。“favor composition”这个建议并非无缘无故。

放在表格中比较后,什么时候该用什么一眼就能看明白
放在表格中比较后,什么时候该用什么一眼就能看明白

我在实际工作中会这样判断

创建新类时,我习惯先问自己:“这是父类的一种,还是只是想借用父类的功能?”

如果只是想借用功能,我几乎总会选择组合。

观察设计模式也能看出明确的方向。策略模式、装饰器模式等,全部都以组合为基础。

尤其是策略模式,它会把行为拆分成对象,并在运行时替换。这种灵活性很难用继承模拟。

像替换积木一样切换,这就是组合的魅力
像替换积木一样切换,这就是组合的魅力

当然,你需要接受代码会稍微变长,因为必须手动编写委托方法。不过,之后修改结构会方便得多。


“不要使用继承”并不是禁止继承,而是提醒你不要为了复用而滥用继承。is-a 关系明确,就使用继承;只是想借用功能,就使用组合。只要掌握这一个标准,代码就会稳固许多。祝你今天也能做好设计!

推荐阅读