组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
首先,先叠个甲,我并不认同组合一定优于继承这种太过于绝对化的观点,其实只要你对继承和组合这两者都有过思考的话,两者不存在孰优孰劣,只是组合更适合当下的开发模式。
不过,还是会有人好奇,近十年的新兴编程语言,都在不约而同的舍弃或弱化继承,比如:
- Rust、Go 没有继承
- Kotlin、Swift 弱化继承
这难道不是在证明组合优于继承吗?这个话题很有意思,在这篇文章中,我将会聊一聊这个话题。
继承解决的问题:代码复用
在面向对象编程发展的早期,程序员面临一个非常现实的问题:大量重复的代码。
class Dog {
void breath() { System.out.println("呼吸..."); }
void eat() { System.out.println("进食..."); }
}
class Cat {
void breath() { System.out.println("呼吸..."); }
void eat() { System.out.println("进食..."); }
}
而这就是继承最初要解决的问题:消除重复代码,实现代码复用。
这里举一个经典的例子 Animal 与 Dog,Dog 是 Animal 的一种,是 is-a 的关系。
所以当 Dog 类继承 Animal 类,就能复用 Animal 的呼吸、进食等方法,再添加自己的吠叫方法。
class Animal {
void breath() { System.out.println("呼吸..."); }
void eat() { System.out.println("进食..."); }
}
class Dog extends Animal {
void bark() { System.out.println("吠叫..."); }
}
这样一来,公共逻辑复用、代码量减少和结构层次清晰,而且继承天然符合人类的认知。在那个年代,继承几乎被认为是软件开发的最佳实践。
继承最大的敌人:变化
然而进入二十一世纪,随着互联网的兴起,软件的交付模式从“一次性发布、长期维护”转向了“敏捷迭代、快速试错”。这也就导致项目的迭代周期越来越短、需求的变更越来越频繁。
在这一背景下,继承这个曾经被奉为圭臬的代码复用手段,其底层设计的固有缺陷被持续放大,逐渐成为软件演化中的负担。
根源就是继承使得父类与子类强耦合在一起。这在项目的早期还好,公共逻辑放在父类,子类自动拥有。但是随着项目不断迭代,问题就出现了。
最典型的就是基类脆弱问题(Fragile Base Class Problem),对父类(基类)进行的任何微小修改,都可能会导致子类出现故障或意外行为。
同时,子类不需要的方法也会被迫继承下来,比如:
// 父类
class Animal {
void fly() {}
void swim() {}
void run() {}
}
// 子类
class Fish extends Animal {}
Fish 被迫拥有对于它来说无意义的 fly 和 run 方法,只能通过抛出异常的方式进行处理。
除此之外,继承还有一个更隐蔽的问题:随着继承树的自然变深,会让系统变得越来越难以扩展。例如:
# 最初的版本
Animal
├── Dog
└── Cat
# 经过多轮迭代后的版本
Animal
└── Mammal
└── Pet
├── Cat
└── Dog
└── PoliceDog
└── MilitaryPoliceDog
可以看到经过多轮迭代后,继承树变得越来越复杂。此时如果进行需求变更:警犬也可以作为宠物。
怎么办?修改继承关系?增加新的父类?还是复制代码?无论选择哪一种,这都将引入新的复杂度。
组合的出场
组合是将多个独立的组件(对象)组合成一个新对象,新对象通过持有其他组件的实例,复用组件的功能,而非直接继承其代码,如同搭建乐高积木一般。
例如汽车与引擎的关系,汽车拥有引擎,同时还可以拥有车轮、仪表盘等组件,通过调用这些组件的方法,实现行驶、显示车速等功能。
像现在前端领域的组件化开发,就是非常典型的组合案例:
import Input from "./Input";
import SearchIcon from "./SearchIcon";
import InputWithIcon from "./InputWithIcon";
export default function CustomizedInput() {
return (
<InputWithIcon icon={<SearchIcon />}>
<Input placeholder="Search..." />
</InputWithIcon>
);
}
继承和组合最核心的区别在于对关系的理解不同。继承为了实现代码复用构建出父类与子类,也就是 is-a 的关系。而组合的代码复用,复用的是能力,是 has-a 的关系。
结语
随着时代的发展,现如今程序员面临的主要问题不再是如何解决代码复用,而是如何让代码适应变化,而组合比继承更适应变化。
当然,这并不意味着继承就彻底退出历史舞台了,最典型的就是各类传统行业的软件系统,比如银行核心业务系统、大型企业 ERP 等。这类项目的领域模型往往经过了数十年的行业沉淀,业务规则高度固化,核心需求变更频率极低。
综上,组合与继承不存在孰优孰劣,只有谁更适合。

228

被折叠的 条评论
为什么被折叠?



