12重构(第2版)

处理继承关系

继承体系的优化

Pull Up FieldPush Down Method

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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)

提取超类是从两个或多个相似类中提取共同行为到一个新的超类中。

动机:

  • 多个类有相似的字段和方法
  • 希望复用共同的行为
  • 建立类型层次以支持多态

识别机会:

javascript
// 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)

折叠继承体系是移除不必要的继承层级,将超类和子类合并。

动机:

  • 超类和子类差异很小,分离没有价值
  • 继承层级过深,简化结构
  • 超类变得过于抽象,难以理解

识别机会:

javascript
// 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)

以委托取代子类是用委托对象替代子类,用组合替代继承。

动机:

  • 子类只在一两个方面与超类不同
  • 多种维度的变化导致子类爆炸
  • 希望在运行时改变行为

识别机会:

javascript
// 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)

以委托取代超类是将继承关系改为委托关系,打破不合适的继承。

动机:

  • 子类继承了超类的不需要或不合适的方法
  • 违反了里氏替换原则
  • 超类的变化影响了不相关的子类

识别机会:

javascript
// 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中需要类型断言,失去类型安全

识别机会:

javascript
// 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):

typescript
interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

class Duck implements Flyable, Swimmable {
  fly() { /* ... */ }
  swim() { /* ... */ }
}

混入(Mixin):

javascript
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)) {}

组合:

javascript
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'
});

高阶函数:

javascript
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:

javascript
// 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>;
}

场景二:游戏角色的能力系统

javascript
// 使用组合而非继承
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 示例一:完整的继承重构

javascript
// 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接口替代继承

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提供了多种继承的替代方案:接口、混入、组合、高阶函数等。理解这些选项并根据场景选择合适的方案是重要的设计技能。

继承本身不是坏的,但应该谨慎使用。在大多数情况下,组合是更好的选择。记住:"组合优于继承"。


八、习题

理解题: 解释"组合优于继承"的原因。继承在什么情况下仍然是合适的?

实践题: 将以下继承改为组合:

javascript
class Animal { eat() { /* ... */ } }
class Dog extends Animal { bark() { /* ... */ } }

设计题: 为一个游戏系统设计角色能力系统,要求支持动态添加/移除能力。

分析题: 比较TypeScript接口和类继承的优缺点。

综合题: 重构以下代码,消除向下转型:

javascript
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的组合和冲突解决。

12/12处理继承
0%