591游戏城 - 最新网游活动资讯聚合平台
首页活动中心正文

接手祖传代码后,我终于理解了"组合优于继承"

2026-07-13 11:02:19

前言

不管是看些博客,文章还是啥,经常会看到"组合优于继承"这句话,心里想的是:啥玩意儿?继承多好用啊,extends一下就完事了,还能复用代码,多香。也没去详细想过相关的问题。

直到有一天,我接手了一个"祖传"项目,看到了这样的代码:

javascript

复制代码

class Animal {}

class Mammal extends Animal {}

class Dog extends Mammal {}

class GuideDog extends Dog {}

class ServiceDog extends GuideDog {}

class TherapyDog extends ServiceDog {}

// 我:???这是什么套娃现场

然后产品经理说:我们要加个会飞的狗。

我:......

这时候才明白,原来继承不是万能的。今天就来聊聊为什么"组合优于继承",以及什么时候该用继承,什么时候该用组合。

继承的问题到底在哪

问题1:类层次太脆弱

继承最大的问题是:你的子类和父类绑死了。

javascript

复制代码

class Bird {

constructor(name) {

this.name = name;

}

fly() {

return `${this.name} is flying`;

}

layEggs() {

return `${this.name} laid eggs`;

}

}

class Penguin extends Bird {

// 问题来了:企鹅不会飞

fly() {

throw new Error('Penguins cannot fly!');

}

}

const penguin = new Penguin('波波');

penguin.fly(); // 💥 抛错了

你看,企鹅确实是鸟,但它不会飞。如果用继承,要么:

在子类里覆盖方法抛错(但这违反了里氏替换原则)

把父类拆成更细的类(但这又导致类爆炸)

这就是所谓的"脆弱基类问题"(Fragile Base Class Problem)。父类改一点东西,所有子类都可能挂。

问题2:多继承的困境

JavaScript只支持单继承,但现实世界很多东西需要多种能力:

javascript

复制代码

// 我想要一个会飞又会游泳的鸭子

class Duck extends Bird, Fish { // ❌ 语法错误,JS不支持多继承

// ...

}

有人说,那我用Mixin啊:

javascript

复制代码

class Duck extends Bird {}

Object.assign(Duck.prototype, Swimmable, Diveable);

但这又引入了新问题:

Mixin的方法可能冲突

没有类型检查

原型链变得混乱

问题3:逻辑散落在继承链中

javascript

复制代码

class Animal {

constructor(name) {

this.name = name;

}

eat() {

this.energy += 10; // 等等,energy在哪定义的?

}

}

class Mammal extends Animal {

constructor(name) {

super(name);

this.energy = 100; // 原来在这

}

}

class Dog extends Mammal {

bark() {

if (this.energy < 20) { // 要理解这个逻辑,得翻三层继承链

return 'Too tired to bark';

}

this.energy -= 5;

return 'Woof!';

}

}

要理解Dog.bark()的完整逻辑,你得:

看Dog类的bark方法

往上翻Mammal的构造函数(找energy定义)

再往上翻Animal的eat方法(看energy怎么变化)

这就是"知识散落"问题。继承链一长,代码就像侦探小说,到处找线索。

组合是什么

组合的核心思想很简单:把功能拆成独立的小块,需要什么就组装什么。

就像搭乐高:

继承:买了个"城堡套装",想改成"太空站",得把塔楼锯掉,很麻烦

组合:买了一堆积木,想搭啥搭啥

最简单的组合例子

javascript

复制代码

// 把能力定义成独立的对象

const canFly = {

fly() {

return `${this.name} is flying at ${this.flySpeed} km/h`;

}

};

const canSwim = {

swim() {

return `${this.name} is swimming at ${this.swimSpeed} km/h`;

}

};

const canWalk = {

walk() {

return `${this.name} is walking at ${this.walkSpeed} km/h`;

}

};

// 鸭子:会飞、会游、会走

function createDuck(name) {

const duck = {

name,

flySpeed: 50,

swimSpeed: 5,

walkSpeed: 3

};

return Object.assign(duck, canFly, canSwim, canWalk);

}

// 企鹅:只会游、会走

function createPenguin(name) {

const penguin = {

name,

swimSpeed: 30,

walkSpeed: 2

};

return Object.assign(penguin, canSwim, canWalk);

}

const duck = createDuck('唐老鸭');

console.log(duck.fly()); // ✅ 唐老鸭 is flying at 50 km/h

console.log(duck.swim()); // ✅ 唐老鸭 is swimming at 5 km/h

const penguin = createPenguin('波波');

