第6章 重新组织函数
简介:
本章重构手法中,很大一部分是对函数进行整理,使之更恰当地包装代码。
重构的很多问题其实都来自“过长函数(Long Methods)”,要重构它是因为它往往包含过多的信息,这些信息又被他错综复杂的逻辑掩盖,不易鉴别。
解决过长函数的重构方法,其中一个是“提炼函数(Extract Method)”,它把一段代码从源函数中提炼出来,放入一个单独的函数中;或者就是使用“内联函数(Inline Method)”,它将一个函数调用替换为函数本体,这样替换是因为提炼的函数没有做任何实质性的事情。
提炼函数难点是:处理局部变量,如临时变量,参数等。
提炼函数处理函数替换临时变量的一个方法:“以查询取代临时变量”,处理参数的方法:“移除对参数的赋值”。
Extract Method(提炼函数)
1.概念:你有一段代码可以组织在一起并独立出来,将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。
2.动机:
(1)如果每个函数的粒度都很小,那么函数被复用的机会更大。
(2)其次,这会使高层函数读起来像注释(小型函数要很好地命名)。
(3)最后,如果函数都是细粒度,那么函数的覆写也会更容易些。
(ps:函数命名长度不是问题,重要的是函数名称和函数本体之间的语义距离。)
3.做法:
(1)创造一个新函数,根据函数意图对它命名(即使你提炼的代码很简单,如只是一条消息或一个函数调用,只要新的函数名称能够更好昭示代码意图,就应该提炼)。
(2)将提炼出的代码从源函数复制到目标函数。
(3)检查提炼出的代码,看是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。
(4)检查是否有“仅用于被提炼代码段”的临时变量,如果有,在目标函数中将它们声明为临时变量。
(5)检查提炼代码段,看是否有任何局部变量的值被它改变。(疑问1:这里不太理解,是指提炼代码改变了源函数的局部变量值么?)
(6)将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。
(7)处理完所有局部变量后,再进行编译。
(8)源函数中,将被提炼代码段替换为对目标函数的调用。
(9)编译,测试。
4.小结:
(1)将能提出的尽量都提出来。
(2)提出的代码如果希望返回两个值,就挑选另一块代码来提炼,最好让每个函数只有一个返回值。
Inline Method(内联函数)
1.概念:一个函数的本体与名称同样通俗易懂。于是在函数调用点插入函数本体,然后移除该函数。
举个例子,原函数:
public class ExtractMethod { int number = 6; public int getRating() { return (moreThanFiveLateDeliveries()) ? 2 : 1; } private boolean moreThanFiveLateDeliveries() { return number > 5; } }
重构后函数:
public class ExtractMethod { int number = 6; public int getRating() { return (number > 5) ? 2 : 1; } }
2.动机:
(1)当遇到其内部代码与函数名称同样清晰易读,即去掉这种无用的间接层,留下有用的间接层。
(2)当你手上有一群组织不甚合理的函数,就可以先将它们内联到一个大型函数,然后再提炼出合理地小型函数。
3.做法:
(1)检查函数,确定它不具有多态性(如果有子类继承了这个函数,就不要将这个函数内敛,不然子类无法覆写一个不存在的函数)。
(2)找出这个函数所有的被调用点。
(3)将这个函数所有的被调用点都替换为函数本体。
(4)编译,测试。
(5)删除该函数的定义。
Inline Temp(内联临时变量)
1.概念:
一个临时变量,只被一个简单的表达式赋值一次,而他妨碍了其他重构手法。将所有对该变量的引用动作,替换为对他赋值的那个表达式自身。
//改前 BigDecimal applePrice = getPrice().multiply(BigDecimal.valueOf(100L)); return (applePrice > 100); //改后 return (getPrice().multiply(BigDecimal.valueOf(100L)) > 100);
2.动机:
如果一个临时变量妨碍了其他重构手法,就应该将它内联化。
3.做法:
(1)检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
(2)如果这个临时变量未被声明为final,则先声明为final,然后编译(通过看是否能编译通过,来确认这个临时变量只被赋值过一次)。
(3)找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。
(4)每次修改后,编译并测试。
(5)修改完所有引用点后,删除该临时变量的声明和赋值语句。
(6)编译,测试。
Replace Temp with Query(以查询取代临时变量)
1.概念:
如果程序以一个临时变量保存某一表达式运算结果,则将这个表达式提炼到单独函数中,将所有对临时变量的引用点替换为对新函数的调用,此后,新函数就可被其他函数使用。
//改前 double applePrice = getPrice() * basePrice; if (applePrice > 100) { return applePrice * 0.95; } else { return applePrice * 0.98 } //改后 if (applePrice() > 100) { return applePrice * 0.95; } else { return applePrice * 0.98 } private double applePrice() { getPrice() * basePrice; }
2.动机:
2.1临时变量问题在于:
(1)是暂时的,只能在函数内使用
(2)可能会写出很长的函数表达式
2.2改成函数后:
(1)可供其他函数调用
(2)使代码可读性强
3.做法:
用例子看:
//原始代码 public double getPrice() {
int basePrice = quantity * itemPrice; double discountFactor; if (basePrice > 1000) { discountFactor = 0.95; } else { discountFactor = 0.98; } return basePrice * discountFactor; }
(1)找出赋值一次的临时变量,然后先用final去定义它们,去检查是否真的只被赋值了一次,如果编译出错说明不止被赋值了一次,就不该进行这项重构:
public double getPrice() {final int basePrice = quantity * itemPrice; final double discountFactor; if (basePrice > 1000) { discountFactor = 0.95; } else { discountFactor = 0.98; } return basePrice * discountFactor; }
(2)每次提取一个临时变量的函数,编译通过后再进行下一个:
public double getPrice() { final int basePrice = getBasePrice(); final double discountFactor; if (basePrice > 1000) { discountFactor = 0.95; } else { discountFactor = 0.98; } return basePrice * discountFactor; } private int getBasePrice() { return quantity * itemPrice; }
(3)编译测试完后,再依次替换所有的引用:
public double getPrice() { final int basePrice = getBasePrice(); final double discountFactor; if (getBasePrice() > 1000) { discountFactor = 0.95; } else { discountFactor = 0.98; } return getBasePrice() * discountFactor; } private int getBasePrice() { return quantity * itemPrice; }
(4)然后再提炼下一个临时变量:
public double getPrice() { final int basePrice = getBasePrice(); final double discountFactor = getDiscountFactor; return getBasePrice() * discountFactor; } private int getBasePrice() { return quantity * itemPrice; } private double getDiscountFactor() { if (getBasePrice() > 1000) { discountFactor = 0.95; } else { discountFactor = 0.98; } }
(5)删去final定义的变量,最后再进行一次编译测试:
public double getPrice() { return getBasePrice() * getDiscountFactor(); } private int getBasePrice() { return quantity * itemPrice; } private double getDiscountFactor() { if (getBasePrice() > 1000) { discountFactor = 0.95; } else { discountFactor = 0.98; } }
Introduce Explaining Variable(引入解释性变量)
1.概念:
有一个复杂的表达式,将该复杂表达式的结果放进一个临时变量,以此表达式名称解释表达式用途。
//改变前 if ((platform.toUpperCase().indexOf("MAC") > -1) && (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized && resize > 0) { //do something }
//改变后
final boolean isMacOs = platform.toUpperCase.indexOf("MAX") > -1;
final boolean isIEBrowser = browser.toUpperCase.indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
//do something
}
2.动机:
表达式可能非常复杂和难以阅读,这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。
3.做法:
//改之前
public double privce() { //price is base price - quantity discount + shipping return quantity * itemPrice - Math.max(0, quantity - 500) * itemPrice * 0.05 + Math.min(quantity * itemPrice * 0.1, 100.0); }
(1)声明一个final临时变量,将待分解表达式中一部分的运算当做结果赋值给它,并替换临时变量:
public double privce() { //price is base price - quantity discount + shipping final double basePrice = quantity * itemPrice; return basePrice - Math.max(0, quantity - 500) * itemPrice * 0.05 + Math.min(quantity * itemPrice * 0.1, 100.0); }
(2)依次提出:
public double privce() { //price is base price - quantity discount + shipping final double basePrice = quantity * itemPrice; final double quantityDiscount = Math.max(0, quantity - 500) * itemPrice * 0.05; final double shipping = Math.min(quantity * itemPrice * 0.1, 100.0); return basePrice - quantityDiscount + shipping; }
(*)运用提炼函数来试着处理:
public double privce() { return getBasePrice() - getQuantityDiscount() + getShipping(); } private double getBasePrice() { return quantity * itemPrice; } private double getQuantityDiscount() { return Math.max(0, quantity - 500) * itemPrice * 0.05; } private double getShipping() { return Math.min(quantity * itemPrice * 0.1, 100.0); }
问:到底什么时候用引入解释性变量的方式,什么时候用提炼函数的方式呢?
答:该重构方法主要是在提炼函数需要花费更大工作量时才使用。比如你有一个拥有大量局部变量的算法,那么使用提炼函数绝非易事。这时候就可以使用本文的方法来整理代码,然后再考虑下一步
怎么办;一旦搞清楚代码逻辑后,就可以运用以查询取代临时变量把中间引入的那些临时变量去掉。我想你会比较喜欢提炼函数,因为对于同一对象的任何部分,都可以根据自己的需要取用这些提炼
出来的函数。一开始会把这些新函数声明为private;如果其它对象也需要它们,就可以轻易释放这些函数的访问控制。
Split Temporary Variable(分解临时变量)
1.概念:
你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被利用于搜集计算结果。针对每次赋值,创造一个独立的,对应的临时变量。
//原始代码
double temp = 2 * (height + weight); log.info("temp:{}", temp); temp = height * width; log.info("temp:{}", temp);
//改后 final double perimeter = 2 * (height + weight); log.info("perimeter:{}", perimeter); final double area = height * width; log.info("area:{}", area);
2.动机:
当一个临时变量被赋值多次,就意味着它承担了一个以上的职责,就会降低可读性,因此就应该被替换成多个临时变量。
3.做法:
//改变前,可以看出acc被赋值两次 public 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; }
(1)将赋值两次的变量依次替换:
public 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; }
(2)然后再用其他手法进行重构
Remove Assignments to Paramenters(移除对参数的赋值)
1.概念:
代码对一个参数进行赋值,要以一个临时变量取代该参数的位置。
//修改前 public int discount(int inputVal, int quantity, int yearToDate) { if (inputVal > 50) { inputVal -= 2; } } //修改后 public int discount(int inputVal, int quantity, int yearToDate) { int result = inputVal; if (inputVal > 50) { result -= 2; } }
2.动机:
如例子所示,代码中改变了传入参数inputVal的值,降低了代码清晰度,混用了按值传递和按引用传递这两种参数传递方式。
3.做法:
(1)新建一个临时变量,把待处理的参数值附给它。
(2)然后将代码后面所有对参数的引用替换为对新建临时变量的引用。
(3)修改赋值语句,使其改为对新建临时变量的赋值。
(4)编译,测试。
(ps:较长函数中可以使用final来看引用的次数,提高代码清晰度,但在短的或者看起来很清楚的代码中,没有必要用)
Replace Method with Method Object(以函数对象取代函数)
1.概念:
当有一个大型函数,其中对局部变量的使用使你无法采取提炼函数,于是将这个函数放进单独对象中,如此一来局部变量就成了对象内的字段,然后你可以在同一个对象中将这个大型函数分解为多个小型函数。
2.动机:
当一个函数中局部变量泛滥,想分解这个函数是非常困难的,那么使用以函数对象取代函数这个方法,可以减轻这个负担。它会将所有局部变量都变成函数对象的字段,然后就能对新对象通过提炼函数创造新的函数,从而将原本的大型函数拆解变短。
3.做法:
原函数:
class Account{ int gamm(int value, int quantity, int year2Date){ int importValue1 = (value * quantity) + delta(); int importValue2 = (value * year2Date) + 200; if(year2Date - importValue1 >200) importValue2-=50; int importValue3 = importValue2 * 8; //...... return importValue3 - 2 * importValue1; } }
(1)建立一个新类,根据待处理函数用途,为这个类命名。
class Gamm{}
(2)在新类中建立final字段,用以保存原先大型函数所在的对象。将这个字段称为源对象,在新类中把原函数的临时变量和参数字段一一对应过来。
class Gamm{ private final Account _account; private int value; private int quantity; private int year2Date; private int importValue1; private int importValue2; private int importValue3;
....
}
(3)在新类中创建一个构造函数,接收源对象及原函数的所有参数作为参数。
class Gamm{
private final Account _account;
private int value;
private int quantity;
private int year2Date;
private int importValue1;
private int importValue2;
private int importValue3;
Gamm(Account source, int inputVal, int quantity, int year2Date){ this._account = source; this.value = inputVal; this.quantity = quantity; this.year2Date = year2Date; }
}
(4)在新类中建立一个compute()函数。
(5)将原函数的代码复制到compute()函数中,如果需要调用原函数的任何函数,请通过源对象字段调用。
int compute(){ importValue1 = (value * quantity) + _account.delta(); importValue2 = (value * year2Date) + 200; if(year2Date - importValue1 >200) importValue2-=50; importValue3 = importValue2 * 8; //..... return importValue3 - 2 * importValue1; }
(6)编译
(7)将旧函数的函数本体替换成这样一句话,创建上述新类的一个新对象,而后调用其中的compute()函数。
int gamm(int value, int quantity, int year2Date){ return new Gamm(this,value,quantity,year2Date).compute(); }
int compute(){ importValue1 = (value * quantity) + _account.delta(); importValue2 = (value * year2Date) + 200; importantThing(); importValue3 = importValue2 * 8; //..... return importValue3 - 2 * importValue1; } private void importantThing() { if(year2Date - importValue1 >200) importValue2-=50; }
小结:将大型函数中,或者变量特别多的函数,将它们拆分成小的函数,可以轻松地对compute()函数采取提炼函数,且不必担心参数传递的问题。
Substitute Algorithm(替换算法)
1.概念:你想要把某个算法替换为另一个更加清晰的算法,即将函数本体替换成另一个算法。
//改变前 public String foundPerson(String[] people) { for (int i = 0; i < people.length; i++) { if (people[i].equalsIgnoreCase("Don")) { return "Don"; } if (people[i].equalsIgnoreCase("John")) { return "John"; } if (people[i].equalsIgnoreCase("Kent")) { return "Kent"; } } return " "; }
//改变后
public String foundPerson(String[] people) { List candidates = Arrays.asList(new String[]{"Dom", "John", "Kent"}); for (int i = 0; i < people.length; i++) { if (candidates.contains(people[i])) { return people[i]; } } return " "; }
小结
在满足一定情况下:
1.函数过长则重构,重构想到提炼函数的方法(提炼函数)。
2.直接用方法本身(内联函数)。
3.用自身表达式替换当前引用(内敛临时变量,如直接return)。
4.用方法替换某一表达式(以查询取代临时变量)
5.将复杂表达式放进临时变量(引入解释性变量)
6.将一个赋值多次的临时变量替换成不同的变量(分解临时变量)
7.传入参数要赋值,就用另一个临时变量代替(移除对参数的赋值)
8.大型函数局部变量泛滥,将这部分单独拆出去(以函数对象取代函数)
9.将看起来不太聪明的算法替换成看起来简洁干净的算法(替换算法)