曾用名:以字段取代子类(Replace Subclass with Fields)
反向重构:以子类取代类型码(362)
![](https://box.kancloud.cn/a246072830e6dd2dde96f67569905427_417x382.jpeg)
```
class Person {
get genderCode() {return "X";}
}
class Male extends Person {
get genderCode() {return "M";}
}
class Female extends Person {
get genderCode() {return "F";}
}
```
![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg)
```
class Person {
get genderCode() {return this._genderCode;}
}
```
### 动机
子类很有用,它们为数据结构的多样和行为的多态提供支持,它们是针对差异编程的好工具。但随着软件的演化,子类所支持的变化可能会被搬移到别处,甚至完全去除,这时子类就失去了价值。有时添加子类是为了应对未来的功能,结果构想中的功能压根没被构造出来,或者用了另一种方式构造,使该子类不再被需要了。
子类存在着就有成本,阅读者要花心思去理解它的用意,所以如果子类的用处太少,就不值得存在了。此时,最好的选择就是移除子类,将其替换为超类中的一个字段。
### 做法
- 使用以工厂函数取代构造函数(334),把子类的构造函数包装到超类的工厂函数中。
> 如果构造函数的客户端用一个数组字段来决定实例化哪个子类,可以把这个判断逻辑放到超类的工厂函数中。
- 如果有任何代码检查子类的类型,先用提炼函数(106)把类型检查逻辑包装起来,然后用搬移函数(198)将其搬到超类。每次修改后执行测试。
- 新建一个字段,用于代表子类的类型。
- 将原本针对子类的类型做判断的函数改为使用新建的类型字段。
- 删除子类。
- 测试。
本重构手法常用于一次移除多个子类,此时需要先把这些子类都封装起来(添加工厂函数、搬移类型检查),然后再逐个将它们折叠到超类中。
### 范例
一开始,代码中遗留了两个子类。
##### class Person...
```
constructor(name) {
this._name = name;
}
get name() {return this._name;}
get genderCode() {return "X";}
// snip
class Male extends Person {
get genderCode() {return "M";}
}
class Female extends Person {
get genderCode() {return "F";}
}
```
如果子类就干这点儿事,那真的没必要存在。不过,在移除子类之前,通常有必要检查使用方代码是否有依赖于特定子类的行为,这样的行为需要被搬移到子类中。在这个例子里,我找到一些客户端代码基于子类的类型做判断,不过这也不足以成为保留子类的理由。
##### 客户端...
`const numberOfMales = people.filter(p => p instanceof Male).length;`每当想要改变某个东西的表现形式时,我会先将当下的表现形式封装起来,从而尽量减小对客户端代码的影响。对于“创建子类对象”而言,封装的方式就是以工厂函数取代构造函数(334)。在这里,实现工厂有两种方式。
最直接的方式是为每个构造函数分别创建一个工厂函数。
```
function createPerson(name) {
return new Person(name);
}
function createMale(name) {
return new Male(name);
}
function createFemale(name) {
return new Female(name);
}
```
虽然这是最直接的选择,但这样的对象经常是从输入源加载出来,直接根据性别代码创建对象。
```
function loadFromInput(data) {
const result = [];
data.forEach(aRecord => {
let p;
switch (aRecord.gender) {
case 'M': p = new Male(aRecord.name); break;
case 'F': p = new Female(aRecord.name); break;
default: p = new Person(aRecord.name);
}
result.push(p);
});
return result;
}
```
有鉴于此,我觉得更好的办法是先用提炼函数(106)把“选择哪个类来实例化”的逻辑提炼成工厂函数。
```
function createPerson(aRecord) {
let p;
switch (aRecord.gender) {
case 'M': p = new Male(aRecord.name); break;
case 'F': p = new Female(aRecord.name); break;
default: p = new Person(aRecord.name);
}
return p;
}
function loadFromInput(data) {
const result = [];
data.forEach(aRecord => {
result.push(createPerson(aRecord));
});
return result;
}
```
提炼完工厂函数后,我会对这两个函数做些清理。先用内联变量(123)简化`createPerson`函数:
```
function createPerson(aRecord) {
switch (aRecord.gender) {
case 'M': return new Male (aRecord.name);
case 'F': return new Female(aRecord.name);
default: return new Person(aRecord.name);
}
}
```
再用以管道取代循环(231)简化`loadFromInput`函数:
```
function loadFromInput(data) {
return data.map(aRecord => createPerson(aRecord));
}
```
工厂函数封装了子类的创建逻辑,但代码中还有一处用到`instanceof`运算符——这从来不会是什么好味道。我用提炼函数(106)把这个类型检查逻辑提炼出来。
##### 客户端...
```
const numberOfMales = people.filter(p => isMale(p)).length;
function isMale(aPerson) {return aPerson instanceof Male;}
```
然后用搬移函数(198)将其移到`Person`类。
##### class Person...
`get isMale() {return this instanceof Male;}`##### 客户端...
`const numberOfMales = people.filter(p => p.isMale).length;`重构到这一步,所有与子类相关的知识都已经安全地包装在超类和工厂函数中。(对于“超类引用子类”这种情况,通常我会很警惕,不过这段代码用不了一杯茶的工夫就会被干掉,所以也不用太担心。)
现在,添加一个字段来表示子类之间的差异。既然有来自别处的一个类型代码,直接用它也无妨。
##### class Person...
```
constructor(name, genderCode) {
this._name = name;
this._genderCode = genderCode || "X";
}
get genderCode() {return this._genderCode;}
```
在初始化时先将其设置为默认值。(顺便说一句,虽然大多数人可以归类为男性或女性,但确实有些人不是这两种性别中的任何一种。忽视这些人的存在,是一个常见的建模错误。)
首先从“男性”的情况开始,将相关逻辑折叠到超类中。为此,首先要修改工厂函数,令其返回一个`Person`对象,然后修改所有`instanceof`检查逻辑,改为使用性别代码字段。
```
function createPerson(aRecord) {
switch (aRecord.gender) {
case 'M': return new Person(aRecord.name, "M");
case 'F': return new Female(aRecord.name);
default: return new Person(aRecord.name);
}
}
```
##### class Person...
`get isMale() {return "M" === this._genderCode;}`此时我可以测试,删除`Male`子类,再次测试,然后对`Female`子类也如法炮制。
```
function createPerson(aRecord) {
switch (aRecord.gender) {
case 'M': return new Person(aRecord.name, "M");
case 'F': return new Person(aRecord.name, "F");
default: return new Person(aRecord.name);
}
}
```
类型代码的分配有点儿失衡,默认情况没有类型代码,这种情况让我很烦心。未来阅读代码的人会一直好奇背后的原因。所以我更愿意现在做点儿修改,给所有情况都平等地分配类型代码——只要不会引入额外的复杂性就好。
```
function createPerson(aRecord) {
switch (aRecord.gender) {
case 'M': return new Person(aRecord.name, "M");
case 'F': return new Person(aRecord.name, "F");
default: return new Person(aRecord.name, "X");
}
}
```
##### class Person...
```
constructor(name, genderCode) {
this._name = name;
this._genderCode = genderCode || "X";
}
```
- 第1章 重构,第一个示例
- 1.1 起点
- 1.2 对此起始程序的评价
- 1.3 重构的第一步
- 1.4 分解statement函数
- 1.5 进展:大量嵌套函数
- 1.6 拆分计算阶段与格式化阶段
- 1.7 进展:分离到两个文件(和两个阶段)
- 1.8 按类型重组计算过程
- 1.9 进展:使用多态计算器来提供数据
- 1.10 结语
- 第2章 重构的原则
- 2.1 何谓重构
- 2.2 两顶帽子
- 2.3 为何重构
- 2.4 何时重构
- 2.5 重构的挑战
- 2.6 重构、架构和YAGNI
- 2.7 重构与软件开发过程
- 2.8 重构与性能
- 2.9 重构起源何处
- 2.10 自动化重构
- 2.11 延展阅读
- 第3章 代码的坏味道
- 3.1 神秘命名(Mysterious Name)
- 3.2 重复代码(Duplicated Code)
- 3.3 过长函数(Long Function)
- 3.4 过长参数列表(Long Parameter List)
- 3.5 全局数据(Global Data)
- 3.6 可变数据(Mutable Data)
- 3.7 发散式变化(Divergent Change)
- 3.8 霰弹式修改(Shotgun Surgery)
- 3.9 依恋情结(Feature Envy)
- 3.10 数据泥团(Data Clumps)
- 3.11 基本类型偏执(Primitive Obsession)
- 3.12 重复的switch (Repeated Switches)
- 3.13 循环语句(Loops)
- 3.14 冗赘的元素(Lazy Element)
- 3.15 夸夸其谈通用性(Speculative Generality)
- 3.16 临时字段(Temporary Field)
- 3.17 过长的消息链(Message Chains)
- 3.18 中间人(Middle Man)
- 3.19 内幕交易(Insider Trading)
- 3.20 过大的类(Large Class)
- 3.21 异曲同工的类(Alternative Classes with Different Interfaces)
- 3.22 纯数据类(Data Class)
- 3.23 被拒绝的遗赠(Refused Bequest)
- 3.24 注释(Comments)
- 第4章 构筑测试体系
- 4.1 自测试代码的价值
- 4.2 待测试的示例代码
- 4.3 第一个测试
- 4.4 再添加一个测试
- 4.5 修改测试夹具
- 4.6 探测边界条件
- 4.7 测试远不止如此
- 第5章 介绍重构名录
- 5.1 重构的记录格式
- 5.2 挑选重构的依据
- 第6章 第一组重构
- 6.1 提炼函数(Extract Function)
- 6.2 内联函数(Inline Function)
- 6.3 提炼变量(Extract Variable)
- 6.4 内联变量(Inline Variable)
- 6.5 改变函数声明(Change Function Declaration)
- 6.6 封装变量(Encapsulate Variable)
- 6.7 变量改名(Rename Variable)
- 6.8 引入参数对象(Introduce Parameter Object)
- 6.9 函数组合成类(Combine Functions into Class)
- 6.10 函数组合成变换(Combine Functions into Transform)
- 6.11 拆分阶段(Split Phase)
- 第7章 封装
- 7.1 封装记录(Encapsulate Record)
- 7.2 封装集合(Encapsulate Collection)
- 7.3 以对象取代基本类型(Replace Primitive with Object)
- 7.4 以查询取代临时变量(Replace Temp with Query)
- 7.5 提炼类(Extract Class)
- 7.6 内联类(Inline Class)
- 7.7 隐藏委托关系(Hide Delegate)
- 7.8 移除中间人(Remove Middle Man)
- 7.9 替换算法(Substitute Algorithm)
- 第8章 搬移特性
- 8.1 搬移函数(Move Function)
- 8.2 搬移字段(Move Field)
- 8.3 搬移语句到函数(Move Statements into Function)
- 8.4 搬移语句到调用者(Move Statements to Callers)
- 8.5 以函数调用取代内联代码(Replace Inline Code with Function Call)
- 8.6 移动语句(Slide Statements)
- 8.7 拆分循环(Split Loop)
- 8.8 以管道取代循环(Replace Loop with Pipeline)
- 8.9 移除死代码(Remove Dead Code)
- 第9章 重新组织数据
- 9.1 拆分变量(Split Variable)
- 9.2 字段改名(Rename Field)
- 9.3 以查询取代派生变量(Replace Derived Variable with Query)
- 9.4 将引用对象改为值对象(Change Reference to Value)
- 9.5 将值对象改为引用对象(Change Value to Reference)
- 第10章 简化条件逻辑
- 10.1 分解条件表达式(Decompose Conditional)
- 10.2 合并条件表达式(Consolidate Conditional Expression)
- 10.3 以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)
- 10.4 以多态取代条件表达式(Replace Conditional with Polymorphism)
- 10.5 引入特例(Introduce Special Case)
- 10.6 引入断言(Introduce Assertion)
- 第11章 重构API
- 11.1 将查询函数和修改函数分离(Separate Query from Modifier)
- 11.2 函数参数化(Parameterize Function)
- 11.3 移除标记参数(Remove Flag Argument)
- 11.4 保持对象完整(Preserve Whole Object)
- 11.5 以查询取代参数(Replace Parameter with Query)
- 11.6 以参数取代查询(Replace Query with Parameter)
- 11.7 移除设值函数(Remove Setting Method)
- 11.8 以工厂函数取代构造函数(Replace Constructor with Factory Function)
- 11.9 以命令取代函数(Replace Function with Command)
- 11.10 以函数取代命令(Replace Command with Function)
- 第12章 处理继承关系
- 12.1 函数上移(Pull Up Method)
- 12.2 字段上移(Pull Up Field)
- 12.3 构造函数本体上移(Pull Up Constructor Body)
- 12.4 函数下移(Push Down Method)
- 12.5 字段下移(Push Down Field)
- 12.6 以子类取代类型码(Replace Type Code with Subclasses)
- 12.7 移除子类(Remove Subclass)
- 12.8 提炼超类(Extract Superclass)
- 12.9 折叠继承体系(Collapse Hierarchy)
- 12.10 以委托取代子类(Replace Subclass with Delegate)
- 12.11 以委托取代超类(Replace Superclass with Delegate)
- 参考文献
- 重构列表
- 坏味道与重构手法速查表