0. 本章内容导图
1. 重构手法
1.1 提炼函数
概要:
有一段代码可以被组织在一起并独立出来。
将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。
动机:
a. 函数保持较小粒度,容易被复用
b. 使得高层函数读起来就像是一系列注释
c. 细粒度的函数更容易被覆写
示例:
重构前:
void printOwing(double amount) {
printBanner();
// print details
System.out.println("name: " + name);
System.out.println("amount: " + amount);
}
重构后:
void printOwing(double amount) {
printBanner();
printDetails(amount);
}
void printDetails(double amount) {
System.out.println("name: " + name);
System.out.println("amount: " + amount);
}
总结:
细粒度的函数容易理解,也容易保持函数功能单一,如果函数名可以对函数功能自注释,我们的视角就能更关注于逻辑流程,而不囿于具体实现。在大的函数内部,如遇有注释的地方,可以考虑将这部分代码提取出来。另外,从抽象层级来看,一个函数内的代码都处于同一抽象层级,何尝不是更具美感。
提炼函数的时候,欲提取的代码段中可能使用了一些局部变量,这会给函数提取增加很多困难,需谨慎处理,小步推进。
1.2 内联函数
概要:
一个函数的本体与名称同样清楚易懂。
在函数调用点插入函数本体,然后移除该函数。
动机:
a. 去除间接性带来的思维跳跃
b. 高层函数组织不合理,内联后重新组织提炼新的函数
c. 去除过多的间接性委托
示例:
重构前:
int getRating() {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() {
return numberOfLateDeliveries > 5;
}
重构后:
int getRating() {
return (numberOfLateDeliveries > 5) ? 2 : 1;
}
总结:
不必要的间接性会阻碍理解的连贯性。
代码在经多次修改后,组织可能已经不甚合理,将这些函数内联后重新组织提炼,可以使之继续散发芬芳。
1.3 内联临时变量
概要:
有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。
将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。
动机:
a. 此临时变量给其他重构带来困难,其本身也没什么存在价值
b. 给临时变量赋值的表达式在其他函数中也有使用,可提炼为一个查询函数
示例:
重构前:
double basePrice = anOrder.basePrice();
return (basePrice > 1000);
重构后:
return (anOrder.basePrice() > 1000);
总结:
此方法可以去除一些不必要的临时变量,或者作为其他重构手法中的一环。
1.4 以查询取代临时变量
概要:
程序中以一个临时变量保存某一表达式的运算结果。
将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数也可以被其他函数调用。
动机:
a. 类中的其他部分也需要访问临时变量保存的信息
b. 提炼函数时受临时变量困扰,为消除临时变量
示例:
重构前:
double basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
重构后:
if(basePrice() > 1000) {
return basePrice() * 0.95;
} else {
return basePrice() * 0.98;
}
double basePrice() {
return quantity * itemPrice;
}
1.5 引入解释性变量
概要:
你有一个复杂的表达式。
将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
动机:
a. 简化条件表达式
示例:
重构前:
if ( (platform.toUpperCase().indexOf("MAC") > -1)
&& (browser.toUpperCase().indexOf("IE") > -1)
&& wasInitialized() && resize > 0 ) {
// do something
}
重构后:
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if ( isMacOs && isIEBrowser && wasInitialized() && wasResized ) {
// do something
}
总结:
引入解释性变量常用来简化条件表达式,也可以提取函数来封装复杂的表达式,用函数名来解释复杂表达式的功能。
1.6 分解临时变量
概要:
程序中有某个临时变量被赋值超过一次,它既不是循坏变量,也不被用于收集计算结果。
针对每次赋值,创造一个独立、对应的临时变量。
动机:
a. 消除变量被多次赋值造成的理解上的胡乱
示例:
重构前:
double tmp = 2 * (height + width);
System.out.println(tmp);
tmp = height * width;
System.out.println(tmp);
重构后:
final double perimeter = 2 * (height + width);
System.out.println(perimeter);
final double area = height * width;
System.out.println(area);
总结:
单一职责不仅只针对类而言,函数、临时变量都应该保持单一职责。对于临时变量而言,前后不一致的职责势必会造成理解混乱,也容易产生bug。
1.7 移除对参数的赋值
概要:
代码对一个参数进行赋值。
以一个临时变量取代该参数的位置。
动机:
a. 消除值传递、引用传递在语言间易造成的误解
b. 让参数只代表“传递进来的东西”,保持函数本体内的代码清晰
示例:
重构前:
int discount(int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) {
inputVal -= 2;
}
}
重构后:
int discount(int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) {
result -= 2;
}
}
总结:
要理解按值传递和按引用传递的含义。Java中都是按值传递的,当参数传递的是对象时,实际上是多了一个临时变量指向此对象所在的内存区域,由于都指向同一块内存区域,因此也是可以在“被传入对象”上进行操作的。
当传入的参数是集合对象时,要尤其谨慎。
1.8 以函数对象取代函数
概要:
有一个大型函数,里面使用了很多局部变量,使得你无法采用提炼函数的手法对其进行重构。
可将这个函数放进一个单独对象中,这样一来,局部变量就成了对象的字段,就可以在同一个对象中将这个大型函数分解为多个小型函数。
动机:
a. 消除大型函数中因局部变量太多而无法分解成多个小函数的问题
示例:
重构前:
class Order {
// field
...
//类中的大型方法,局部变量过多难以提炼函数
double price() {
double var1;
double var2;
...
double varN;
// long computation
...
}
// other method
...
}
重构后:
// 新建一个类,将上述大型方法放入此类,局部变量变为类的字段
class PriceCalculator {
private double var1;
private double var2;
...
private double varN;
public double price() {
calculate1();
calculate2();
// call other method
...
}
private void calculate1() {
// some operation
}
private void calculate2() {
// some operation
}
// other small method
...
}
class Order {
...
private PriceCalculator calculator;
public double price() {
calculator.price();
}
...
}
总结:
提炼函数的困难在于对局部变量的处理,当大型函数里的局部变量太多,由于作用域的关系,使得根本无法提炼函数对其进行分解,为这个大型函数新建一个类,将函数局部变量变为类的字段,就可以轻松拆分这个大型函数了。
1.9 替换算法
概要:
你想要把某个算法替换为另一个更清晰的算法。
将函数本体替换为另一个算法。
动机:
a. 为了采用更清晰、更简单或性能更好的算法
示例:
重构前:
String findPerson(String[] people) {
for(int i = 0; i < people.length; i++) {
if(people[i].equals("Don")) {
return "Don";
}
if(people[i].equals("John")) {
return "John";
}
if(people[i].equals("Kent")) {
return "Kent";
}
}
return "";
}
重构后:
String findPerson(String[] people) {
List candidates = Arrays.asList(new String[] {"Don", "John", "Kent"});
for(int i = 0; i < people.length; i++) {
if(candidates.contains(people[i])) {
return people[i];
}
}
return "";
}
总结:
对问题的了解越深,可以想到的更好的解决方案就会越多,或者需要对原有的算法进行性能优化,此时你就需要改变原先的算法。替换一个巨大而复杂的算法是非常困难的,在替换之前,你可能需要先将它分解成较为简单的小型函数。