包含旧重构:以State/Strategy取代类型码(Replace Type Code with State/Strategy)
包含旧重构:提炼子类(Extract Subclass)
反向重构:移除子类(369)
![](https://box.kancloud.cn/9de2b9431fc432b5ca55834ad978ae7b_688x376.jpeg)
```
function createEmployee(name, type) {
return new Employee(name, type);
}
```
![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg)
```
function createEmployee(name, type) {
switch (type) {
case "engineer": return new Engineer(name);
case "salesman": return new Salesman(name);
case "manager": return new Manager (name);
}
```
### 动机
软件系统经常需要表现“相似但又不同的东西”,比如员工可以按职位分类(工程师、经理、销售),订单可以按优先级分类(加急、常规)。表现分类关系的第一种工具是类型码字段——根据具体的编程语言,可能实现为枚举、符号、字符串或者数字。类型码的取值经常来自给系统提供数据的外部服务。
大多数时候,有这样的类型码就够了。但也有些时候,我可以再多往前一步,引入子类。继承有两个诱人之处。首先,你可以用多态来处理条件逻辑。如果有几个函数都在根据类型码的取值采取不同的行为,多态就显得特别有用。引入子类之后,我可以用以多态取代条件表达式(272)来处理这些函数。
另外,有些字段或函数只对特定的类型码取值才有意义,例如“销售目标”只对“销售”这类员工才有意义。此时我可以创建子类,然后用字段下移(361)把这样的字段放到合适的子类中去。当然,我也可以加入验证逻辑,确保只有当类型码取值正确时才使用该字段,不过子类的形式能更明确地表达数据与类型之间的关系。
在使用以子类取代类型码时,我需要考虑一个问题:应该直接处理携带类型码的这个类,还是应该处理类型码本身呢?以前面的例子来说,我是应该让“工程师”成为“员工”的子类,还是应该在“员工”类包含“员工类别”属性、从后者继承出“工程师”和“经理”等子类型呢?直接的子类继承(前一种方案)比较简单,但职位类别就不能用在其他场合了。另外,如果员工的类别是可变的,那么也不能使用直接继承的方案。如果想在“员工类别”之下创建子类,可以运用以对象取代基本类型(174)把类型码包装成“员工类别”类,然后对其使用以子类取代类型码(362)。
### 做法
- 自封装类型码字段。
- 任选一个类型码取值,为其创建一个子类。覆写类型码类的取值函数,令其返回该类型码的字面量值。
- 创建一个选择器逻辑,把类型码参数映射到新的子类。
> 如果选择直接继承的方案,就用以工厂函数取代构造函数(334)包装构造函数,把选择器逻辑放在工厂函数里;如果选择间接继承的方案,选择器逻辑可以保留在构造函数里。
- 测试。
- 针对每个类型码取值,重复上述“创建子类、添加选择器逻辑”的过程。每次修改后执行测试。
- 去除类型码字段。
- 测试。
- 使用函数下移(359)和以多态取代条件表达式(272)处理原本访问了类型码的函数。全部处理完后,就可以移除类型码的访问函数。
### 范例
这个员工管理系统的例子已经被用烂了……
##### class Employee...
```
constructor(name, type){
this.validateType(type);
this._name = name;
this._type = type;
}
validateType(arg) {
if (!["engineer", "manager", "salesman"].includes(arg))
throw new Error(`Employee cannot be of type ${arg}`);
}
toString() {return `${this._name} (${this._type})`;}
```
第一步是用封装变量(132)将类型码自封装起来。
##### class Employee...
```
get type() {return this._type;}
toString() {return `${this._name} (${this.type})`;}
```
请注意,`toString`函数的实现中去掉了`this._type`的下划线,改用新建的取值函数了。
我选择从工程师("`engineer`")这个类型码开始重构。我打算采用直接继承的方案,也就是继承`Employee`类。子类很简单,只要覆写类型码的取值函数,返回适当的字面量值就行了。
```
class Engineer extends Employee {
get type() {return "engineer";}
}
```
虽然JavaScript的构造函数也可以返回其他对象,但如果把选择器逻辑放在这儿,它会与字段初始化逻辑相互纠缠,搞得一团混乱。所以我会先运用以工厂函数取代构造函数(334),新建一个工厂函数以便安放选择器逻辑。
```
function createEmployee(name, type) {
return new Employee(name, type);
}
```
然后我把选择器逻辑放在工厂函数中,从而开始使用新的子类。
```
function createEmployee(name, type) {
switch (type) {
case "engineer": return new Engineer(name, type);
}
return new Employee(name, type);
}
```
测试,确保一切运转正常。不过由于我的偏执,我随后会修改`Engineer`类中覆写的`type`函数,让它返回另外一个值,再次执行测试,确保会有测试失败,这样我才能肯定:新建的子类真的被用到了。然后我把`type`函数的返回值改回正确的状态,继续处理别的类型。我一次处理一个类型,每次修改后都执行测试。
```
class Salesman extends Employee {
get type() {return "salesman";}
}
class Manager extends Employee {
get type() {return "manager";}
}
function createEmployee(name, type) {
switch (type) {
case "engineer": return new Engineer(name, type);
case "salesman": return new Salesman(name, type);
case "manager": return new Manager (name, type);
}
return new Employee(name, type);
}
```
全部修改完成后,我就可以去掉类型码字段及其在超类中的取值函数(子类中的取值函数仍然保留)。
##### class Employee...
```
constructor(name, type){
this.validateType(type);
this._name = name;
this._type = type;
}
get type() {return this._type;}
toString() {return `${this._name} (${this.type})`;}
```
测试,确保一切工作正常,我就可以移除验证逻辑,因为分发逻辑做的是同一回事。
##### class Employee...
```
constructor(name, type){
this.validateType(type);
this._name = name;
}
function createEmployee(name, type) {
switch (type) {
case "engineer": return new Engineer(name, type);
case "salesman": return new Salesman(name, type);
case "manager": return new Manager (name, type);
default: throw new Error(`Employee cannot be of type ${type}`);
}
return new Employee(name, type);
}
```
现在,构造函数的类型参数已经没用了,用改变函数声明(124)把它干掉。
##### class Employee...
```
constructor(name, type){
this._name = name;
}
function createEmployee(name, type) {
switch (type) {
case "engineer": return new Engineer(name, type);
case "salesman": return new Salesman(name, type);
case "manager": return new Manager (name, type);
default: throw new Error(`Employee cannot be of type ${type}`);
}
}
```
子类中获取类型码的访问函数——`get type`函数——仍然留着。通常我会希望把这些函数也干掉,不过可能需要多花点儿时间,因为有其他函数使用了它们。我会用以多态取代条件表达式(272)和函数下移(359)来处理这些访问函数。到某个时候,已经没有代码使用类型码的访问函数了,我再用移除死代码(237)给它们送终。
### 范例:使用间接继承
还是前面这个例子,我们回到最起初的状态,不过这次我已经有了“全职员工”和“兼职员工”两个子类,所以不能再根据员工类别代码创建子类了。另外,我可能需要允许员工类别动态调整,这也会导致不能使用直接继承的方案。
##### class Employee...
```
constructor(name, type){
this.validateType(type);
this._name = name;
this._type = type;
}
validateType(arg) {
if (!["engineer", "manager", "salesman"].includes(arg))
throw new Error(`Employee cannot be of type ${arg}`);
}
get type() {return this._type;}
set type(arg) {this._type = arg;}
get capitalizedType() {
return this._type.charAt(0).toUpperCase() + this._type.substr(1).toLowerCase();
}
toString() {
return `${this._name} (${this.capitalizedType})`;
}
```
这次的`toString`函数要更复杂一点,以便稍后展示用。
首先,我用以对象取代基本类型(174)包装类型码。
```
class EmployeeType {
constructor(aString) {
this._value = aString;
}
toString() {return this._value;}
}
```
##### class Employee...
```
constructor(name, type){
this.validateType(type);
this._name = name;
this.type = type;
}
validateType(arg) {
if (!["engineer", "manager", "salesman"].includes(arg))
throw new Error(`Employee cannot be of type ${arg}`);
}
get typeString() {return this._type.toString();}
get type() {return this._type;}
set type(arg) {this._type = new EmployeeType(arg);}
get capitalizedType() {
return this.typeString.charAt(0).toUpperCase()
+ this.typeString.substr(1).toLowerCase();
}
toString() {
return `${this._name} (${this.capitalizedType})`;
}
```
然后使用以子类取代类型码(362)的老套路,把员工类别代码变成子类。
##### class Employee...
```
set type(arg) {this._type = Employee.createEmployeeType(arg);}
static createEmployeeType(aString) {
switch(aString) {
case "engineer": return new Engineer();
case "manager": return new Manager ();
case "salesman": return new Salesman();
default: throw new Error(`Employee cannot be of type ${aString}`);
}
}
class EmployeeType {
}
class Engineer extends EmployeeType {
toString() {return "engineer";}
}
class Manager extends EmployeeType {
toString() {return "manager";}
}
class Salesman extends EmployeeType {
toString() {return "salesman";}
}
```
如果重构到此为止的话,空的`EmployeeType`类可以去掉。但我更愿意留着它,用来明确表达各个子类之间的关系。并且有一个超类,也方便把其他行为搬移进去,例如我专门放在`toString`函数里的“名字大写”逻辑,就可以搬到超类。
##### class Employee...
```
toString() {
return `${this._name} (${this.type.capitalizedName})`;
}
```
##### class EmployeeType...
```
get capitalizedName() {
return this.toString().charAt(0).toUpperCase()
+ this.toString().substr(1).toLowerCase();
}
```
熟悉本书第1版的读者大概能看出,这个例子来自第1版的以State/Strategy取代类型码重构手法。现在我认为这是以间接继承的方式使用以子类取代类型码,所以就不再将其作为一个单独的重构手法了。(而且我也一直不喜欢那个老重构手法的名字。)
- 第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)
- 参考文献
- 重构列表
- 坏味道与重构手法速查表