组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?

组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?

首先,先叠个甲,我并不认同组合一定优于继承这种太过于绝对化的观点,其实只要你对继承和组合这两者都有过思考的话,两者不存在孰优孰劣,只是组合更适合当下的开发模式。

不过,还是会有人好奇,近十年的新兴编程语言,都在不约而同的舍弃或弱化继承,比如:

  • Rust、Go 没有继承
  • Kotlin、Swift 弱化继承

这难道不是在证明组合优于继承吗?这个话题很有意思,在这篇文章中,我将会聊一聊这个话题。

继承解决的问题:代码复用

在面向对象编程发展的早期,程序员面临一个非常现实的问题:大量重复的代码

java 复制代码
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 的呼吸、进食等方法,再添加自己的吠叫方法。

java 复制代码
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),对父类(基类)进行的任何微小修改,都可能会导致子类出现故障或意外行为

同时,子类不需要的方法也会被迫继承下来,比如:

java 复制代码
// 父类
class Animal {
    void fly() {}
    void swim() {}
    void run() {}
}

// 子类
class Fish extends Animal {}

Fish 被迫拥有对于它来说无意义的 fly 和 run 方法,只能通过抛出异常的方式进行处理。

除此之外,继承还有一个更隐蔽的问题:随着继承树的自然变深,会让系统变得越来越难以扩展。例如:

plaintext 复制代码
# 最初的版本
Animal
 ├── Dog
 └── Cat

# 经过多轮迭代后的版本
Animal 
  └── Mammal 
    └── Pet
      ├── Cat
      └── Dog 
        └── PoliceDog 
          └── MilitaryPoliceDog

可以看到经过多轮迭代后,继承树变得越来越复杂。此时如果进行需求变更:警犬也可以作为宠物。

怎么办?修改继承关系?增加新的父类?还是复制代码?无论选择哪一种,这都将引入新的复杂度。

组合的出场

组合是将多个独立的组件(对象)组合成一个新对象,新对象通过持有其他组件的实例,复用组件的功能,而非直接继承其代码,如同搭建乐高积木一般。

例如汽车与引擎的关系,汽车拥有引擎,同时还可以拥有车轮、仪表盘等组件,通过调用这些组件的方法,实现行驶、显示车速等功能。

像现在前端领域的组件化开发,就是非常典型的组合案例:

typescript 复制代码
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 等。这类项目的领域模型往往经过了数十年的行业沉淀,业务规则高度固化,核心需求变更频率极低。

综上,组合与继承不存在孰优孰劣,只有谁更适合。

相关推荐
IT_陈寒2 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro2 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax3 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH3 小时前
Koa和Express的区别
后端
MariaH3 小时前
Koa框架的使用
后端
Jack204 小时前
HarmonyOS开发中RESTful API封装:网络层架构设计
编程语言
luckdewei4 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某5 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy5 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试