第12章 处理继承关系(Dealing with Inheritance)
一、导读
1.1 本章学习目标
通过本章的学习,你将能够:
理解继承的力量和陷阱,认识到继承是双刃剑
掌握提取超类(Extract Superclass)的技术,从相似类中提取共同行为
学会折叠继承体系(Collapse Hierarchy),移除不必要的继承层级
掌握以委托取代子类(Replace Subclass with Delegate),用组合替代继承
学会以委托取代超类(Replace Superclass with Delegate),打破不合适的继承关系
理解消除向下转型(Remove Downcasting)的重要性,提高类型安全
能够在实际项目中识别继承滥用的情况,并应用合适的重构手法
掌握现代JavaScript/TypeScript中替代继承的模式和技术
1.2 为什么本章如此重要
继承是面向对象编程的三大支柱之一,但它也是最容易被误用的特性。许多开发者在学习OOP时首先接触继承,导致倾向于过度使用继承来解决问题。
本章的重要性体现在以下几个方面:
首先,继承创建了类之间的强耦合。子类的实现依赖于超类的实现细节,超类的变化可能破坏所有子类。这种耦合使得代码难以维护和演进。
其次,深层的继承体系难以理解。要理解一个子类的行为,可能需要追溯多层超类。这增加了认知负担,降低了代码的可读性。
第三,"组合优于继承"是现代软件设计的重要原则。组合提供了更大的灵活性,可以在运行时改变行为,而继承的行为在编译时就固定了。
第四,随着函数式编程和原型编程的影响,越来越多的开发者认识到继承不是代码复用的唯一方式。混入(mixin)、高阶函数、策略模式等都可以替代继承。
第五,在TypeScript中,接口和类型系统提供了比继承更灵活的抽象机制。理解何时使用接口、何时使用继承是重要的设计决策。
第六,许多经典的设计模式(如Strategy、Decorator、Observer)本质上是为了避免继承的局限性而诞生的。学习这些模式可以帮助写出更灵活的代码。
最后,遗留系统中往往存在滥用继承的情况。掌握处理继承的重构技术对于维护和改进遗留代码至关重要。
对于任何希望编写高质量面向对象代码的开发者来说,本章都是必读的。它不仅告诉你如何处理继承,更告诉你何时不应该使用继承。
1.3 核心问题引导
在阅读本章之前,请思考以下问题:
你见过最深的继承体系有多少层?理解这样的体系是什么感受?
继承和组合各有什么优缺点?在什么情况下应该优先使用组合?
你是否曾经因为超类的变化而不得不修改多个子类?
子类重写超类方法时,什么情况下会违反里氏替换原则?
JavaScript的原型继承与Java/C++的类继承有什么区别?
TypeScript的接口可以实现多重继承吗?它与类继承有什么不同?
React Hooks的出现是否改变了我们对组件复用的思考方式?
在现代前端开发中,继承还有多少用武之地?
带着这些问题阅读本章,你会对继承有更深刻的理解。
二、核心概念详解
2.1 继承的力量与陷阱
继承的力量:
代码复用: 子类可以继承超类的属性和方法,避免重复代码。
多态: 子类可以重写超类方法,实现不同的行为。
类型层次: 继承建立了类型的层次关系,支持向上转型。
继承的陷阱:
紧耦合: 子类依赖超类的实现细节。
脆弱基类问题: 超类的变化可能破坏子类。
深层嵌套: 多层继承使代码难以理解。
菱形问题: 多重继承可能导致歧义(JavaScript通过原型链避免了这个问题)。
继承的适用场景:
- "是一个"(is-a)关系:子类确实是超类的一种
- 需要多态行为
- 共享稳定的公共行为
组合的适用场景:
- "有一个"(has-a)关系:对象包含其他对象
- 需要在运行时改变行为
- 避免紧耦合
2.2 提取超类(Extract Superclass)
提取超类是从两个或多个相似类中提取共同行为到一个新的超类中。
动机:
- 多个类有相似的字段和方法
- 希望复用共同的行为
- 建立类型层次以支持多态
识别机会:
// Before - 两个相似的类
class Department {
constructor(name, staff) {
this._name = name;
this._staff = staff;
}
get totalSalary() {
return this._staff.reduce((total, s) => total + s.salary, 0);
}
get headCount() {
return this._staff.length;
}
}
class ProductLine {
constructor(name, staff) {
this._name = name;
this._staff = staff;
}
get totalSalary() {
return this._staff.reduce((total, s) => total + s.salary, 0);
}
get headCount() {
return this._staff.length;
}
}
// After - 提取超类
class Organization {
constructor(name, staff) {
this._name = name;
this._staff = staff;
}
get name() {
return this._name;
}
get totalSalary() {
return this._staff.reduce((total, s) => total + s.salary, 0);
}
get headCount() {
return this._staff.length;
}
}
class Department extends Organization {
constructor(name, staff) {
super(name, staff);
}
}
class ProductLine extends Organization {
constructor(name, staff) {
super(name, staff);
}
}机制:
识别共同特征: 找出两个类中相同的字段和方法。
创建超类: 创建一个新的超类。
迁移共同行为: 将共同的字段和方法移到超类中。
让原类继承超类: 修改原类使其继承超类。
删除重复代码: 从子类中删除已移到超类的代码。
测试: 确保行为不变。
注意事项:
- 确保是真正的"is-a"关系
- 不要为了复用而强行建立继承关系
- 考虑使用混入或组合作为替代
2.3 折叠继承体系(Collapse Hierarchy)
折叠继承体系是移除不必要的继承层级,将超类和子类合并。
动机:
- 超类和子类差异很小,分离没有价值
- 继承层级过深,简化结构
- 超类变得过于抽象,难以理解
识别机会:
// Before - 不必要的继承
class Employee {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}
class SalesEmployee extends Employee {
// 没有添加任何新行为
}
// After - 折叠
class Employee {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}机制:
选择要合并的类: 确定哪两个类应该合并。
合并字段和方法: 将一个类的成员移到另一个类中。
更新引用: 将所有引用改为使用合并后的类。
删除多余的类: 删除不再需要的类。
测试: 确保行为不变。
2.4 以委托取代子类(Replace Subclass with Delegate)
以委托取代子类是用委托对象替代子类,用组合替代继承。
动机:
- 子类只在一两个方面与超类不同
- 多种维度的变化导致子类爆炸
- 希望在运行时改变行为
识别机会:
// Before - 子类
class Bird {
constructor(name) {
this._name = name;
}
fly() {
return `${this._name} is flying`;
}
}
class Penguin extends Bird {
fly() {
throw new Error('Penguins cannot fly');
}
}
class Ostrich extends Bird {
fly() {
throw new Error('Ostriches cannot fly');
}
}
// After - 委托
class Bird {
constructor(name, flightBehavior) {
this._name = name;
this._flightBehavior = flightBehavior;
}
fly() {
return this._flightBehavior.fly(this._name);
}
}
class CanFly {
fly(name) {
return `${name} is flying`;
}
}
class CannotFly {
fly(name) {
throw new Error(`${name} cannot fly`);
}
}
// 使用
const eagle = new Bird('Eagle', new CanFly());
const penguin = new Bird('Penguin', new CannotFly());机制:
识别子类的差异: 确定子类与超类的不同之处。
创建委托类: 为每种变体创建委托类。
在超类中添加委托: 在超类中添加委托字段。
迁移行为: 将子类的特有行为移到委托类中。
创建工厂: 用工厂函数创建带适当委托的对象。
删除子类: 确认所有使用都更新后,删除子类。
测试: 确保行为不变。
优点:
- 可以在运行时改变行为
- 避免了子类爆炸
- 符合开闭原则
2.5 以委托取代超类(Replace Superclass with Delegate)
以委托取代超类是将继承关系改为委托关系,打破不合适的继承。
动机:
- 子类继承了超类的不需要或不合适的方法
- 违反了里氏替换原则
- 超类的变化影响了不相关的子类
识别机会:
// Before - 不合适的继承
class Stack extends Array {
push(item) {
super.push(item);
}
pop() {
return super.pop();
}
// 但Stack也继承了Array的所有其他方法,如map、filter等
// 这些方法对Stack来说可能不合适
}
// After - 委托
class Stack {
constructor() {
this._items = [];
}
push(item) {
this._items.push(item);
}
pop() {
return this._items.pop();
}
get length() {
return this._items.length;
}
// 只暴露需要的方法
}机制:
在子类中创建委托字段: 创建一个指向超类实例的字段。
重写超类方法: 将超类方法的调用转发给委托对象。
逐步迁移: 每次迁移一个方法,测试后再继续。
移除继承: 当所有方法都迁移后,移除extends。
测试: 确保行为不变。
2.6 消除向下转型(Remove Downcasting)
向下转型是将超类引用强制转换为子类类型。消除向下转型可以提高类型安全。
动机:
- 向下转型不安全,可能在运行时失败
- 表明设计有问题,应该用多态替代
- TypeScript中需要类型断言,失去类型安全
识别机会:
// Before - 向下转型
class Animal {
speak() { /* abstract */ }
}
class Dog extends Animal {
speak() { return 'Woof'; }
fetch() { return 'Fetching'; }
}
class Cat extends Animal {
speak() { return 'Meow'; }
}
function playWithAnimal(animal) {
if (animal instanceof Dog) {
const dog = animal; // 向下转型
return dog.fetch();
}
return animal.speak();
}
// After - 多态
class Animal {
play() {
return this.speak();
}
speak() { /* abstract */ }
}
class Dog extends Animal {
speak() { return 'Woof'; }
play() { return this.fetch(); }
fetch() { return 'Fetching'; }
}
class Cat extends Animal {
speak() { return 'Meow'; }
}
function playWithAnimal(animal) {
return animal.play(); // 无需向下转型
}机制:
识别向下转型: 找出instanceof检查或类型断言。
分析原因: 确定为什么需要向下转型。
引入多态: 在超类中添加方法,让子类提供不同实现。
替换条件逻辑: 用多态调用替代instanceof检查。
测试: 确保行为不变。
2.7 现代JavaScript/TypeScript中的继承替代
接口(Interface):
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
class Duck implements Flyable, Swimmable {
fly() { /* ... */ }
swim() { /* ... */ }
}混入(Mixin):
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 Bird extends withFlying(withSwimming(Animal)) {}组合:
class Bird {
constructor(name, abilities) {
this.name = name;
this.abilities = abilities;
}
perform(action) {
if (this.abilities[action]) {
return this.abilities[action]();
}
throw new Error(`Cannot ${action}`);
}
}
const duck = new Bird('Duck', {
fly: () => 'Flying',
swim: () => 'Swimming'
});高阶函数:
const withLogging = (fn) => (...args) => {
console.log(`Calling ${fn.name} with`, args);
return fn(...args);
};
const add = withLogging((a, b) => a + b);三、程序员的视角
3.1 在实际项目中的应用
场景一:React组件的复用
早期React使用ES6类继承,后来转向组合和Hooks:
// Before - 继承
class BaseComponent extends React.Component {
componentDidMount() {
this.trackAnalytics();
}
trackAnalytics() { /* ... */ }
}
class MyComponent extends BaseComponent { /* ... */ }
// After - Hooks
function useAnalytics() {
useEffect(() => {
trackAnalytics();
}, []);
}
function MyComponent() {
useAnalytics();
return <div>...</div>;
}场景二:游戏角色的能力系统
// 使用组合而非继承
class Character {
constructor(name) {
this.name = name;
this.abilities = [];
}
addAbility(ability) {
this.abilities.push(ability);
}
useAbilities() {
return this.abilities.map(a => a.use());
}
}
const hero = new Character('Hero');
hero.addAbility(new Flying());
hero.addAbility(new Shooting());3.2 常见陷阱与误区
陷阱一:过度使用继承
为了一点代码复用就建立继承关系。
教训: 优先考虑组合。
陷阱二:深层继承
继承层级超过3层通常表明设计问题。
教训: 保持扁平的继承结构。
陷阱三:违反里氏替换原则
子类不能正确替代超类。
教训: 确保子类保持超类的契约。
3.3 与其他层级的联系
与设计模式的联系:
- Replace Subclass → Strategy Pattern
- Replace Superclass → Adapter Pattern
- Mixin → Decorator Pattern
与架构的联系:
- 微服务中的代码共享策略
- 单体仓库中的包组织
- 插件系统的扩展点设计
四、代码实战
4.1 示例一:完整的继承重构
// Before - 复杂的继承体系
class Vehicle {
constructor(speed) {
this._speed = speed;
}
move() {
return `Moving at ${this._speed}`;
}
}
class Car extends Vehicle {
drive() {
return `${this.move()} on road`;
}
}
class Boat extends Vehicle {
sail() {
return `${this.move()} on water`;
}
}
class AmphibiousCar extends Car {
// 既要drive又要sail,但无法同时继承Car和Boat
sail() {
return `${this.move()} on water`;
}
}
// After - 使用组合
class Vehicle {
constructor(movement) {
this._movement = movement;
}
move() {
return this._movement.move();
}
}
class RoadMovement {
move() {
return 'Moving on road';
}
}
class WaterMovement {
move() {
return 'Moving on water';
}
}
class AmphibiousMovement {
move(surface) {
return surface === 'road' ? 'Moving on road' : 'Moving on water';
}
}
// 使用
const car = new Vehicle(new RoadMovement());
const boat = new Vehicle(new WaterMovement());
const amphibious = new Vehicle(new AmphibiousMovement());4.2 示例二:TypeScript接口替代继承
// Before - 继承
abstract class Shape {
abstract area(): number;
abstract perimeter(): number;
}
class Circle extends Shape {
constructor(private radius: number) { super(); }
area() { return Math.PI * this.radius ** 2; }
perimeter() { return 2 * Math.PI * this.radius; }
}
// After - 接口
interface Shape {
area(): number;
perimeter(): number;
}
class Circle implements Shape {
constructor(private radius: number) {}
area() { return Math.PI * this.radius ** 2; }
perimeter() { return 2 * Math.PI * this.radius; }
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area() { return this.width * this.height; }
perimeter() { return 2 * (this.width + this.height); }
}五、深入思考
5.1 历史背景
继承的概念源于Simula和Smalltalk。早期的OOP强调继承作为代码复用的主要机制。
GoF设计模式(1994年)提出了"组合优于继承"的原则,开始反思继承的局限性。
Java的单继承+接口设计反映了对多重继承问题的回应。
JavaScript的原型继承提供了另一种对象复用模型。
现代语言如Rust、Go完全摒弃了传统继承,采用trait/composition模型。
5.2 前沿方向
无继承语言:
- Go的组合和接口
- Rust的trait系统
- Elm的不可变数据
React的范式转变:
- 从HOC到Hooks
- 组合式API
- 自定义Hook作为复用机制
Web Components:
- Mixin模式
- Composition API
5.3 延伸阅读
- 《Design Patterns》by GoF - 经典设计模式
- 《Head First Design Patterns》- 易于理解的模式介绍
- React官方文档 - Hooks和组合的最佳实践
六、实验笔记
实验 1:重构一个继承体系
目标: 选择一个项目中的继承体系,对其进行重构。
步骤:
分析当前继承结构
识别问题
设计改进方案
实施重构
验证功能正常
记录:
- 当前继承结构:
- 发现的问题:
- 重构方案:
- 改善效果:
实验 2:用组合替代继承
目标: 将一个使用继承的场景改为组合。
步骤:
识别继承关系
设计组合方案
实现组合
删除继承
测试
记录:
- 原继承关系:
- 组合设计:
- 代码变化:
- 灵活性改善:
七、本章小结
本章介绍了处理继承关系的各种重构技术。
提取超类从相似类中提取共同行为,促进代码复用。折叠继承体系移除不必要的层级,简化结构。
以委托取代子类和以委托取代超类用组合替代继承,提供了更大的灵活性和更低的耦合。这是现代软件设计推崇的方向。
消除向下转型通过多态替代类型检查,提高了类型安全性和代码的可扩展性。
现代JavaScript/TypeScript提供了多种继承的替代方案:接口、混入、组合、高阶函数等。理解这些选项并根据场景选择合适的方案是重要的设计技能。
继承本身不是坏的,但应该谨慎使用。在大多数情况下,组合是更好的选择。记住:"组合优于继承"。
八、习题
理解题: 解释"组合优于继承"的原因。继承在什么情况下仍然是合适的?
实践题: 将以下继承改为组合:
class Animal { eat() { /* ... */ } }
class Dog extends Animal { bark() { /* ... */ } }设计题: 为一个游戏系统设计角色能力系统,要求支持动态添加/移除能力。
分析题: 比较TypeScript接口和类继承的优缺点。
综合题: 重构以下代码,消除向下转型:
function handleShape(shape) {
if (shape instanceof Circle) {
return shape.radius * 2 * Math.PI;
} else if (shape instanceof Rectangle) {
return 2 * (shape.width + shape.height);
}
}讨论题: "继承已经过时了",你同意吗?为什么?
挑战题: 实现一个Mixin系统,支持多个mixin的组合和冲突解决。