《重构--改善既有代码的设计》总结三之重新组织函数

一、Extract Method(提炼函数)

你有一段代码可以被组织在一起并独立出来


将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。

private void printOwing(double amount){
    printBanner();
    
    //print details
    System.out.println("name:"+_name);
    System.out.println("amount:"+amount);
}

改为

private void printOwing(double amount){
    printBanner();
    printDetails(amount);
}

private void printDetails(double amount){
    System.out.println("name:"+_name);
    System.out.print("amount:"+amount);
}


动机

当看见一个过长的函数或者一段需要注释才能让人理解用途的代码,就将这段代码放进一个独立函数中。

首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来就像一系列注释;再次,如果函数都是细粒度,那么函数的覆写也会更容易些。

函数长度不是问题,关键在于函数名称和函数本体之间的语义感。如果提炼可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。


做法

创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎样做”命名)。

(即使你想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好的方式昭示代码意图,你也应该提炼它。但如果你想不出一个更有意义的名称,就别动。)

将提炼出的代码从原函数复制到新建的目标函数中。

仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。

检查被提炼的代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量的值被修改了,看看是否可以将被提炼的代码段处理为一个查询,并将结果赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。你可能需要Split Temporary Variable,然后再尝试提炼。也可以使用Replace Temp with Query将临时变量消灭掉。

将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。

处理完所有局部变量之后,进行编译。

在源函数中,将被提炼代码段替换为对目标函数的调用(如果你将任何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在你可以删除这些声明式了)。

编译,测试。


二、Inline Method(内联函数)

一个函数的本体与名称同样清楚易懂。


在函数调用点插入函数本体,然后移除该函数。

private int GetRating(){
    return (moreThanFiveLateDeliveries())?2:1;
}

private boolean moreThanFiveLateDeliveries(){
    return _numberOfLateDeliveries > 5;
}
改为

private int getRating(){
    return (_numberOfLateDeliveries > 5)?2:1;
}


动机

有些函数,其内部代码和函数名称同样清晰易读,也可能你重构了该函数,使得其内容和其名称变得同样清晰。果真如此,你就应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总是让人不舒服。

另一种情况是:你手上有一群组织不甚合理的函数。你可以将它们内连到一个大型函数中,再从中提炼出组织合理的小型函数。

比起既要移动一个函数、又要移动它所调用的其他所有函数,将整个大型函数作为整体来移动会比较简单。

间接层有其价值,但不是所有的间接层都有其价值。试着使用内连手法,可以找出那些有用的间接层,同时将那些无用的间接层去除。


做法

检查函数,确定它不具多态性(如果子类继承了这个函数,就不要将此函数内连,因为子类无法覆写一个根本不存在的函数)。

找出这个函数所有被调用点。

将这个函数所有被调用点都替换为函数本体。

编译,测试。

删除该函数的定义。


三、Inline Temp(内联临时变量)

你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。


将所有对该变量的引用动作,替换为对它赋值的安格表达式自身。

double basePrice = anOrder.basePrice();
return (basePrice > 1000);
改为

return (anOrder.basePrice() > 1000);


动机

Inline Temp多半是作为Replace Temp with Query的一部分使用的,所以真正的动机出现在后者那。唯一单独使用Inline Temp的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说,这样的临时变量不会有任何危害,可以放心地把它留在那。但是如果这个临时变量妨碍了其他的重构手法,例如Extract Method,你就应该将它内联化。


做法

检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。

如果这个临时变量并未被声明为final,那就将它声明为final,然后编译(这可以检查该临时变量手法真的只被赋值一次)。

找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。

每次编译后,编译并测试。

修改完所有引用点之后,删除该临时变量的声明和赋值语句。

编译,测试。


四、Replace Temp with Query(以查询取代临时变量)

你的程序以一个临时变量保存某一表达式的运算结果。


将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可以被其他函数使用。

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;
...

private double basePrice(){
    return _quantity*_itemPrice;
}
    


动机

临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用。如果把临时变量替换为一个查询,那么同一个类中的所有函数都将可以获得这份信息。


做法

首先是简单情况:

找出只被赋值一次的临时变量(如果某个临时变量被赋值超过一次,考虑使用Split Temporary Variable将它分割为多个变量)。

将该临时变量声明为final。

编译(这可确保该临时变量的确只被赋值一次)。

将“对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中(首先将函数声明为private,日后你会发现有更多类需要使用它,那时放松对它的保护也很容易。确保提炼出的函数五任何副作用,也就是说该函数并不修改任何对象内容。如果它有副作用,就对它进行Separate Query from Modifler)。

编译,测试。
在该临时变量身上实施Inline Temp。

注:如果临时变量不只被赋值一次,就不该进行这项重构。


五、Introduce Explaining Variable(引入解释性变量)

你有一个复杂的表达式。


将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。

if((platform.toUperCase().indexOf("MAC")) > -1) &&
(browser.toUperCase().indexOf("IE") > -1) &&
wasInitialized() && resize > 0){
    //do something
}
改为

