天天看點

重構 改善既有代碼的設計—— 重新組織方法

一.Extract Method(提煉方法)

1.動機:如果函數過長或代碼段需要注釋才能了解,就将這段代碼放到獨立函數中;有幾個原因造成我喜歡簡短而命名良好的的方法:

A.函數粒度小,複用幾率高

B.函數粒度小,複寫容易

C.函數粒度小,使高層函數讀起來向一系列注釋

   常常有人在問,一個方法長度多長才算合适。在我看來,方法多長不是問題,關鍵是方法名和方法體之間的語義距離。如果提煉可以強化方法的清晰度,即使提煉出來的方法的      方法名比方法體還長也無所謂。

2.做法

A.創造新的目标函數,根據函數功能命名;即使提煉的代碼很簡單, 隻要目标函數的名稱能更好的昭示代碼意圖,也應該提煉它。如果你想不出一個更有意義的名稱,那就别動它;

B.将提煉的代碼從源函數中複制到目标函數;

C.仔細檢查提煉出的代碼,檢視其中是否引用了作用域僅限于源函數的變量(包括局部變量和源函數參數);

D.檢查是否有僅用于被提煉代碼段的臨時變量,如果有,在目标函數中将他們聲明為臨時變量;

E.檢查被提煉函數,檢視是否有局部變量被其改變;如果有一個臨時變量被改變,看看是否可以将被提煉代碼段處理成查詢,将傳回值賦給相關變量;如果有多個臨時變量被改變,就需要先使用Split Temporary Variable,然後再提煉;也可以先使用Replace Temp with Query消滅臨時變量;

F.将被提煉代碼段中需要的變量當作參數傳給目标函數;

G.處理完所有變量後,編譯;

H.在源函數中,将被提煉代碼段替換成目标函數的引用;如果你将被提煉代碼段的任何變量都放到目标函數中,請檢查它們原來的聲明是否還在,如果在的話,可以删除了;

I.編譯,測試;

3.範例 

A無局部變量

private void printOwing(Enumeration enumeration){
    double outStanding = 0.0;

    System.out.println("*****************************");
    System.out.println("*******Custorm Owes**********");
    System.out.println("*****************************");

    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }

    System.out.println("name:" + name);
    System.out.println("outStanding :" + outStanding);
}      

提煉列印橫幅的代碼段,隻要剪切複制即可:

private void printOwing(Enumeration enumeration){
    double outStanding = 0.0;

    printBanner();
    
    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }

    System.out.println("name:" + name);
    System.out.println("outStanding :" + outStanding);
}


private void printBanner(){
    System.out.println("*****************************");
    System.out.println("*******Custorm Owes**********");
    System.out.println("*****************************");
}      

B.有局部變量;問題點在意源函數的參數和源函數聲明的臨時變量,局部變量的作用域僅限于源函數,是以提煉代碼段時需要花功夫處理這些臨時變量;局部變量最簡單的情況是被提煉代碼隻是讀取這些值,而不用修改它們,這種情況下可以将局部變量當作參數傳遞給目标函數;

private void printOwing(Enumeration enumeration){
    double outStanding = 0.0;

    System.out.println("*****************************");
    System.out.println("*******Custorm Owes**********");
    System.out.println("*****************************");

    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }

    System.out.println("name:" + name);
    System.out.println("outStanding :" + outStanding);
}      

将列印詳細資訊的代碼段提煉到目标函數中:

private void printOwing(Enumeration enumeration){
    double outStanding = 0.0;
    printBanner();
    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }
    printDetail(outStanding);
}

private void printDetail(double outStanding){
    System.out.println("name:" + name);
    System.out.println("outStanding :" + outStanding);
}      

C.對局部變量指派

如果被提煉的代碼段對局部變量指派,就略微複雜;此時可分兩種情況:1.被指派的臨時變量隻在目标函數中使用,此時可以将聲明直接放到目标函數中。2.被提煉的代碼段之外的代碼也使用了這個變量,這也分兩種情況:1.如果這個變量在被提煉代碼段後使用,直接在代碼段中修改就好了;如果被提煉的代碼段後的代碼也使用了這個變量,就需要讓目标函數傳回該變量修改後的值;代碼如下:

