1、提炼函数(Extract Method)
(1)若有一段代码可以被组织在一起并独立出来,就把这段代码放进一个独立的函数,并让函数名称解释该函数的用途
(2)提炼函数的契机:过长函数、需要注释才能让人理解
(3)注意函数名称和函数本体之间的语义距离
(4)创造一个新函数,要根据它“做什么”来命名,而不是以它“怎么做”命名
(5)仔细检查提炼出来的代码,看看其中是否引用了“作用域局限于原函数”的变量(包括局部变量和源函数参数)
(6)检查被提炼的代码块,是否有任何局部变量的值被它改变
(7)如果被提炼代码块只是读取局部变量,并不修改它们,这种情况就可以简单的将它们当作参数传给目标函数
(8)被提炼出来的代码块组成新的目标函数,如果被提炼代码块对局部变量赋值,这种情况相对复杂,可以分为两种情况:
- 如果变量仅仅在被提炼代码块中使用,可以将这个临时变量的声明移到被提炼代码块中
- 如果被提炼代码块之外也使用了这个变量,这又分为两种情况
- 如果这个变量在被提炼代码块之后未再被使用,直接在提炼出来的代码块中修改就可以了
- 如果被提炼代码块之后的代码还使用了这个变量,就需要让目标函数返回该变量改变后的值
(9)如果需要返回的变量不止一个怎么办?
答:挑选另一块代码来提炼,安排多个函数,用以返回多个值
2、内联函数(Inline Method)
(1)如果提炼的函数内部代码和函数名称同样清晰易读,就应该去掉这个函数,减少非必要的间接性
(2)使用内联手法,找出有用的间接层,同时消除那些无用的间接层
- 检查函数,确定它不具有多态性,如果子类继承了这个函数,就不要将此函数内联
- 找出这个函数的所有被调用点
- 将这个函数的所有被调用点都替换成函数本体
- 编译测试
- 删除该函数的定义
3、内联临时变量(Inline Temp)
(1)如果一个临时变量只被简单表达式赋值一次,就将该变量替换成表达式本身
(2)如果某个临时变量妨碍了其他重构手法,例如Extract Method ,就应该将这个变量内联化
- 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用
- 如果这个变量没有被声明为final,就将它声明为final,然后编译,检查该临时变量是否只被赋值一次
- 找到该临时变量的所有引用点,将它们替换成相应的赋值表达式
- 每次修改后,编译并测试
- 修改完所有引用点之后,删除该临时变量的声明和赋值语句
- 编译,测试
4、以查询取代临时变量(Replace Temp with Query)
(1)由于临时变量是暂时的,只能在所属函数内使用,可以将临时变量对应的表达式提炼到独立函数,这样就可以被其他函数使用
(2)局部变量会使得代码难以提炼,应该尽可能把它们替换成查询
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;
}else{
return 0.98;
}
}
5、引入解释性变量(Introduce Explaining Variable)
(1)如果表达式过于复杂,应该将该复杂表达式或者一部分放进一个临时变量,以此变量的名称来解释表达式的用途
(2)常见应用场景:
- 在条件逻辑中,重构将每个条件子句提炼出来,以一个良好命名的临时变量解释对应的条件子句
- 在较长算法中,可以运用临时变量来解释每一步运算的意义
(3)注意临时变量有一定局限性,只在它所处的那个函数中才有意义
(4)有时候当局部变量使得提取函数难以进行时,就会引入解释性变量的方式
6、分解临时变量(Split Temporary Variable)
(1)对于某些被赋值超过一次的临时变量,应当替换(分解)为多个临时变量,每个变量只承担一个责任
(2)特殊情况:循环变量、结果收集变量
范例:计算苏格兰布丁运动的距离
double getDistanceTravelled(int time){
double result;
double acc = primaryForce / mass;
int primaryTime = Math.min(time, delay);
result = 0.5 * acc * primaryTime * primaryTime;
int secondaryTime = time - delay;
if(secondaryTime > 0){
double primaryVel = acc * delay;
acc = (primaryForce + secondaryForce) / mass;
result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
}
return result;
}
说明:上述代码中acc变量被赋值两次,acc变量有两个责任:第一是保存第一个力造成的初始加速度;
第二是保存两个力共同造成的加速度。
----------------------------------------------------------------------------------
修改后的代码如下:
double getDistanceTravelled(int time){
double result;
final double primaryAcc = primaryForce / mass;
int primaryTime = Math.min(time, delay);
result = 0.5 * primaryAcc * primaryTime * primaryTime;
int secondaryTime = time - delay;
if(secondaryTime > 0){
double primaryVel = primaryAcc * delay;
final double secondaryAcc = (primaryForce + secondaryForce) / mass;
result += primaryVel * secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime;
}
return result;
}
7、移除对参数的赋值(Remove Assignments to Parameters)
(1)不要对函数的入参进行赋值,降低了代码的清晰度,而且混用了按值传递和按引用传递这两种参数传递方式(Java只采用按值传递)
int discount(int inputVal, int quantity, int yearToDate){
if(inputVal > 50){
inputVal -= 2;
}
if(quantity > 100){
inputVal -= 1;
}
if(yearToDate > 10000){
inputVal -= 4;
}
return inputVal;
}
---------------------------------------------------------------------------
修改后的代码
int discount(int inputVal, int quantity, int yearToDate){
int result = inputVal;
if(inputVal > 50){
result -= 2;
}
if(quantity > 100){
result -= 1;
}
if(yearToDate > 10000){
result -= 4;
}
return result;
}
8、以函数对象取代函数(Replace Method with Method Object)
(1)如果因为局部变量的缘故,对一个大型函数无法采用提炼函数(Extract Method),可以将这个函数放进一个单独对象,这样的话,局部变量就成了对象内的字段
(2)由于局部变量变成了字段,所以可以分解这个大型函数,不必传递参数
public int gamma(int inputValue, int quantity, int yearToDate) {
int importantValue1 = inputValue * quantity + delta();
int importantValue2 = inputValue * yearToDate + 100;
if ((yearToDate - importantValue1) > 100) {
importantValue2 -= 20;
}
int importantValue3 = importantValue2 * 7;
return importantValue3 - 2 * importantValue1;
}
-----------------------------------------------------------------------
修改之后,如下
public class Gamma {
private final Test test;
private int inputValue;
private int quantity;
private int yearToDate;
private int importantValue1;
private int importantValue2;
private int importantValue3;
public Gamma(Test test, int inputValue, int quantity, int yearToDate) {
super();
this.test = test;
this.inputValue = inputValue;
this.quantity = quantity;
this.yearToDate = yearToDate;
}
public int compute() {
importantValue1 = inputValue * quantity + test.delta();
importantValue2 = inputValue * yearToDate + 100;
importantThing();
importantValue3 = importantValue2 * 7;
return importantValue3 - 2 * importantValue1;
}
private void importantThing(){
if ((yearToDate - importantValue1) > 100) {
importantValue2 -= 20;
}
}
}
public int gamma(int inputValue, int quantity, int yearToDate) {
return new Gamma(this,inputValue,quantity,yearToDate).compute();
}
9、替换算法(Substitute Algorithm)
(1)把某个算法替换成另一个更清晰的算法
- 准备好另一个(替换用)算法,让它编译通过
- 针对现有测试,执行上述的新算法,如果结果与原本结果相同,重构结束
尾注
- 上述的总结与思考是基于对《重构—改善既有代码的设计》这本书的精读与演绎
- 更多及时干货,请关注微信公众号:JAVA万维猿圈