拆分变量(Split Variable)
曾用名:移除对参数的赋值(Remove Assignments to Parameters)
曾用名:分解临时变量(Split Temp)
let temp = 2 * (height + width);
console.log(temp);
temp = height * width;
console.log(temp);
const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width;
console.log(area);
动机
变量有各种不同的用途,其中某些用途会很自然地导致临时变量被多次赋值,“循环变量”和“结果收集变量”就是两个典型例子。
除了这两种情况,还有很多变量用于保存一段冗长代码的运算结果,以便稍后使用。这种变量应该只被赋值一次。如果它们被赋值超过一次,就意味它们在函数中承担了一个以上的责任。如果变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。
做法
- 在待分解变量的声明及其第一次被赋值处,修改其名称。
- 如果可能的话,将新的变量声明为不可修改。
- 以该变量的第二次赋值动作为界,修改此前对该变量的所有引用,让它们引用新变量。
- 测试。
- 重复上述过程。每次都在声明处对变量改名,并修改下次赋值之前的引用,直至到达最后一处赋值。
字段改名(Rename Field)
class Organization {
get name() {...}
}
class Organization {
get title() {...}
}
动机
命名很重要,对于程序中广泛使用的记录结构,其中字段的命名格外重要。
数据结构是理解程序行为的关键。记录结构中的字段可能需要改名,类的字段也一样。在类的使用者看来,取值和设值函数就等于是字段。对这些函数的改名,跟裸记录结构的字段改名一样重要。
做法
- 如果记录的作用域较小,可以直接修改所有该字段的代码,然后测试。后面的步骤就都不需要了。
- 如果记录还未封装,请先使用封装记录(162)。
- 在对象内部对私有字段改名,对应调整内部访问该字段的函数。
- 测试。
- 如果构造函数的参数用了旧的字段名,运用改变函数声明(124)将其改名。
- 运用函数改名(124)给访问函数改名。
以查询取代派生变量(Replace Derived Variable with Query)
get discountedTotal(){return this._discountedTotal;}
set discount(aNumber){
const old = this._discount;
this._discount=aNumber;
this._discountedTotal += old - aNumber
}
get discountedTotal(){return this_baseTotal - this._discount;}
set discount(aNumber) {this._discount=aNumber;}
动机
可变数据是软件中最大的错误源头之一。对数据的修改常常导致代码的各个部分以丑陋的形式互相耦合:在一处修改数据,却在另一处造成难以发现的破坏。完全去掉可变数据并不现实,但还是强烈建议:尽量把可变数据的作用域限制在最小范围。
有些变量其实可以很容易地随时计算出来,可以去掉这些变量。计算常能更清晰地表达数据的含义,而且也避免了“源数据修改时忘了更新派生变量”的错误。
有一种合理的例外情况:如果计算的源数据是不可变的,并且我们可以强制要求计算的结果也是不可变的,那么就不必重构消除计算得到的派生变量。
两种不同的编程风格:一种是对象风格,把一系列计算得出的属性包装在数据结构中;另一种是函数风格,将一个数据结构变换为另一个数据结构。
做法
-
识别出所有对变量做更新的地方。如有必要,用拆分变量(240)分割各个更新点。
-
新建一个函数,用于计算该变量的值。
-
用引入断言(302)断言该变量和计算函数始终给出同样的值。
如有必要,用封装变量(132)将这个断言封装起来。
-
测试。
-
修改读取该变量的代码,令其调用新建的函数。
-
测试。
-
用移除死代码(237)去掉变量的声明和赋值。
将引用对象改为值对象(Change Reference to Value)
反向重构:将值对象改为引用对象(256)
class Product{
applyDiscount(arg) {this._price.amount -= arg;}
// ...
}
class Product {
applyDiscount(arg) {
this._price = new Money(this._price.amount - arg, this._price.currency);
}
// ...
}
动机
在把一个对象(或数据结构)嵌入另一个对象时,位于内部的这个对象可以被视为引用对象,也可以被视为值对象。两者最明显的差异在于如何更新内部对象的属性:如果将内部对象视为引用对象,在更新其属性时,保留原对象不动,更新内部对象的属性;如果将其视为值对象,替换整个内部对象,新换上的对象会有想要的属性值。
如果想在几个对象之间共享一个对象,以便几个对象都能看见对共享对象的修改,那么这个共享的对象就应该是引用。
一般说来,不可变的数据结构处理起来更容易。可以放心地把不可变的数据值传给程序的其他部分,而不必担心对象中包装的数据被偷偷修改。
做法
- 检查重构目标是否为不可变对象,或者是否可修改为不可变对象。
- 用移除设值函数(331)逐一去掉所有设值函数。
- 提供一个基于值的相等性判断函数,在其中使用值对象的字段。
将值对象改为引用对象(Change Value to Reference)
反向重构:将引用对象改为值对象(252)
let customer = new Customer(customerData);
let customer = customerRepository.get(customerData.id);
动机
一个数据结构中可能包含多个记录,而这些记录都关联到同一个逻辑数据结构。
如果共享的数据需要更新,将其复制多份的做法就会遇到巨大的困难。漏掉一个副本没有更新,就会遭遇麻烦的数据不一致。
把值对象改为引用对象会带来一个结果:对于一个客观实体,只有一个代表它的对象。这通常意味着会需要某种形式的仓库,在仓库中可以找到所有这些实体对象。只为每个实体创建一次对象,以后始终从仓库中获取该对象。
做法
- 为相关对象创建一个仓库(如果还没有这样一个仓库的话)。
- 确保构造函数有办法找到关联对象的正确实例。
- 修改宿主对象的构造函数,令其从仓库中获取关联对象。每次修改后执行测试。