1. 程式人生 > 實用技巧 >第6章 重新組織函式

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

(完)