17、处理继承(一):函数上移、字段上移、构造函数本体上移、函数下移

继承,是面向对象设计里最基础也最容易用错的东西。我见过太多人一上来就搞三层五层的继承树,结果改一个基类,整个系统跟着抖三抖。其实重构继承关系,核心就两件事:把该共用的往上提,把该独立的往下放

今天我们先聊四个最常用的手法:函数上移、字段上移、构造函数本体上移、函数下移。它们就像整理衣柜——把全家都能穿的衣服挂到公共区,把只有你穿的那件放回自己房间。

为什么需要整理继承关系?

你想想看,子类里出现重复代码,是不是很常见?两个子类都有同样的方法,只是实现细节略有不同。这时候直觉告诉我:把共同部分抽到父类去

但很多人不敢动。怕改了父类影响所有子类。其实恰恰相反——该上移的不上移,才是真正的隐患。重复代码意味着维护成本翻倍,改一个地方还得记着改另一个。

核心原则:如果两个子类有相同的行为或数据,就应该上移到父类。如果某个子类有父类不需要的行为,就应该下移到子类。

函数上移(Pull Up Method)

这是最常用的手法。两个子类里有完全相同的函数,直接把它搬到父类里。

我在项目中遇到过这样一个场景:两个订单处理类都有 calculateTotal() 方法,逻辑一模一样。当时我第一反应是复制粘贴?不,我选择了上移。

步骤很简单

  1. 在两个子类中找到完全相同的函数
  2. 在父类中创建这个函数
  3. 删除子类中的重复函数
  4. 检查所有调用点,确保它们仍然正常工作

来看个例子:

// 重构前
class Employee {
  // ...
}

class Salesman extends Employee {
  get name() { return this._name; }
}

class Engineer extends Employee {
  get name() { return this._name; }
}

// 重构后
class Employee {
  get name() { return this._name; }
}

class Salesman extends Employee { }
class Engineer extends Employee { }

嗯,这里要注意:如果两个函数只是看起来像,但实现细节不同,不要强行上移。先统一逻辑,再考虑上移。

我的习惯:上移前先用 diff 工具对比两个函数,确认完全一致。肉眼有时候会骗人。

字段上移(Pull Up Field)

字段上移和函数上移是孪生兄弟。如果多个子类都有相同的字段,就把它搬到父类里。

说白了,就是消除数据层面的重复。我记得有一次重构一个支付系统,三个子类都有 _amount 字段,类型和用途完全一样。这种重复不改,后面加新功能时就会到处复制粘贴。

// 重构前
class Salesman extends Employee {
  constructor() {
    this._name = '';
  }
}

class Engineer extends Employee {
  constructor() {
    this._name = '';
  }
}

// 重构后
class Employee {
  constructor() {
    this._name = '';
  }
}

class Salesman extends Employee { }
class Engineer extends Employee { }

这里有个坑:字段的访问权限。如果子类里字段是 private,上移到父类后要改成 protected,否则子类访问不了。我曾经因为这个疏忽,导致编译不过,排查了半天。

避坑指南:上移字段时,记得检查所有子类中对这个字段的引用方式。如果有的子类用 this._field,有的用 super._field,统一成 this._field 更安全。

构造函数本体上移(Pull Up Constructor Body)

这个稍微复杂一点。子类的构造函数里有公共代码,你想把它搬到父类构造函数里。但构造函数有特殊性——你不能直接复制粘贴,因为子类构造函数第一行必须是 super() 调用。

我建议的做法是:

  1. 在父类中定义一个新的构造函数,接收公共参数
  2. 子类构造函数中调用 super() 并传入参数
  3. 删除子类构造函数中的重复代码
// 重构前
class Employee {
  constructor(name) { }
}

class Salesman extends Employee {
  constructor(name, quota) {
    this._name = name;
    this._quota = quota;
  }
}

class Engineer extends Employee {
  constructor(name, project) {
    this._name = name;
    this._project = project;
  }
}

// 重构后
class Employee {
  constructor(name) {
    this._name = name;
  }
}

class Salesman extends Employee {
  constructor(name, quota) {
    super(name);
    this._quota = quota;
  }
}

class Engineer extends Employee {
  constructor(name, project) {
    super(name);
    this._project = project;
  }
}

为什么会这样?因为 this._name = name 这段代码在两个子类里重复了。把它上移到父类构造函数里,子类只需要调用 super(name) 就行。

个人经验:如果子类构造函数里除了公共代码还有私有逻辑,先提取私有逻辑成独立方法,再上移公共部分。这样更干净。

函数下移(Push Down Method)

有上就有下。函数下移和上移正好相反——父类里的某个方法只被部分子类使用,就把它下移到那些子类里

我见过一个系统,父类里有 calculateBonus() 方法,但只有销售类需要,工程师类根本用不上。这种时候,留在父类里反而增加了耦合。

// 重构前
class Employee {
  get quota() { return this._quota; }
}

class Salesman extends Employee { }
class Engineer extends Employee { }

// 重构后
class Employee { }

class Salesman extends Employee {
  get quota() { return this._quota; }
}

class Engineer extends Employee { }

嗯,这里要注意:下移前先确认父类中是否有其他子类依赖这个方法。如果有,就不能下移,或者考虑用模板方法模式重新设计。

我曾经踩过的坑:下移一个方法后,发现父类中还有其他方法调用了它。结果编译报错,又得把调用也一并下移。所以下移前,先检查调用链。

知识体系总览

下面这张图帮你理清这四个手法的关系:

继承重构四手法 父类(基类) 子类 函数上移 重复方法 → 父类 字段上移 重复字段 → 父类 构造函数上移 公共初始化 → 父类 函数下移 特有方法 → 子类 上移:消除重复,提升复用性 | 下移:降低耦合,保持单一职责

实战建议

这四个手法看起来简单,但用好了能解决很多继承问题。我个人总结了几条经验:

  • 先上移后下移:先消除重复,再处理特例。顺序别搞反了。
  • 小步提交:每次只做一个上移或下移,然后跑测试。别一次改太多。
  • 注意测试覆盖:上移/下移后,原有行为不能变。没有测试覆盖的代码,我建议先补测试再重构。
  • 警惕过度继承:如果发现父类里全是空方法或者抛出异常的方法,说明继承层次设计有问题。这时候考虑用组合代替继承。

一句话总结:函数上移和字段上移解决重复问题,构造函数上移解决初始化公共逻辑,函数下移解决职责归属问题。这四个手法是继承重构的基石,掌握了它们,你就能更自信地调整类层次结构。

好了,这一章就到这里。记住:继承不是目的,消除重复、保持清晰才是。下一章我们继续聊继承重构的其他手法,包括提炼子类、提炼超类等。到时候见。


公众号:蓝海资料掘金营,微信deep3321