![](https://box.kancloud.cn/5498417d0be630bae671847b439c5a4f_545x380.jpeg)
```
function base(aReading) {...}
function taxableCharge(aReading) {...}
```
![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg)
```
function enrichReading(argReading) {
const aReading = _.cloneDeep(argReading);
aReading.baseCharge = base(aReading);
aReading.taxableCharge = taxableCharge(aReading);
return aReading;
}
```
### 动机
在软件中,经常需要把数据“喂”给一个程序,让它再计算出各种派生信息。这些派生数值可能会在几个不同地方用到,因此这些计算逻辑也常会在用到派生数据的地方重复。我更愿意把所有计算派生数据的逻辑收拢到一处,这样始终可以在固定的地方找到和更新这些逻辑,避免到处重复。
一个方式是采用数据变换(transform)函数:这种函数接受源数据作为输入,计算出所有的派生数据,将派生数据以字段形式填入输出数据。有了变换函数,我就始终只需要到变换函数中去检查计算派生数据的逻辑。
函数组合成变换的替代方案是函数组合成类(144),后者的做法是先用源数据创建一个类,再把相关的计算逻辑搬移到类中。这两个重构手法都很有用,我常会根据代码库中已有的编程风格来选择使用其中哪一个。不过,两者有一个重要的区别:如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,我就会遭遇数据不一致。
我喜欢把函数组合起来的原因之一,是为了避免计算派生数据的逻辑到处重复。从道理上来说,只用提炼函数(106)也能避免重复,但孤立存在的函数常常很难找到,只有把函数和它们操作的数据放在一起,用起来才方便。引入变换(或者类)都是为了让相关的逻辑找起来方便。
### 做法
- 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
> 这一步通常需要对输入的记录做深复制(deep copy)。此时应该写个测试,确保变换不会修改原来的记录。
- 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段。
> 如果计算逻辑比较复杂,先用提炼函数(106)提炼之。
- 测试。
- 针对其他相关的计算逻辑,重复上述步骤。
### 范例
在我长大的国度,茶是生活中的重要部分,以至于我想象了这样一种特别的公共设施,专门给老百姓供应茶水。每个月,从这个设备上可以得到读数(reading),从而知道每位顾客取用了多少茶。
`reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};`几个不同地方的代码分别根据茶的用量进行计算。一处是计算应该向顾客收取的基本费用。
##### 客户端1...
```
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
```
另一处是计算应该交税的费用—比基本费用要少,因为政府明智地认为,每个市民都有权免税享受一定量的茶水。
##### 客户端2...
```
const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
```
浏览处理这些数据记录的代码,我发现有很多地方在做着相似的计算。这样的重复代码,一旦需要修改(我打赌这只是早晚的问题),就会造成麻烦。我可以用提炼函数(106)来处理这些重复的计算逻辑,但这样提炼出来的函数会散落在程序中,以后的程序员还是很难找到。说真的,我还真在另一块代码中找到了一个这样的函数。
##### 客户端3...
```
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);
function calculateBaseCharge(aReading) {
return baseRate(aReading.month, aReading.year) * aReading.quantity;
}
```
处理这种情况的一个办法是,把所有这些计算派生数据的逻辑搬移到一个变换函数中,该函数接受原始的“读数”作为输入,输出则是增强的“读数”记录,其中包含所有共用的派生数据。
我先要创建一个变换函数,它要做的事很简单,就是复制输入的对象:
```
function enrichReading(original) {
const result = _.cloneDeep(original);
return result;
}
```
我用了Lodash库的`cloneDeep`函数来进行深复制。
这个变换函数返回的本质上仍是原来的对象,只是添加了更多的信息在上面。对于这种函数,我喜欢用“enrich”(增强)这个词来给它命名。如果它生成的是跟原来完全不同的对象,我就会用“transform”(变换)来命名它。
然后我挑选一处想要搬移的计算逻辑。首先,我用现在的`enrichReading`函数来增强“读数”记录,尽管该函数暂时还什么都没做。
##### 客户端3...
```
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);
```
然后我运用搬移函数(198)把`calculateBaseCharge`函数搬移到增强过程中:
```
function enrichReading(original) {
const result = _.cloneDeep(original);
result.baseCharge = calculateBaseCharge(result);
return result;
}
```
在变换函数内部,我乐得直接修改结果对象,而不是每次都复制一个新对象。我喜欢不可变的数据,但在大部分编程语言中,保持数据完全不可变很困难。在程序模块的边界处,我做好了心理准备,多花些精力来支持不可变性。但在较小的范围内,我可以接受可变的数据。另外,我把这里用到的变量命名为`aReading`,表示它是一个累积变量(accumulating variable)。这样当我把更多的逻辑搬移到变换函数`enrichReading`中时,这个变量名也仍然适用。
修改客户端代码,令其改用增强后的字段:
##### 客户端3...
```
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;
```
当所有调用`calculateBaseCharge`的地方都修改完成后,就可以把这个函数内嵌到`enrichReading`函数中,从而更清楚地表明态度:如果需要“计算基本费用”的逻辑,请使用增强后的记录。
在这里要当心一个陷阱:在编写`enrichReading`函数时,我让它返回了增强后的读数记录,这背后隐含的意思是原始的读数记录不会被修改。所以我最好为此加个测试。
```
it('check reading unchanged', function() {
const baseReading = {customer: "ivan", quantity: 15, month: 5, year: 2017};
const oracle = _.cloneDeep(baseReading);
enrichReading(baseReading);
assert.deepEqual(baseReading, oracle);
});
```
现在我可以修改客户端1的代码,让它也使用这个新添的字段。
##### 客户端1...
```
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const baseCharge = aReading.baseCharge;
```
此时可以考虑用内联变量(123)去掉`baseCharge`变量。
现在我转头去看“计算应税费用”的逻辑。第一步是把变换函数用起来:
```
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
```
基本费用的计算逻辑马上就可以改用变换得到的新字段代替。如果计算逻辑比较复杂,我可以先运用提炼函数(106)。不过这里的情况足够简单,一步到位修改过来就行。
```
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = aReading.baseCharge;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
```
执行测试之后,我就用内联变量(123)去掉`base`变量:
```
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));
```
然后把计算逻辑搬移到变换函数中:
```
function enrichReading(original) {
const result = _.cloneDeep(original);
result.baseCharge = calculateBaseCharge(result);
result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
return result;
}
```
修改使用方代码,让它使用新添的字段。
```
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = aReading.taxableCharge;
```
测试。现在我可以再次用内联变量(123)把`taxableCharge`变量也去掉。
增强后的读数记录有一个大问题:如果某个客户端修改了一项数据的值,会发生什么?比如说,如果某处代码修改了`quantity`字段的值,就会导致数据不一致。在JavaScript中,避免这种情况最好的办法是不要使用本重构手法,改用函数组合成类(144)。如果编程语言支持不可变的数据结构,那么就没有这个问题了,那样的语言中会更常用到变换。但即便编程语言不支持数据结构不可变,如果数据是在只读的上下文中被使用(例如在网页上显示派生数据),还是可以使用变换。
- 第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)
- 参考文献
- 重构列表
- 坏味道与重构手法速查表