18、处理继承(上):函数上移、字段上移、构造函数本体上移、函数下移
继承这东西,用好了是神器,用砸了就是灾难。我在项目里见过太多继承滥用的情况——一个基类堆了上百个方法,子类只用到其中两三个。说白了,继承关系也需要持续维护,就像代码本身一样。
今天我们先聊四个最常用的重构手法:函数上移、字段上移、构造函数本体上移、函数下移。它们解决的核心问题就一个:让继承体系中的职责归属更合理。
核心原则:子类共有的东西往上提,子类独有的东西往下放。
函数上移(Pull Up Method)
先看个场景。两个子类里有完全相同的函数,你打算怎么办?
我个人的习惯是:先别急着抽到基类。先确认一下——这两个函数真的完全一样吗?还是只是长得像?
确认无误后,直接上移。步骤很简单:
- 在两个子类中确认函数签名和实现完全一致
- 在基类中声明该函数
- 删除子类中的重复实现
来看个例子:
// 重构前
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 { }
嗯,这里要注意:如果两个函数只是实现相似但逻辑不同,千万别硬上移。我曾经在一个项目中看到有人把两个完全不同的计算逻辑强行合并到一个基类方法里,结果加了三个if分支——那还不如不重构。
小技巧:上移之前,先用「模板方法模式」的思路审视一下。如果子类函数只有部分步骤不同,可以考虑把公共部分上移,差异部分留给子类实现。
字段上移(Pull Up Field)
字段上移比函数上移更简单,但坑也不少。
两个子类有同名字段?上移到基类。但有个前提:字段的用途必须一致。
你想想看,如果Salesman里的_rate是小时费率,Engineer里的_rate是项目费率——虽然名字一样,但语义不同,上移就会出问题。
// 重构前
class Salesman extends Employee {
private String _name;
}
class Engineer extends Employee {
private String _name;
}
// 重构后
class Employee {
protected String _name;
}
class Salesman extends Employee { }
class Engineer extends Employee { }
这里有个细节:字段的访问权限要改成protected。我之前见过有人忘了改,结果子类访问不到,又加了一堆getter/setter——绕了一大圈。
避坑指南:我曾经把两个子类的「同名不同义」字段上移了,导致后续三个月都在修bug。所以上移前,一定确认字段语义一致。
构造函数本体上移(Pull Up Constructor Body)
这个手法稍微复杂一点。子类的构造函数里有公共代码,你想把它提到基类构造函数里。
但构造函数有个特殊性:子类构造函数必须先调用父类构造函数。所以上移的时候,不能简单地把代码复制过去。
正确的做法是:
- 在基类中定义一个新的构造函数,接收公共参数
- 子类构造函数调用
super()并传入参数 - 删除子类构造函数中的公共代码
// 重构前
class Employee {
protected String _name;
protected String _id;
}
class Salesman extends Employee {
Salesman(String name, String id, Double salesTarget) {
this._name = name;
this._id = id;
this._salesTarget = salesTarget;
}
}
// 重构后
class Employee {
Employee(String name, String id) {
this._name = name;
this._id = id;
}
}
class Salesman extends Employee {
Salesman(String name, String id, Double salesTarget) {
super(name, id);
this._salesTarget = salesTarget;
}
}
你可能会问:如果基类已经有构造函数了怎么办?那就用重载。别去改已有的构造函数签名,那会影响其他子类。
个人经验:构造函数上移时,我习惯先把子类构造函数中的公共代码圈出来,然后问自己一个问题——「这些代码真的属于构造函数吗?」有时候,初始化逻辑更适合放到一个独立的initialize()方法里。
函数下移(Push Down Method)
函数下移是上移的反向操作。当基类中的某个方法只被部分子类使用时,就该把它下移。
说白了,基类应该只放所有子类都需要的公共行为。如果一个方法只有两三个子类在用,那它就不该待在基类里。
// 重构前
class Employee {
void calculateQuota() { ... }
}
class Salesman extends Employee { }
class Engineer extends Employee { } // 不需要calculateQuota
// 重构后
class Employee { }
class Salesman extends Employee {
void calculateQuota() { ... }
}
class Engineer extends Employee { }
嗯,这里要注意:下移之前,先检查基类中是否有其他方法调用了这个即将下移的方法。如果有,你得考虑要不要一起下移,或者改成模板方法。
避坑指南:我曾经把一个基类方法下移后,发现另一个子类通过super调用了它——结果编译报错。所以下移前,最好全局搜索一下调用链。
知识体系总览
这四个手法其实构成了一个完整的继承重构闭环。我画了张图帮你理清思路:
如何选择用哪个?
我整理了一个简单的判断表,帮你快速决策:
| 场景 | 推荐手法 | 注意事项 |
|---|---|---|
| 多个子类有相同方法 | 函数上移 | 确认实现完全一致,语义相同 |
| 多个子类有相同字段 | 字段上移 | 字段用途必须一致,访问权限改protected |
| 子类构造函数有公共初始化代码 | 构造函数本体上移 | 用super()调用,不要改已有构造函数签名 |
| 基类方法只被部分子类使用 | 函数下移 | 检查调用链,避免super引用 |
总结一下:这四个手法是继承重构的入门基本功。上移解决重复,下移解决职责错位。我个人建议:每次修改继承体系时,都先问自己一句——这个方法/字段放在这里合理吗? 养成这个习惯,你的继承体系会越来越干净。
嗯,今天就先聊到这儿。这四个手法看着简单,但用好了能解决继承体系中80%的坏味道。下一节我们继续聊继承重构的另外几个手法——到时候你会看到,有些问题比今天讲的要棘手得多。