console.log(penguin.swim()); // ✅ 波波 is swimming at 30 km/h

console.log(penguin.fly); // ✅ undefined - 企鹅本来就不会飞

看到区别了吗?

用继承:企鹅继承了Bird,但得覆盖fly方法抛错

用组合:企鹅压根不组装fly能力,自然就不会飞

组合的几种实现方式

方式1:工厂函数 + Object.assign

这是最简单的方式,上面已经展示过了。优点是简单直接,缺点是没有类型提示。

方式2:类组合(Class Composition)

如果你喜欢class语法,可以这样:

javascript

复制代码

class Animal {

constructor(name) {

this.name = name;

}

}

// 把能力定义成装饰函数

function withFlying(Base) {

return class extends Base {

fly() {

return `${this.name} is flying`;

}

};

}

function withSwimming(Base) {

return class extends Base {

swim() {

return `${this.name} is swimming`;

}

};

}

// 组装能力

class Duck extends withSwimming(withFlying(Animal)) {

constructor(name) {

super(name);

}

}

const duck = new Duck('唐老鸭');

duck.fly(); // ✅

duck.swim(); // ✅

duck instanceof Animal; // ✅ true

这种方式的好处是:

保留了class的语法

instanceof检查仍然有效

IDE能提供代码补全

方式3:依赖注入

更高级的做法是把能力注入进去:

javascript

复制代码

class Bird {

constructor(name, abilities = []) {

this.name = name;

// 注入能力

abilities.forEach(ability => {

ability.attachTo(this);

});

}

}

// 定义能力模块

const flyingAbility = {

attachTo(target) {

target.flySpeed = 50;

target.fly = function() {

return `${this.name} is flying at ${this.flySpeed} km/h`;

};

}

};

const swimmingAbility = {

attachTo(target) {

target.swimSpeed = 10;

target.swim = function() {

return `${this.name} is swimming at ${this.swimSpeed} km/h`;

};

}

};

// 使用

const duck = new Bird('唐老鸭', [flyingAbility, swimmingAbility]);

const penguin = new Bird('波波', [swimmingAbility]);

duck.fly(); // ✅

penguin.fly; // ✅ undefined

这种方式更灵活,能力模块可以有自己的初始化逻辑。

实战案例:重构继承代码

来看个真实场景。假设我们要做一个游戏角色系统。

用继承实现(一团糟)

javascript

复制代码

class Character {

constructor(name, hp, mp) {

this.name = name;

this.hp = hp;

this.mp = mp;

}

attack() {

return `${this.name} attacks!`;

}

}

class Mage extends Character {

castSpell(spellName) {

if (this.mp < 10) return 'Not enough MP!';

this.mp -= 10;

return `${this.name} casts ${spellName}!`;

}

}

class Warrior extends Character {

useSkill(skillName) {

if (this.hp < 20) return 'Too injured!';

this.hp -= 20;

return `${this.name} uses ${skillName}!`;

}

}

// 现在产品说:我们要加个"魔剑士",既能施法又能用战技

class SpellBlade extends Mage { // 继承法师

useSkill(skillName) { // 复制粘贴战士的代码

if (this.hp < 20) return 'Too injured!';

this.hp -= 20;

return `${this.name} uses ${skillName}!`;

}

}

// 问题:

// 1. SpellBlade复制了Warrior的代码(违反DRY原则:DRY - Don't Repeat Yourself(不要重复自己))

// 2. 如果Warrior的useSkill改了,SpellBlade也得改

// 3. 继承Mage还是Warrior?这是个哲学问题

用组合重构(清爽)

javascript

复制代码

// 把能力拆成独立模块

const combatAbility = {

attack() {

return `${this.name} attacks for ${this.attackPower} damage!`;

}

};

const magicAbility = {

castSpell(spellName) {

if (this.mp < 10) return 'Not enough MP!';

this.mp -= 10;

return `${this.name} casts ${spellName}!`;

}

};

const skillAbility = {

useSkill(skillName) {

if (this.hp < 20) return 'Too injured!';

this.hp -= 20;

return `${this.name} uses ${skillName}!`;

}

};

// 工厂函数

function createCharacter(name, hp, mp, abilities) {

const character = {

name,

hp,

mp,

attackPower: 50

};

return Object.assign(character, ...abilities);

}

// 创建不同职业

const mage = createCharacter('甘道夫', 100, 200, [combatAbility, magicAbility]);

const warrior = createCharacter('阿拉贡', 200, 50, [combatAbility, skillAbility]);

const spellBlade = createCharacter('塞尔达', 150, 150, [combatAbility, magicAbility, skillAbility]);

