Extract Method(110)最大的困难时处理局部变量,而临时变量则是其中一个主要的困难源头。
- Extract Method(110)是我最常用的重构手法之一。当我看见一个过长的函数或者一段需要注释才能让人理解用途的代码,我就将这段代码放进一个独立的函数。
- 如果每个函数的粒度都很小,那么函数被复用的机会就更大。
- 函数命名长度不是问题,关键在与函数名称与函数本体之间的语义距离。
做法:
创造一个新函数,根据函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎么做”来命名):即使你想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好的方式招示代码意图,也应该提炼它。但如果想不出更有意义的名称,就别动。
将提炼出的代码从源函数复制到新建的目标函数中。
仔细渐渐提炼出的代码,看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。
检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将它们声明为临时变量。
检查被提炼代码段,看看是否有任何局部变量的值被它改变。 如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询。并将结果复制给相关变量,如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动的提炼出来。你可能需要先使用Split Temporary Variable(128),然后再尝试提炼。也可以使用Replace Temp With Query(120)将临时变量消灭掉。
将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。
处理完所有局部变量之后,进行编译。
在源函数中,将被提炼代码段替换为对目标函数的调用。
编译,测试。
动机
有时候你遇到某些函数,其内部代码和函数名称同样清晰易读。重构了该函数后,其内容和名称同样清洗。你该直接使用其中的代码。
可以将所要的函数的所有调用对象的函数内容都内联到函数对象中。
做法:
检查函数,确定它不具多态性。如果子类继承了这函数,就不要将词函数内联,因为子类无法覆写一个根本不存在的函数。
找出这个函数的所有被调用点。
将这个函数的所有被调用点都替换为函数本体。
编译、测试、
删除该函数的定义。
动机:
Inline Temp (119)多半是作为Replace Temp with Query(120)的一部分使用的,所以真正的动机出现在后者那儿。唯一单独使用Inline Temp(119)的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般说,这样的临时变量不会有任何危害,可以放心的把它留在那儿。但如果那个临时变量妨碍了其他的重构手法,例如Extract Method(110),你就应该将它内联化。
做法:
- 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用;
- 如果这个练市变量未被声明为final,那就将它声明为final,然后编译。> 这可以检查该临时变量是否真的只被赋值一次。
- 找到该临时变量的所有引用点,将他们替换为“为临时变量赋值”的表达式。
- 每次修改后,编译并测试。
- 修改完所有的引用点,删除该临时变量的声明和赋值语句。
- 编译、测试。
将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用。伺候,新函数就可以被其他函数使用。
动机:
- 临时变量的问题在于:他们是暂时的,而且只能在所属函数内使用。由于临时变量只在所属函数内可见,所以他们会驱使你写出更长的函数。
- Replace Temp with Query(120)往往是你运用Extract Method(110)之前必不可少的一个步骤。局部变量会使代码难以被提炼,所以你应该尽可能把他们替换为查询式。
- 这个重构手法较为简单的情况是:临时变量只被赋值一次,或者赋值给临时变量的表达式不受其他条件影响。
做法:
-
找出只被赋值一次的临时变量:如果某个临时变量被赋值超过一次,考虑使用Split Temporary Variable(128)将它分割成多个变量。
-
将该临时变量声明为final。
-
编译。
这可确保该临时变量的确只被赋值一次。
-
将“对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中
首先将函数声明为private。日后你可能会发现有更多类需要使用它,那是放松对它的保护也很容易。
确保提炼出来的函数无任何副作用,就是说该函数并不修改任何对象内容。如果有副作用,就对他进行Separate Query from Modifier(279)。
-
编译、测试。
-
在该临时变量身上实施Inline Temp (119)。
例子
重构之前:
double getPrice(){
int basePrice = _quantity * _itemPrice;
double discountFactor;
if (basePrice >1000) {
discountFactor =0.95;
}
else{
discountFactor= 0.98;
}
return basePrice * discountFactor;
}
重构之后:
private int basePrice(){
return _quantity * _itemPrice;
}
private double discountFactor(){
if(basePrice()>1000){
return 0.95;
}
else{
return 0.98;
}
}
double getPrice(){
return basePrice()*discountFactor();
}
你有 一个复杂的表达式,将该复杂表达式(或其中一部分)的结果放进一个临时变量,一次变量名称来解释表达式用途。
动机
- 表达式有可能非常复杂而难以阅读,这时临时变量可以帮助你将表达式分解为比较容易管理的形式。
- 在条件逻辑中,Introduce Explaining Variable(124)特别有价值。你可以用这项重构将每个条件自居提炼出来,以一个良好命名的临时变量来解释对应条件字句的意义。另一种情况是,在较长算法中,可以运用临时变量来解释每一步运算的意义。
- 临时变量局限性很大,当局部变量使Extract Method(110)难以进行时,我就使用Introduce Explaining Variable(124).
做法
-
声明一个final临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它。
-
将表达式中的“运算结果”这一部分,替换为上述临时变量。
如果被替换掉这一部分在代码中重复出现,你可以每次一个,逐一替换。
-
编译,测试。
-
重复上述过程,处理表达式的其他部分。
示例:重构前
double price(){
return _quantity * _itemPrice -
Math.max(0,_quantity-500)*_itemPrice*0.05+
Math.min(_quantity *_itemPrice *0.1,100.0);
}
运用Introduce Explaining Variable重构后
double price(){
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;
}
运用Extract Method 重构
double price(){
return basePrice()-quantityDiscount()+shipping();
}
private double quantityDiscount(){
return Math.max(0,_quantity-500)*_itemPrice*0.05;
}
private double shipping(){
return Math.min(_quantity *_itemPrice *0.1,100.0);
}
private double basePrice(){
return _quantity * _itemPrice;
}
何时使用Introduce Explaining Variable:答案是在Extract Method(110)需要花费更大工作量时。如果处理的是一个拥有大量局部变量的算法,那么使用Extract Method绝非易事。
你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。针对每次赋值,创造一个独立、对应的临时变量。
动机
- 临时变量有各种不同用途,其中某些用途会很自然你导致临时变量被多次赋值。“循环变量”和“结果收集变量”就是典型例子。
- 如果临时变量承担多个责任,他就应该被替换分解为多个临时变量,每个变量值承担一个责任。同一个临时变量承担两件不同的事件,会令代码阅读者糊涂。
做法
-
在带分解临时变量的声明及第一次被赋值处,修改其名称。
如果稍后的赋值语句是[i= i+某表达式]形式,就以为这是个结果收集变量,那就不要分解它。结果收集变量的作用通常是累加、字符串接合、写入流或者向集合添加元素。
-
将新的临时变量声明为final。
-
以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让他们引用新的临时变量。
-
在第二次赋值处,重新声明原先那边临时变量。
-
编译,测试。
-
组词重复上述过程。每次都在声明处对临时变量改名,并修改下次赋值之前的引用点。
范例
double getDistanceTravelled(int time){
double result;
double acc= _primaryForce/_mass;
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;
}
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 secodaryAcc = (_primaryForce +_secondaryForce)/_mass;
result+= primaryVel*secondaryTime +0.5*secodaryAcc*secondaryTime*secondaryTime;
}
return result;
}
苏格兰布丁
代码对一个参数进行赋值,以一个临时变量取代该参数的位置
动机
- 对参数赋值,会降低代码的清晰度,混用了按值传递和按引用传递这两种参数船体方式。Java只采用按值传递方式。
- 在按值传递的情况下,对参数的任何修改都不会对调用端造成任何影响。
- 在Java中,不要对参数赋值,如果做了,请使用Remove Assignment Parameters(131).
做法
-
建立一个临时变量,把待处理的参数值赋予它。
-
以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为“对此临时变量 的引用”。
-
修改赋值语句,使其改为对新建的临时变量赋值。
-
编译,测试。
如果代码的语义是按引用传递的,请在调用端检查调用后是否还使用了这个参数,也要检查有多少个按引用传递的参数被赋值后有被使用。请尽量只以return方式返回一个值。如果需要返回的值不止一个,看看可否把需返回的大堆数据编程单一对象,或干脆把每个返回值设计对应的独立函数。
范例
int discount(int input Val,int quantity,int yearToDate){
if(inputVal >50) inputVal-=2;
if(quantity>100) inputVal-=1;
if(yearToDate>10000) inputVal-=4;
return inputVal;
}
重构1
int discount(int input Val,int quantity,int yearToDate){
int result=inputVal;
if(inputVal >50) result-=2;
if(quantity>100) result-=1;
if(yearToDate>10000) result-=4;
return result;
}
重构2:还可以为参数加上关键字final,从而强制它遵循“不对参数复制”这一惯例
int discount(final int input Val,final int quantity,final int yearToDate){
int result=inputVal;
if(inputVal >50) result-=2;
if(quantity>100) result-=1;
if(yearToDate>10000) result-=4;
return result;
}
注:第二种方法不常用,通常会在较长的函数中使用它,让它帮忙检查参数是否被做了修改
Java的按值传递
- 在所有地方,Java都严格采用按值传递的函数调用方式。
你有一个大型函数,其中对局部变量的使用使你无法采用Extract Method(110).
将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段,然后你可以在同一对象中将大型函数分解为多个小型函数。
动机
- 小型函数的优美动人,只要将相对独立的代码从大型函数提取出来,就可以大大提高代码的可读性。
- 但是局部变量的存在会增加函数的分解难度。函数对象会将所有局部变量都变成函数对象的字段,然后就可以对这个对象使用Extract Method(110)创造出新函数。
做法
- 建立一个新类,根据待处理函数的用途,为其命名;
- 在新类中建立一个final字段,用以保存原先大型函数所在的对象,我们将其称为“源对象”。同时,针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保存之。
- 在新类中建立一个构造函数,接受源对象及原函数的所有参数作为参数。
- 在新类中建立一个compute()函数。
- 将原函数的代码赋值到compute()函数中,如果需要调用源对象的任何函数,请通过源对象字段调用。
- 编译。
- 将旧函数的函数本体替换为这样一条语句“创建上述新类的一个新对象,而后调用其中的compute()函数”。
范例
class Account{
int gamma(int inputVal,int quantity,int yearToDate){
int importantValue1 =(inputVal*quantity)+delta();
int importantValue2 = (inputVal*yearToDate)+100;
if((yearToDate-importantValue1)>100){
importantValue2-=20;
}
int importantValue3= importantValue2*7;
//and so on
return importantValue3 - 2* importantValue1;
}
}
重构后
class Gamma{
private final Account _account;
private int inputVal;
private int quantityVal;
private int yearToDate;
private int importValue1;
private int importValue2;
private int importValue3;
Gamma(Account source,int inputValArg,int quantityArg,int yearToDateArg){impo
_account=source;
inputVal = inputValArg;
quantityVal=quantityArg;
yearToDate = yearToDateArg;
}
int commpute(){
importantValue1 =(inputVal*quantity)+_account.delta();
importantValue2 = (inputVal*yearToDate)+100;
importantThing()
int importantValue3= importantValue2*7;
//and so on
return importantValue3 - 2* importantValue1;
}
}
int gamma(int inputVal,int quantity,int yearToDate){
return new Gamma(this,inputVal,quantity,yearToDate).commpute();
}
void importantThing(){
if((yearToDate-importantValue1)>100){
importantValue2-=20;
}
}
你想要把某个算法替换成另一个更清晰的算法。
将函数本体替换为另一个算法。
String foundPerson(String[] people){
for(int i=0;i<people.length;i++){
if(people[i].euqals("Don")){
return "Don";
}
if(people[i].equals("John")){
return "John";
}
if(people[i].equals("Kent")){
return "kent";
}
}
return "";
}
重构后
String foundPerson(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 "";
}
动机
- 重构可以把一些复杂东西简单化,但有时必须装饰断腕,删掉整个算法,代之简单算法。
- 使用这项重构手法之前,先确定自己已经尽可能分解了原先函数。
做法
-
准备好另一个(替换用)算法,让它通过编译。
-
针对现有测试,执行上述的新算法。如果结果与原本结果相同,重构结束。
-
入股屙屎结果不同于原先,在测试和调试过程中,以旧算法为比较参照标准。
对于每个测试用例,分别以新旧算法运行,并观察两者结果是否相同。这可帮助你看到哪一个测试用例出现麻烦,以及出现了怎么的麻烦。