第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.将看起来不太聪明的算法替换成看起来简洁干净的算法(替换算法)

posted @ 2020-08-24 11:24  pmingup9012  阅读(235)  评论(0编辑  收藏  举报