final boolean isMacOs = platform.toUperCase().indexOf("MAC") >-1;
final boolean isIEBrowser = browser.toUperCase().indexOf("IE") > -1;
final boolean wasResized = resized > 0;
if(isMacOs && isIEBrowser && wasInitialized() && wasResized){
    //do something
}


动机

表达式有可能非常复杂而难以阅读。在这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。


做法

声明一个final临时变量,将待分解之复杂表达式中的一部分动作的运算结果复制给它。

将表达式的“运算结果”这一部分,替换为上述临时变量(如果被替换的这一部分在代码中重复出现,你可以每次一个,逐一替换)。

编译,测试。

重复上述过程,处理表达式的其他部分。

注:在什么时候使用Introduc Explaining Variable呢?答案是:在Extract Method需要花费更大工作量时。


六、Split Temporary Variable(分解临时变量)

你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。


针对每次赋值,创造一个独立、对应的临时变量。

double temp = 2*(_height+_width);
System.out.println(temp);
temp = _height*_width;
System.out.println(temp);
改为

final double perimeter = 2*(_height + _width);
System.out.println(perimeter);
final double area = _height*_width;
System.out.println(area);


动机

临时变量应该只被赋值一次。如果它们被赋值超过一次,就意味着它们在函数中承担了一个以上的责任。如果临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,每个变量只承担一个责任。同一个临时变量承担两件不同的事情,会令代码阅读者糊涂。


做法

在待分解临时变量的声明及第一次被赋值处,修改其名称(如果稍后之赋值语句是【i=i+某表达式】形式,就意味着这是个结果收集变量,那么就不要分解它。结果收集变量的作用通常是累加、字符串接合、写入流或者向集合添加元素)。

将新的临时变量声明为final。

以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。

在第二次赋值处,重新声明原先那个临时变量。

编译,测试。

逐次重复上述过程。每次都在声明处对临时变量改名,并修改下次赋值之前的引用点。


七、Remove Assignments to Parameters(移除对参数的赋值)

代码对一个参数进行赋值。


以一个临时变量取代该参数的位置。

private int discount (int inputVal, int quantity, int yearToDate){
    if(inputVal > 50)
        inputVal -= 2;
        
改为

private int discount(int inputVal, int quantity, int yearToDate){
    int result = inputVal;
    if(inputVal > 50)
        result -=2;    


动机

之所以不喜欢这样的做法,因为它降低了代码的清晰度,而且混用了按值传递和按引用传递这两种参数传递方式。

在java中,不要对参数赋值:如果代码已经这样做了,请使用Remove Assignments to Parameters。


做法

建立一个临时变量,把待处理的参数数值赋予它。

以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为“对此临时变量的引用”。

修改赋值语句,使其改为对新建之临时变量赋值。

编译,测试(如果代码的语义是按引用传递的,请在调用端检查调用后是否还使用了这个参数。也要检查有多少个按引用传递的参数被赋值后又被使用。请尽量只以return方式返回一个值。如果需要返回的值不止一个,看看可否把需要返回的大堆数据变成单一对象,或干脆为每个返回值设计对应的一个独立函数)。



八、Replace Method with Method Object(以函数对象取代函数)

你有一个大型函数,其中对局部变量的使用使你无法采用Extract Method.


将这个函数放进一个单独对象中,如此一来局部变量就成了对象内字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数。

class Order...
    double price(){
        double primaryBasePrice;
        double secondaryBasePrice;
        double tertiaryBasePrice;
        //long computation;
        ...
    }
改为




动机

小型函数更加优美动人。只要将相对独立的代码从大型函数中提炼出来,就可以大大提高代码的可读性。

但是,局部变量的存在会增加函数分解难度。Replace Temp with Query可以帮助减轻这一负担,但有时候你会发现根本无法拆解一个需要拆解的函数。这种情况下,应该把手伸进工具箱的深处,基础函数对象(method object)这件法宝。


做法

建立一个新类,根据待处理函数的用途,为这个类命名。

在新类中建立一个final字段,用以保存原先大型函数所在的对象。我们将这个字段称为“源对象”。同时,针对源函数的每个临时变量和每个参数,在新类中建立一个对应的字段保存之。

在新类中建立一个钩子函数,接收源对象及原函数的所有参数作为参数。

在新类中建立一个compute()函数。

将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。

编译。

将旧函数的函数本体替换为这样一条语句:”创建上述新类的一个新对象,然后调用其中的compute()函数“。


因为所有局部变量现在都成了字段,所以你可以任意分解这个大型函数,不必传递任何参数。


九、Substitute Algorithm(替换算法)

你想要把某个算法替换为另一个更清晰的算法。


将函数本体 替换为另一个算法。

private String foundPerson(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 "";
}
改为

private 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 "";
}

动机

如果你发现做一件事可以有更清晰的方式,就应该以较清晰的方式取代赋值的方式。


做法

准备好另一个(替换用)算法,让它通过编译。

针对现有测试,执行上述的新算法。如果结果与原本结果相同,重构结束。

如果测试结果不同于原先,在测试和调试过程中,以旧算法为比较参照标准(对于每个测试用例,分别以新旧两种算法执行,并观察两者结果是否相同。这可以帮助你看到哪一个测试用例出现麻烦,以及出现了怎样的麻烦)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值