重构-改善既有代码的设计(五)
重新组织数据
拆分变量(Split Variable)
示例
1
2
3
4let temp = 2 * (height + width);
console.log(temp);
temp = height * width;
console.log(temp);重构为
1
2
3
4let perimeter = 2 * (height + width);
console.log(perimeter);
let area = height * width;
console.log(area);动机
如果变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。同一个变量承担两件不同的事情,会令代码阅读者糊涂。
做法
- 在待分解变量的声明及其第一次被赋值处,修改其名称。
- 如果可能的话,将新的变量声明为不可修改。
- 以该变量的第二次赋值动作为界,修改此前对该变量的所有引用,让它们引用新变量。
- 测试。
- 重复上述过程。每次都在声明处对变量改名,并修改下次赋值之前的引用,直至到达最后一次赋值。
字段改名(Rename Field)
示例
1
2
3class Organization{
get name() { ... }
}重构为
1
2
3class Organization{
get title() { ... }
}动机
命名很重要,对于程序中广泛使用的记录结构,其中字段的命名格外重要。数据结构对于帮助阅读者理解特别重要。
做法
- 如果记录的作用域较小,可以直接修改所有该字段的代码,然后测试。后面的步骤就都不需要了。
- 如果记录还未封装,请先使用封装记录。
- 在对象内部私有字段改名,对应调整内部访问该字段的函数。
- 测试。
- 如果构造函数的参数使用了旧的字段名,运用改变函数声明将其改名。
- 运用函数改名给访问函数改名。
以查询取代派生变量(Replace Derived Variablewith Query)
示例
1
2
3
4
5
6get discountedTotal() {return this._discountedTotal;}
set discount(aNumber){
const old = this._discount;
this._discount = aNumber;
this._discountedTotal += old - aNumber;
}重构为
1
2get discountedTotal() {return this._baseTotal - this._discount;}
set discount() {this._discount = aNumber;}动机
有些变量其实可以很容易地随时计算出来。如果能去掉这些变量,也算朝着消除可变性的方向迈出了一大步。计算常能更清晰地表达数据的含义,而且也避免了“源数据修改时忘了更新派生变量”的错误
做法
- 识别出所有对变量做更新的地方。如有必要,用拆分变量分隔各个更新点。
- 新建一个函数,用于计算该变量的值。
- 用引入断言断言该变量和计算函数始终给出同样的值。
- 测试。
- 修改读取该变量的代码,令其调用新建的函数。
- 测试。
- 用移除死代码去掉变量的声明和赋值。
将引用对象改为值对象(Change Reference to Value)
示例
1
let customer = new Customer(customerData);重构为
1
let customer = customerRepository.get(customerData.id);动机
把值对象改为引用对象会带来一个结果:对于一个客观实体,只有一个代表它的对象。这通常意味着我会需要某种形式的仓库,在仓库中可以找到所有这些实体对象,只为每个实体创建一次对象,以后始终从仓库中获取该对象。
做法
- 为相关对象创建一个仓库(如果还没有这样一个仓库的话)。
- 确保构造函数有办法找到关联对象的正确实例。
- 修改宿主对象的构造函数,令其从仓库中获取关联对象。每次修改后执行测试。
简化条件逻辑
分解条件表达式(Decompose Conditional)
示例
1
2
3
4if(!aData.isBefore(plan.summerStart) && !aData.isAfter(plan.summerEnd))
charge = quantity * plan.summerRate;
else
charge = quantity * plan.regularRate + plan.regularServiceCharge;重构为
1
2
3
4if(summer())
charge = summerCharge();
else
charge = regularCgharge();动机
对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。
做法
- 对条件判断和每个条件分支分别运用提炼函数手法。
合并条件表达式(Consolidate Conditional Expression)
示例
1
2
3if(anEmployee.seniority < 2) return 0;
if(anEmployee.mothsDisabled > 12) return 0;
if(anEmployee.isPartTime) return 0;重构为
1
2
3
4
5if(isNotEligibleForDisability()) return 0;
function isNotEligibleForDisability(){
return ((anEmployee.seniority < 2) || (anEmployee.mothsDisabled > 12) || (anEmployee.isPartTime));
}动机
- 合并后的条件代码会表述“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这一次检查的用意更清晰。
- 将检查条件提炼成一个独立的函数对于厘清代码意义非常有用,因为它把描述“做什么”的语句换成了“为什么这样做”。
做法
- 确定这些条件表达式都没有副作用。
- 使用适当的逻辑运算符,将两个相关条件表达式合并为一个。
- 测试。
- 重复前面的合并过程,直到所有相关的条件表达式都合并到一起。
- 可以考虑对合并后的条件表达式实施提炼函数。
以卫语句取代嵌套条件表达式(Replace NestedConditional with Cuard Clauses)
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function getPayAmount(){
let result;
if(isDead)
result = deadAmount;
else{
if(isSeparated)
result = separatedAmount();
else{
if(isRetired)
result = retiredAmount();
else
result = normalPayAmount();
}
}
return result;
}重构为
1
2
3
4
5
6function getPayAmount(){
if(isDead) return deadAmount();
if(isDead) return separatedAmount();
if(isDead) return retiredAmount();
return normalPayAmount();
}动机
以卫语句取代嵌套条件表达式的精髓就是:给某一条分支以特别的重视。如果使用 if-then-else 结构,你对 if 分支和 else 分支的重视是同等的。这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句就不同了,它告诉阅读者:“这种情况不是本函数的核心逻辑所关心的,如果它真发生了,请做一些必要的整理工作,然后退出。”
做法
- 选中最外层需要被替换的条件逻辑,将其替换成卫语句。
- 测试。
- 有需要的话,重复上述步骤。
- 如果所有卫语句都引发同样的结果,可以使用合并条件表达式合并之。
以多态取代条件表达式(Replace Conditional with Polymorphism)
示例
1
2
3
4
5
6
7
8
9
10switch (bird.type){
case 'EuropeanSwallow':
return "average";
case 'AfricanSwallow':
return (bird.numberOfCoconuts > 2) ? "tired" : "average";
case 'NorwegianBlueParrot':
return (bird.voltage > 100) ? "scorched" : "beautiful";
default:
return "unknown";
}重构为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class EuropeanSwallow{
get plumage(){
return "average";
}
}
class AfricanSwallow{
get plumage(){
return (bird.numberOfCoconuts > 2) ? "tired" : "average";
}
}
class NorwegianBlueParrot{
get plumage(){
return (bird.voltage > 100) ? "scorched" : "beautiful";
}
}动机
我发现可以将条件逻辑拆分到不同的场景,从而拆解复杂的条件逻辑。这种拆分有时用条件逻辑本身的结构就足以表达,但使用类和多态能把逻辑的拆分表述得更清晰。
做法
- 如果现有的类尚不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当的对象实例。
- 在调用方代码中使用工厂函数获得对象实例。
- 将带有条件逻辑的函数移到超类中。
- 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新函数中,并对它进行适当调整。
- 重复上述过程,处理其他条件分支。
- 在超类函数中保留默认情况的逻辑。或者,如果超类应该是抽象的,就把该函数声明为 abstract,或在其中直接抛出异常,表明计算责任都在子类中。
引入特例(Introduce Special Case)
示例
1
if(aCustomer === "unknown") customerName = "occupant";重构为
1
2
3class UnknownCustomer{
get name() {return "occupant";}
}动机
创建一个特例元素,用以表达对这种特例的共用行为的处理,这样我就可以用一个函数调用取代大部分特例检查逻辑。
做法
- 给重构目标添加检查特例的属性,令其返回 false。
- 创建一个特例对象,其中只有检查特例的属性,返回 true。
- 对“与特例值做比对”的代码运用提炼函数,确保所有客户端都是用这个新函数,而不再直接做特例的比对。
- 将新的特例对象引入代码中,可以从函数调用中返回,也可以在变换函数中生成。
- 修改特例比对函数的主体,在其中直接使用检查特例的属性。
- 测试。
- 使用函数组合成类或函数组合成变换,把通用的特例处理逻辑都搬移到新建的特例对象中。
- 对特例比对函数使用内联函数,将其内联到仍然需要的地方。
引入断言(Introduce Assertion)
示例
1
if(this.discountRate) base = base - (this.discountRate * base);重构为
1
2
3
4assert(this.discountRate >= 0);
if(this.discountRate)
base = base - (this.discountRate * base);
}动机
断言是一种很有价值的交流形式——它们告诉阅读者,程序在执行到这一点时,对当前状态做了何种假设。另外断言对调试也很有帮助。
做法
- 如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况。