曾用名:合并重复的代码片段(Consolidate Duplicate Conditional Fragments)

```
const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;
```

```
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder();
let charge;
```
### 动机
让存在关联的东西一起出现,可以使代码更容易理解。如果有几行代码取用了同一个数据结构,那么最好是让它们在一起出现,而不是夹杂在取用其他数据结构的代码中间。最简单的情况下,我只需使用移动语句就可以让它们聚集起来。此外还有一种常见的“关联”,就是关于变量的声明和使用。有人喜欢在函数顶部一口气声明函数用到的所有变量,我个人则喜欢在第一次需要使用变量的地方再声明它。
通常来说,把相关代码搜集到一处,往往是另一项重构(通常是在提炼函数(106))开始之前的准备工作。相比于仅仅把几行相关的代码移动到一起,将它们提炼到独立的函数往往能起到更好的抽象效果。但如果起先存在关联的代码就没有彼此在一起,那么我也很难应用提炼函数(106)的手法。
### 做法
- 确定待移动的代码片段应该被搬往何处。仔细检查待移动片段与目的地之间的语句,看看搬移后是否会影响这些代码正常工作。如果会,则放弃这项重构。
> 往前移动代码片段时,如果片段中声明了变量,则不允许移动到任何变量的声明语句之前。往后移动代码片段时,如果有语句引用了待移动片段中的变量,则不允许移动到该语句之后。往后移动代码片段时,如果有语句修改了待移动片段中引用的变量,则不允许移动到该语句之后。往后移动代码片段时,如果片段中修改了某些元素,则不允许移动到任何引用了这些元素的语句之后。
- 剪切源代码片段,粘贴到上一步选定的位置上。
- 测试。
如果测试失败,那么尝试减小移动的步子:要么是减少上下移动的行数,要么是一次搬移更少的代码。
### 范例
移动代码片段时,通常需要想清楚两件事:本次调整的目标是什么,以及该目标能否达到。第一件事通常取决于代码所在的上下文。最简单的情况是,我希望元素的声明点和使用点互相靠近,因此移动语句的目标便是将元素的声明语句移动到靠近它们的使用处。不过大多数时候,我移动代码的动机都是因为想做另一项重构,比如在应用提炼函数(106)之前先将相关的代码集中到一块,以方便做函数提炼。
确定要把代码移动到哪里之后,我就需要思考第二个问题,也就是此次搬移能否做到的问题。为此我需要观察待移动的代码,以及移动中间经过的代码段,我得思考这个问题:如果我把代码移动过去,执行次序的不同会不会使代码之间产生干扰,甚至于改变程序的可观测行为?
请观察以下代码片段:
```
1 const pricingPlan = retrievePricingPlan();
2 const order = retreiveOrder();
3 const baseCharge = pricingPlan.base;
4 let charge;
5 const chargePerUnit = pricingPlan.unit;
6 const units = order.units;
7 let discount;
8 charge = baseCharge + units * chargePerUnit;
9 let discountableUnits = Math.max(units - pricingPlan.discountThreshold, 0);
10 discount = discountableUnits * pricingPlan.discountFactor;
11 if (order.isRepeat) discount += 20;
12 charge = charge - discount;
13 chargeOrder(charge);
```
前七行是变量的声明语句,移动它们通常很简单。假如我想把与处理折扣(discount)相关的代码搬移到一起,那么我可以直接将第7行(`let discount`)移动到第10行上面(`discount = ...`那一行)。因为变量声明没有副作用,也不会引用其他变量,所以我可以很安全地将声明语句往后移动,一直移动到引用`discount`变量的语句之上。此种类型的语句移动也十分常见——当我要提炼函数(106)时,通常得先将相关变量的声明语句搬移过来。
我会再寻找类似的没有副作用的变量声明语句。类似地,我可以毫无障碍地把声明了`order`变量的第2行(`const order = ...`)移动到使用它的第6行(`const units = ...`)上面。
上面搬移变量声明语句之所以顺利,除了因为语句本身没有副作用,还得益于我移动语句时跨过的代码片段同样没有副作用。事实上,对于没有副作用的代码,我几乎可以随心所欲地编排它们的顺序,这也是优秀的程序员都会尽量编写无副作用代码的原因之一。
当然,这里还有一个小细节,那就是我从何得知第2行代码没有副作用呢?我只有深入检查`retrieveOrder()`函数的内部实现,才能真正确保它确实没有副作用(除了检查函数本身,还得检查它内部调用的函数都没有副作用,以及它调用的函数内部调用的函数都没有副作用……一直检查到调用链的底端)。实践中,我编写代码总是尽量遵循命令与查询分离(Command-Query Separation)\[mf-cqs\]原则,在这个前提下,我可以确定任何有返回值的函数都不存在副作用。但只有在我了解代码库的前提下才如此自信;如果我对代码库还不熟悉,我就得更加小心。但在我自己的编码过程中,我确实总是尽量遵循命令与查询分离的模式,因为它让我一眼就能看清代码有无副作用,而这件事情真是价值不菲。
如果待移动的代码片段本身有副作用,或者它需要跨越的代码存在副作用,移动它们时就必须加倍小心。我得仔细寻找两个代码片段中间的代码有没有副作用,是不是对执行次序敏感。因此,假设我想将第11行(`if(order.isRepeat)...`)挪动到段落底部,我会发现行不通,因为中间第12行语句引用了`discount`变量,而我在第11行中可能改动这个变量;类似地,假设我想将第13行(`chargeOrder(charge)`)往上搬移,那也是行不通的,因为第13行引用的`charge`变量在第12行会被修改。不过,如果我想将第8行代码(`charge = baseCharge + ...`)移动到第9行到第11行中间的任意地方则是可行的,因为这几行都未修改任何变量的状态。
移动代码时,最容易遵守的一条规则是,如果待移动代码片段中引用的变量在另一个代码片段中被修改了,那我就不能安全地将前者移动到后者之后;同样,如果前者会修改后者中引用的变量,也一样不能安全地进行上述移动。但这条规则仅仅作为参考,它也不是绝对的,比如下面这个例子,虽然两个语句都修改了彼此之间的变量,但我仍能安全地调整它们的先后顺序。
```
a = a + 10;
a = a + 5;
```
但无论如何,要判断一次语句移动是否安全,都意味着我得真正理解代码的工作原理,以及运算符之间的组合方式等。
正因此项重构如此需要关注状态更新,所以我会尽量移除那些会更新元素状态的代码。比如此例中的`charge`变量,在移动其相关的代码之前,我会先看看是否能对它应用拆分变量(240)手法。
以上的分析都比较简单,没什么难度,因为代码里修改的都是局部变量,局部变量是比较好处理的。但处理更复杂的数据结构时,情况就不同了,判断代码段之间是否存在相互干扰会困难得多。这时测试扮演了重要角色:每次移动代码之后运行测试,看看有没有任何测试失败。如果我的测试覆盖足够全面,我就会对这次重构比较有信心;但如果测试覆盖不够,我就得小心一些了——通常,我会先改善代码的测试,然后再进行重构。
如果移动过后测试失败了,那么意味着我得减小移动的步子,比如一次先移动5行,而不是10行;或者先移动到那些看着比较可能出错的代码上面,但不越过它,看看效果。同时,测试失败也可能是一个征兆,提醒我这次移动可能还不是时候,可能还需要在别处先做一些其他的工作。
### 范例:包含条件逻辑的移动
对于拥有条件逻辑的代码,移动手法同样适用。当从条件分支中移走代码时,通常是要消除重复逻辑;将代码移入条件分支时,通常是反过来,有意添加一些重复逻辑。
在下面这个例子中,两个条件分支里都有一个相同的语句:
```
let result;
if (availableResources.length === 0) {
result = createResource();
allocatedResources.push(result);
} else {
result = availableResources.pop();
allocatedResources.push(result);
}
return result;
```
我可以将这两句重复代码从条件分支中移走,只在`if-else`块的末尾保留一句。
```
let result;
if (availableResources.length === 0) {
result = createResource();
} else {
result = availableResources.pop();
}
allocatedResources.push(result);
return result;
```
这个手法同样可以反过来用,也就是把一个语句分别搬移到不同的条件分支里,这样会在每个条件分支里留下同一段重复的代码。
### 延伸阅读
除了我介绍的这个方法,我还见过一个十分相似的重构手法,名字叫作“交换语句位置”(Swap Statement)\[wake-swap\]。该手法同样适用于移动相邻的代码片段,只不过它适用的是只有一条语句的片段。你可以把它想成移动语句手法的一个特例,也就是待移动的代码片段以及它所跨过的代码片段,都只有一条语句。我对这项重构手法很感兴趣,毕竟我也一直在强调小步修改——有时甚至小步到于初学重构的人看来都很不可思议的地步。
但最后,我还是选择在本重构手法中介绍如何移动范围更大的代码片段,因为我自己平时就是这么做的。我只有在处理大范围的语句移动遇到困难时才会变得小步、一次只移动一条语句,但即便是这样的困难我也很少遇见。无论如何,当代码过于复杂凌乱时,小步的移动通常会更加顺利。
- 第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)
- 参考文献
- 重构列表
- 坏味道与重构手法速查表
