17、处理继承(一):函数上移、字段上移、构造函数本体上移、函数下移
继承,是面向对象设计里最基础也最容易用错的东西。我见过太多人一上来就搞三层五层的继承树,结果改一个基类,整个系统跟着抖三抖。其实重构继承关系,核心就两件事:把该共用的往上提,把该独立的往下放。
今天我们先聊四个最常用的手法:函数上移、字段上移、构造函数本体上移、函数下移。它们就像整理衣柜——把全家都能穿的衣服挂到公共区,把只有你穿的那件放回自己房间。
为什么需要整理继承关系?
你想想看,子类里出现重复代码,是不是很常见?两个子类都有同样的方法,只是实现细节略有不同。这时候直觉告诉我:把共同部分抽到父类去。
但很多人不敢动。怕改了父类影响所有子类。其实恰恰相反——该上移的不上移,才是真正的隐患。重复代码意味着维护成本翻倍,改一个地方还得记着改另一个。
核心原则:如果两个子类有相同的行为或数据,就应该上移到父类。如果某个子类有父类不需要的行为,就应该下移到子类。
函数上移(Pull Up Method)
这是最常用的手法。两个子类里有完全相同的函数,直接把它搬到父类里。
我在项目中遇到过这样一个场景:两个订单处理类都有 calculateTotal() 方法,逻辑一模一样。当时我第一反应是复制粘贴?不,我选择了上移。
步骤很简单:
- 在两个子类中找到完全相同的函数
- 在父类中创建这个函数
- 删除子类中的重复函数
- 检查所有调用点,确保它们仍然正常工作
来看个例子:
// 重构前
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() 调用。
我建议的做法是:
- 在父类中定义一个新的构造函数,接收公共参数
- 子类构造函数中调用
super()并传入参数 - 删除子类构造函数中的重复代码
// 重构前
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