mage.castSpell('火球术'); // ✅

warrior.useSkill('狂战士之怒'); // ✅

spellBlade.castSpell('雷电术'); // ✅

spellBlade.useSkill('剑气斩'); // ✅

用组合的好处:

代码不重复,每个能力只定义一次

能力可以随意组合,想要什么组装什么

新增职业超简单,改一下能力列表就行

什么时候该用继承

说了这么多组合的好处,难道继承就完全没用了吗?

也不是。继承适合这些场景:

场景1:严格的"is-a"关系

javascript

复制代码

class Error {

constructor(message) {

this.message = message;

}

}

class TypeError extends Error {

constructor(message) {

super(message);

this.name = 'TypeError';

}

}

class ReferenceError extends Error {

constructor(message) {

super(message);

this.name = 'ReferenceError';

}

}

TypeError 确实是 Error的一种,这种关系很稳定,不会变。用继承没毛病。

场景2:需要利用多态

javascript

复制代码

class Shape {

getArea() {

throw new Error('子类必须实现getArea方法');

}

}

class Circle extends Shape {

constructor(radius) {

super();

this.radius = radius;

}

getArea() {

return Math.PI * this.radius ** 2;

}

}

class Rectangle extends Shape {

constructor(width, height) {

super();

this.width = width;

this.height = height;

}

getArea() {

return this.width * this.height;

}

}

// 利用多态

function calculateTotalArea(shapes) {

return shapes.reduce((sum, shape) => sum + shape.getArea(), 0);

}

const shapes = [

new Circle(5),

new Rectangle(4, 6),

new Circle(3)

];

calculateTotalArea(shapes); // 统一调用getArea,不用管具体类型

这种情况继承的优势很明显:所有形状都有getArea方法,可以统一处理。

场景3:框架要求

javascript

复制代码

// React组件必须继承React.Component

class MyComponent extends React.Component {

render() {

return

Hello
;

}

}

// 虽然现在有了函数组件,但类组件时代就是这样

框架规定了继承,那就没办法,得用。

组合和继承的混合使用

实际项目中,往往是组合和继承混着用。

javascript

复制代码

// 用继承表达"is-a"关系

class Animal {

constructor(name, energy) {

this.name = name;

this.energy = energy;

}

eat() {

this.energy += 10;

}

}

// 用组合添加能力

const canFly = {

fly() {

if (this.energy < 10) return 'Too tired to fly';

this.energy -= 10;

return `${this.name} is flying`;

}

};

const canSwim = {

swim() {

if (this.energy < 5) return 'Too tired to swim';

this.energy -= 5;

return `${this.name} is swimming`;

}

};

// 鸭子是动物(继承),会飞会游(组合)

class Duck extends Animal {

constructor(name) {

super(name, 100);

Object.assign(this, canFly, canSwim);

}

}

const duck = new Duck('唐老鸭');

duck.eat(); // 继承自Animal

duck.fly(); // 组合的能力

duck.swim(); // 组合的能力

这样既保留了继承的多态性,又有了组合的灵活性。

TypeScript中的组合

在TS里,组合的类型安全性更好:

javascript

复制代码

// 定义能力接口

interface Flyable {

flySpeed: number;

fly(): string;

}

interface Swimmable {

swimSpeed: number;

swim(): string;

}

// 组合类型

type Duck = {

name: string;

} & Flyable & Swimmable;

function createDuck(name: string): Duck {

return {

name,

flySpeed: 50,

swimSpeed: 10,

fly() {

return `${this.name} is flying`;

},

swim() {

return `${this.name} is swimming`;

}

};

}

const duck = createDuck('唐老鸭');

duck.fly(); // ✅ 类型安全

duck.walk(); // ❌ 编译错误:Duck没有walk方法

TS的类型系统天然支持组合,用起来比继承舒服多了。

总结

"组合优于继承"不是说继承一无是处,而是说:

优先考虑组合:大部分场景下,组合更灵活、更好维护

继承也有用:严格的is-a关系、需要多态、框架要求等场景下继承更合适

两者结合:实际项目中往往是组合 + 继承混用

记住一个原则:如果你在纠结该不该用继承,那大概率应该用组合。

最后用一句话概括:

继承是"你是什么"(you are what you are)

组合是"你能做什么"(you are what you do)

世界是由能力组成的,不是由血统组成的。代码也一样。

参考文档:

Composition over inheritance - Wikipedia

《设计模式》- Gang of Four

Functional Mixins in JavaScript

老客户维护答疑支招篇:淘宝店铺如何将老客户维护好? 急救与自救
相关内容