private void printOwing(Enumeration enumeration){
    double outStanding = 0.0;
    printBanner();
    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }
    printDetail(outStanding);
}      

現在把計算代碼提煉出來:

private void printOwing(Enumeration enumeration){
    printBanner();
    double outStanding = getOutStanding(enumeration);
    printDetail(outStanding);
}

private double getOutStanding(Enumeration enumeration){
    double outStanding = 0.0;
    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }
    return outStanding;
}      

二.Inline Method(内聯方法)

一個函數的本體與名稱同樣清晰易懂,在函數調用點插入函數本體,然後移除該函數

private int getRating(){
    return (moreThanFiveLaterDeliveries()) ? 3:2;
}

private boolean moreThanFiveLaterDeliveries(){
    return numberOfLaterDeliveries > 5;
}

private int getRating(){
    return (numberOfLaterDeliveries > 5) ?3:2;
}      

1.動機:

A.函數内部代碼和函數名稱同樣清晰可讀,此時應該去除函數名稱,直接使用其中的代碼;

B.有一群不甚合理的函數,此時可以把他們全部内聯到一個大型函數中,再從中提煉出小函數;

C.如果使用了太多的間接層,使得系統中所有函數都似乎是對其他函數的簡單委托,造成我在這些委托之間暈頭轉向,此時可以使用内聯方法;當然間接層有其價值,但并    非所有間接層都有意義,找出那些無意義的,然後直接删除;

2.做法

A.檢查函數,确定它不具有多态性(如果子類繼承了該函數,就不要内聯,因為子類無法繼承一個不存在的函數);

B.找出該函數的所有調用點;

C.将這個函數的所有調用點替換成函數本體

D.編譯、測試

E.删除該函數的定義;

三.Inline Temp(内聯臨時變量)

如果一個臨時變量隻被一個簡單表達式指派一次,而它妨礙了其他重構手法,将所有對該變量的操作,替換成對它指派的那個表達式本身。

1.動機

A.Inline Temp多作為Replace Temp with Query的一部分使用的,是以真正的動機在後者;

B.Inline Temp單獨使用的情況是,某個臨時變量被賦予某個函數的傳回值。一般來說,這個變量不會有什麼影響,但是如果這個變量妨礙了其他重構手法,你就應該将之内聯化;

2.做法

A.檢查給臨時變量指派的語句,確定等号後邊的指派語句沒有副作用;

B.如果這個臨時變量沒有被聲明為final,那就聲明為final,然後編譯(可以檢查該臨時變量是否隻被指派一次);

C.找到該臨時變量的所有引用點,替換成“為臨時變量指派的語句”表達式;

D.每次修改後,編譯并測試;

E.修改完所有引用點後 ,删除該臨時變量的聲明和指派語句;

F.編譯、測試;

四.Replace Temp with Query(以查詢取代臨時變量)

程式以臨時變量儲存某一表達式的運算結果,将這個表達式提煉到獨立函數中,把表達式的所有調用點換成獨立函數的調用。

private int getResult(){
    int result = num * price;
    if (result > 10){
        return result + 20;
    }
    return result + 10;
}      
private int getResult(){
    if (getCaculateResult() > 10){
        return getCaculateResult() + 20;
    }
    return getCaculateResult() + 10;
}

private int getCaculateResult(){
    return num * price;
}      

1.動機

A.臨時變量的問題在于作用域隻在函數内,如果通路需要的臨時變量,可能會驅使寫出很長的代碼;如果把臨時變量換成查詢,那麼整個類中的所有函數都可以通路;

2.做法

