关于 composition-over-inheritance
面向开发者的软件设计技能,帮助选择类行为的组织方式。它建议通过接口组合能力,而非依赖继承,同时指出继承仍适用的有限情形。
主要功能
- 01优先使用组合而非继承
- 02通过接口组合不同能力
- 03识别继承设计的警示信号
- 04在有限类型层级中使用继承
适用场景
- 01设计具有多种行为的类
- 02重新审视生硬或过深的类层级
- 03解耦行为以便于测试
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ yanko-belov/ code-craft --skill composition-over-inheritance安装后在 Claude Code 中输入/composition-over-inheritance确认已加载。
详细介绍
组合优于继承
概述
优先使用对象组合,而非类继承。
继承会造成紧密耦合和僵化的层级结构。组合则能创建灵活、可复用的组件,并可自由搭配。
适用场景
- 设计类之间的关系
- 想要使用
extends时 - 类需要来自多个来源的行为时
- 创建“是一个”关系时
- 构建类层级时
铁律
NEVER use inheritance when composition would work.
没有例外:
- 不能因为“这是面向对象编程的做法”
- 不能因为“这是‘是一个’关系”
- 不能为了“通过 extends 复用代码”
- 不能为了“多态”
默认使用组合。只有真正的类型层级才使用继承。
检测:继承异味
如果继承让人觉得别扭或牵强,就使用组合:
// ❌ VIOLATION: Inheritance hierarchy
class Animal {
eat(): void { console.log('Eating'); }
}
class FlyingAnimal extends Animal {
fly(): void { console.log('Flying'); }
}
class SwimmingAnimal extends Animal {
swim(): void { console.log('Swimming'); }
}
// Duck needs both fly AND swim - inheritance can't do this cleanly
class Duck extends FlyingAnimal {
swim(): void { console.log('Swimming'); } // Duplicated!
}
正确模式:使用接口进行组合
将能力定义为接口,并进行组合:
// ✅ CORRECT: Composition
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
interface Eatable {
eat(): void;
}
// Reusable behaviors
const flyingBehavior: Flyable = {
fly() { console.log('Flying'); }
};
const swimmingBehavior: Swimmable = {
swim() { console.log('Swimming'); }
};
const eatingBehavior: Eatable = {
eat() { console.log('Eating'); }
};
// Compose what you need
class Duck implements Flyable, Swimmable, Eatable {
fly = flyingBehavior.fly;
swim = swimmingBehavior.swim;
eat = eatingBehavior.eat;
}
class Fish implements Swimmable, Eatable {
swim = swimmingBehavior.swim;
eat = eatingBehavior.eat;
}
class Bird implements Flyable, Eatable {
fly = flyingBehavior.fly;
eat = eatingBehavior.eat;
}
继承为何会失败
| 问题 | 示例 |
|---|---|
| 菱形问题 | 鸭子需要飞行和游泳能力 |
| 紧密耦合 | 子类了解父类的内部细节 |
| 僵化的层级结构 | 无法更改父类而不破坏子类 |
| 强制继承 | 获得自己不需要的方法 |
| 脆弱的基类 | 父类的更改会破坏所有子类 |
组合为何更胜一筹
| 优点 | 示例 |
|---|---|
| 灵活 | 可组合任意行为 |
| 松散耦合 | 组件彼此互不知晓 |
| 易于测试 | 可单独模拟各项行为 |
| 运行时更改 | 可动态替换行为 |
| 不受层级结构限制 | 可自由添加新的组合 |
抵御压力的流程
1. “这是面向对象编程的做法”
压力:“面向对象编程使用继承。”
**回应:**现代面向对象编程更倾向于组合。继承被过度使用了。
**行动:**使用接口和组合。这仍然是面向对象编程。
2. “这是‘是一个’关系”
压力:“鸭子是鸟,所以它应该继承鸟。”
**回应:**需求变化后,“是一个”往往会变成“有一个”。组合可以应对这两种情况。
**行动:**将其建模为“具有某些行为”,而不是“属于某种类型”。
3. “通过 extends 复用代码”
压力:“我需要父类的方法。”
**回应:**组合可以更好地复用代码,同时避免耦合。
**行动:**将共享行为提取为可组合的单元。
4. “多态需要继承”
压力:“我需要统一处理不同类型。”
**回应:**接口无需继承即可提供多态。
**行动:**定义接口,并让类实现它。
危险信号——停止并重新考虑
如果注意到以下任何一点,请改用组合:
- 代码中出现
extends关键字 - 类层级超过 2 层
- 子类重写父类方法
- “菱形问题”——需要多个父类
- 子类没有使用父类的所有方法
- 更改父类会破坏子类
- 不实例化父类就难以测试
以上情况都意味着:重构为组合。
适合使用继承的场景
仅在以下情况下使用继承:
- 真正的类型层级(很少见)
- 框架要求这样做(如 React 类组件等)
- 扩展由你无法控制的库类
即便如此,也要保持层级结构扁平(最多 2 层)。
快速参考
| 继承 | 组合 |
|---|---|
class Dog extends Animal | class Dog implements Animal + 注入行为 |
| 僵化的层级结构 | 灵活的组合 |
| 只能有一个父类 | 多种行为 |
| 紧密耦合 | 松散耦合 |
| 更改会层层蔓延 | 更改彼此隔离 |
常见借口(都不成立)
| 借口 | 事实 |
|---|---|
| “这是面向对象编程的做法” | 现代面向对象编程更倾向于组合。 |
| “这是‘是一个’关系” | “具有某种行为”更灵活。 |
| “需要父类的方法” | 改为组合相应行为。 |
| “多态需要它” | 接口可以提供多态。 |
| “用 extends 代码更少” | 组合更灵活。 |
| “我已经注明它有问题” | 如果它有问题,就不要这样做。 |
核心要点
组合行为,而非继承行为。
设计类时:为各种能力定义接口,创建可组合的行为,并注入每个类所需的内容。仅在万不得已时使用 extends。
评论
支持 Markdown