《重构 改善代码既有设计》第六章内容读书笔记
重新组织函数往往面对的问题是过长的函数,使用的手法是提炼函数
难点1:局部变量
- 查询取代局部变量
- 分解临时变量
- 以函数对象取代函数
难点2:参数
- 移除对参数的赋值
当提炼函数
将长函数变得足够清晰后,便可使用替换算法
优化其中的算法,或者使用内联函数
回退掉不必要的函数提炼
提炼函数
有一段代码可以被组织在一起并独立出来:将这段代码放进一个独立函数中,并让函数名称解释该函数的用途
动机
用于过长的函数或一段需要注释才能让人理解的代码
目的是使函数能够自注释,强化代码清晰度。重构的结果就是函数越来越多,每个独立代码段都由函数名自注释。
做法
对局部变量的处理:
- 被提炼的函数仅读取局部变量 / 通过对象或引用传递的方式修改局部变量,直接将局部变量作为参数传入即可
- 被提炼函数需要对局部变量重新赋值
- 变量仅在新函数中使用:将声明移入
- 变量在原函数中有使用:用参数传入上文,返回值传给下文
- 复杂情况:需要返回的变量不止一个,需要挑选另一块代码重新提炼
- 以查询取代局部变量、以函数对象取代函数
范例
重构前:
void printOwing(double previousAmount) {
Enumeration e = _oeders.elements();
double outstanding = previousAmount * 1.2;
printBanner();
// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElements();
outstanding += each.getAmount();
}
printDetails(outstanding); // 参数传入
}
重构后:
void printOwing(double previousAmount) {
printBanner();
double outstanding = getOutstanding(previousAmount * 1.2);
printDetails(outstanding);
}
double getOutstanding(double initialValue) { // 参数传入上文,返回值传出下文
double result = initialValue;
Enumeration e = _oeders.elements(); // 声明移入
while (e.hasMoreElements()) {
Order each = (Order) e.nextElements();
result += each.getAmount();
}
return result;
}
内联函数
一个函数的本体与名称同样清楚易懂:在函数调用点插入函数本体,然后移除该函数。
动机
- 函数的本体与名称同样清楚易懂,就没有必要引入一个间接层
- 一群组织不甚合理的函数,可以先把它们内联到一个大函数中,再进行
提炼函数
或以函数对象取代函数
- 使用了太多间接层,系统中几乎所有函数都只是一个简单委托。间接层有其价值,但不是所有的间接层都有价值。
内联临时变量
你有一个临时变量,只被一个简单表达式复制一次,而它妨碍了其他重构手法:将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。
将所有对该变量的引用动作替换为对它赋值的那个表达式本身。常作为以查询取代临时变量
的一个步骤
重构前:
double basePrice = anOrder.basePrice();
return (basePrice > 1000);
重构后:
return (anOrder.basePrice() > 1000);
以查询取代临时变量
你的程序以一个临时变量保存某一表达式的运算结果:将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可被其他函数使用。
动机
临时变量的问题在于,它们是暂时的,而且只能在所属函数内使用,会驱使你写出更长的函数。如果把临时变量替换为一个查询,那么同一个类中的所有函数都可以获得这一信息(复用)
往往在提炼函数
前运用。比较棘手的情况是,临时变量被赋值了多次,或者赋值方式受其他条件影响。此时可能需要先运用分解临时变量
或查询和修改函数分离
使情况变得简单一些,然后再替换临时变量。
范例
重构前:
double getPrice() {
int basePrice = _quantity * _itemPrice;
double discountFactor;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
重构后:
double getPrice() {
return basePrice() * discountFactor();
}
private int basePrice() {
return _quantity * _itemPrice;
}
private double discountFactor() {
if (basePrice() > 1000) return 0.95;
return 0.98;
}
引入解释性变量
你有一个复杂的表达式:将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式的用途。
动机
表达式可能非常复杂而难以阅读,这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。
- 条件逻辑:使用临时变量将每个条件子句提炼出来
- 较长算法:运用临时变量来解释每一步运算的意义
范例
重构前:
double price() {
// base price - quantity discount + shipping
return _quantity * _itemPrice -
Math.max(0, _quantity - 500) * _itemPrice * 0.05 +
Math.min(_quantity * _itemPrice * 0.1. 100.0);
}
使用引入解释性变量
double price() {
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.max(0, _quantity - 500) * _itemPrice * 0.05;
final double shipping = Math.min(basePrice * 0.1, 100.0);
return basePrice - quantityDiscount + shipping;
}
使用提炼方法
double price() {
return basePrice() - quantityDiscount() + shipping();
}
private double basePrice() {
return _quantity * _itemPrice;
}
private double quantityDiscount() {
return Math.max(0, _quantity - 500) * _itemPrice * 0.05;
}
private double shipping() {
return Math.min(basePrice() * 0.1, 100.0);
}
两者都是为了实现代码自注释,提炼函数
的结果更清晰,但是有时工作量会很大,这时候可以考虑使用引入解释性变量
。
分解临时变量
你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不用于手机计算结果:针对每次赋值,创造一个独立、对应的临时变量。
动机
除了循环变量和用于收集结果的临时变量(补充:go语言err返回值,也经常被多次使用且不会造成糊涂),其他临时变量往往用于保存一段冗长代码的运算结果。这些临时变量只应该被赋值一次。承担单一的责任。同一临时变量城改两件不同的事情,会使阅读者糊涂/
收集结果的变量,一个重要特征是之后的赋值语句为 [i = i + X] 形式
移除对参数的赋值
代码对一个参数进行赋值:以一个临时变量取代该参数的位置
降低了代码清晰度,混用了按值传递和按引用传递两种方式。有“出参数”的语言可例外,但是还是应减少出参数,或者对出参数用宏明确标识。
可以为参数加上final,让编译器帮助检查是否对参数做了修改。
以函数对象取代函数
你有一个大型函数,其中对局部变量的使用是你无法参与提炼函数
:将这个函数放进一个单独对象中,如此一来局部变量就成了对象中的字段。然后你可以在同一个对象中将这个大型函数分解为多个小函数。
将函数上下文提取到对象中
动机
局部变量的存在增加了函数分解的难度。以函数对象取代函数
会将所有局部变量都变成函数对象的字段。然后就可以对这个函数对象使用提炼函数
创造新函数,将大型函数变小。这时候可以任意分解这个大型函数而不必传递任何参数,因为所有的局部变量都变成了函数对象的字段。
做法
- 建立一个新类,根据待处理函数的用途为这个类命名
- 在新类中建立一个final字段,用以保存原来大型函数归属的对象,称“源对象”;同时,针对原函数的每个临时变量和每个参数,在新类中建立一个对应的i段保存
- 新类中建立一个构造函数,接收源对象以及原函数的所有参数作为参数
- 新类中建立一个compute()函数
- 将原函数的代码复制到compute()函数中。在需要调用源对象方法的地方,修改为使用源对象字段调用
- 编译
- 旧函数的函数本体替换为创建一个函数对象,并调用compute()方法
替换算法
你想要把某个算法替换为另一个更清晰的算法:将函数本体替换为i另一个算法