A.找出隻被指派一次的臨時變量(如果某個臨時變量被多次指派,考慮采用Split Temporary Variable,将之分割成多個臨時變量

B.将該臨時變量聲明為final

C.編譯(可檢測該臨時變量是否隻被指派一次)

D.将“對該變量指派”的語句等号右側提煉到獨立函數中(1.首先将函數聲明為private,以後如有需求再放開權限;2.確定提煉出來的函數無任何副作用,如果有的話,對它進行Separate Query from Modifier)

E.編譯、測試

F.對該臨時變量使用Inline Temp

3.範例

private double getPrice(){
    int basePrice = quantity * itemPrice;
    double discountFactor;
    if (basePrice > 100){
        discountFactor = 0.95;
    }else {
        discountFactor = 0.98;
    }
    return basePrice * discountFactor;
}      

我希望将兩個臨時變量全都替換掉,當然每次一個。首先将變量聲明為final,确認臨時變量隻被指派一次(如果不是的話那說明目前的重構是不可用的)

private double getPrice(){
    final int basePrice = quantity * itemPrice;
    final double discountFactor;
    if (basePrice > 100){
        discountFactor = 0.95;
    }else {
        discountFactor = 0.98;
    }
    return basePrice * discountFactor;
}      

 接下來替換臨時變量,将等式右側的表達式提煉出來;

private double getPrice(){
    final int basePrice = basePrice();
    final double discountFactor;
    if (basePrice > 100){
        discountFactor = 0.95;
    }else {
        discountFactor = 0.98;
    }
    return basePrice * discountFactor;
}

private int basePrice(){
    return quantity * itemPrice;
}      

編譯并測試,沒問題的話将臨時變量調用點全部換成目标函數

private double getPrice(){
    final double discountFactor;
    if (basePrice() > 100){
        discountFactor = 0.95;
    }else {
        discountFactor = 0.98;
    }
    return basePrice() * discountFactor;
}      

然後以類似辦法處理discountFactor

private double getDiscountFactor(){
    if (basePrice() > 100){
        return  0.95;
    }else {
        return  0.98;
    }
}      

最後得到函數:

private double getPrice(){
    return basePrice() * getDiscountFactor();
}      

五.Introduce Explaining Variable(引入解釋性變量)

你有一個複雜的表達式,将該表達式(或其中的一部分)的結果放進一個臨時變量,以此變量名稱來解釋表達式用途。

if ((browser.toUpperCase().indexOf("MAC") == -1) && (platform.toUpperCase().indexOf("IE") == -1) && isWaitInitialized() && resize > 10) {
    // do something
}      

改寫成:

boolean isMacOS = (browser.toUpperCase().indexOf("MAC") == -1);
boolean isIEBrowsers = (platform.toUpperCase().indexOf("IE") == -1);
boolean isResized = (resize > 10);
if ( isMacOS && isIEBrowsers  && isWaitInitialized() && isResized) {
    // do something
}      

1.動機

A.表達式可能非常難讀和了解,換成臨時變量可以有助于分解表達式;尤其是在條件邏輯中,以一個良好命名的臨時變量來解釋對應條件字句的意義。

B.Introduce Explaining Variable和Replace Temp with Query有異曲同工之妙。差别在于臨時變量限制較大,隻能在函數内部使用;而獨立函數可以在整個類中使用。但是當Extract Method有困難的時候,使用Introduce Explaining Variable還是很友善的。

2.做法

A.聲明一個final的臨時變量,将待分解之複雜表達式或部分運算結果指派給它;

B.将表達式中的“運算結果”替換成上述臨時變量;

C.編譯、測試;

六.Split Temporary Variable(分解臨時變量)

程式中有某個臨時變量被指派超過一次,它既不是循環變量,也不用于收集計算結果,針對每次指派,建立一個獨立的、對應的臨時變量。

float temp = 2 * (height + width);
System.out.println("temp = " + temp);
temp = height * width;
System.out.println("tem = " + temp);      

修改成:

float temp = 2 * (height + width);
System.out.println("temp = " + temp);
float area = height * width;
System.out.println("area = " + area);      

1.動機

A.臨時變量有各種用途,某些變量自然會導緻重複指派。循環變量和結果收集變量就是兩個典型例子:循環變量會随着每次循環而發生改變,結果收集變量負責将“通過整個函數運作”的結果收集起來。除了這兩種情況外,還有很多臨時變量用于儲存一段備援代碼的計算結果,以便稍後使用。這種臨時變量應該隻被指派一次。如果被指派超過一次,就意味着在程式中它承擔了多個責任,它就應該被替換成多個臨時變量,每個變量隻承擔一個責任。同一個臨時變量承擔兩件不同的事情,會使程式閱讀者糊塗;

2.做法

A.在待分解的臨時變量聲明及第一次指派處,修改其名稱;

B.将新的臨時變量聲明為final;

C.以該臨時變量的第二次指派動作為界,修改此前對該臨時變量的所有引用點,讓他們信用新的臨時變量;

D.在第二次指派處,重新聲明原先的那個臨時變量;

E.編譯、測試

F.重複上述過程,每次都在聲明處修改變量名稱,并修改下次指派之前的引用點;

七.Remove Assignments To Parameters(移除對參數的指派)

如果程式中對一個參數指派,那麼就用一個臨時變量取代該參數的位置。

private void test(int count,double price){
    if (count > 10){
        price = price * 0.9;
    }
}      

改成

private void test(int count,double price){
    double result = price;
    if (count > 10){
        result = result * 0.9;
    }
}      

1.動機

A.避免降低代碼清晰度,如果隻把參數當作被傳遞進來的東西會清晰很多

B.混淆了java按值傳遞和引用傳遞,前者對參數的任何修改,都不會對調用段産生影響;

2.做法

A.建立一個臨時變量,把待處理的參數值指派給它;

B.以“對此參數指派”為界,把界限之後對參數的所有引用點替換成對此臨時變量的調用;

C.編譯、測試

八.Replace Method with Method Object(以函數對象替代函數)

你有一個大型函數,由于局部變量過多使你無法采用Extract Method(提煉方法),将這個函數放到一個單獨對象中,如此局部變量就變成了對象内的字段,然後你就可以在同一個對象中将這個大型函數分解成多個小型函數;

1.動機

A.對付局部變量,可以采用Inline Temp和Replace Temp with Method,但是如果函數過大,有時候這兩種方式都解決不了問題。此時,應該考慮用對象的方式,這樣可以把所有局部變量變成函數對象的變量,然後可以對此函數使用Extract Method方法提煉成一個個小函數;

2.做法

A.建立一個新類,根據待分解函數的用途為此對象命名;

B.在新類中建立一個final字段,用以儲存原大型函數所在對象,稱為源對象。同時,針對原函數的每個參數和臨時變量,在新類中建立一個對應字段儲存;

C.在新類中建立一個構造函數,用以接受原函數所在對象及所有參數;

D.在新類中建立一個compute()函數;

E.将原函數的所有代碼複制到compute中,如果需要調用源對象的任何參數,請通過源對象調用;

F.編譯

G.将就函數的函數本體替換成這樣一句話“建立上述新類的一個新對象,而後調用其中的compute()函數”

九.Substitute Algorithm(替換算法)

你要想把某個算法替換成另外一個算法,将函數本體替換為另外一個方法

private String findPerson(String[] persons){
    for (int i = 0;i < persons.length;i++){
        if(persons[i].equals("張三")){
            return "張三";
        }else if (persons[i].equals("李四")){
            return "李四";
        }else if (persons[i].equals("王五")){
            return "王五";
        }
    }
    return "";
}      

替換成

private String findPerson(String[] persons){
    List candidates = Arrays.asList(new String[]{"張三","李四","王五"});
    for (int i = 0;i < persons.length;i++){
       if(candidates.contains(persons[i])){
           return persons[i];
       }
    }
    return "";
}      

1.動機

A.解決問題用多種方法,總有某些方法會比較容易,當你覺得目前的算法邏輯有更好的取代方案時,那你就替換上就好了

2.做法

A.準備好另外一個算法,并測試通過編譯

B.針對現有測試,替換上述新算法,如果結果與之前一緻,重構結束;如果不一緻,在測試和調試過程中,以就算法為比較參考标準;