《重構---改善既有代碼的設計》之從新組織函數

那有什麼天生如此,只是咱們每天堅持。算法

本篇文章主要講解 《重構---改善既有代碼的設計》 這本書中的 第六章從新組織函數中 的知識點,app

將現有的函數從新分解是 進行大型優雅重構的第一步!函數

內聯函數

問題:一個函數的本體與名稱一樣清楚易懂測試

解決:在函數調用點插入函數本體,而後移除該函數this

//重構前  
public int getRating(){  
    return (moreThanSixLateDeliveries()) ? 2 : 1;  
}  
  
boolean moreThanSixLateDeliveries(){  
    return _numberOfLateDeliveries > 6;  
}
//重構後  
public int getRating(){  
    return (_numberOfLateDeliveries > 6) ? 2 : 1;  
}

動機

重構過程當中常常會以簡短的函數來表現動做意圖,這樣就使得代碼更清晰易讀。
但有時你會遇到某些函數,其內部代碼和函數名稱一樣清晰易讀。可能你重構了該函數,使得其內容和其名稱變得一樣清晰。果然如此,你就應該去掉這個函數,直接使用其中的代碼。間接性可能會帶來一些幫助,可是沒有必要的間接性老是讓人感受不舒服。設計

還有一種狀況是:你手上有一羣組織不甚合理的函數。
你能夠將它們都內聯到一個大型函數中,再從中提煉出組織合理的小型函數。比起既要移動一個函數、又要移動它所調用的其它全部函數,將整個大型函數做爲總體來移動會比較的簡單。code

若是你發現代碼中使用了太多的間接層,使得系統中的全部函數都彷佛只是對另外一個函數的簡單委託,形成對被些委託動做弄的暈頭轉向,這時一般也會使用內聯函數。orm

作法

(1)檢查函數,肯定其不具備多態性。(若是子類繼承了這個函數,就不要將此函數內聯,由於子類沒法複寫一個根本不存在的函數)。
(2)找出這個函數的全部被調用點。
(3)將這個函數的全部被調用點都替換爲函數本體。
(4)編譯,測試。
(5)一切正常後,刪除該函數的定義。對象

內聯函數看起來彷佛很簡單。但狀況每每那並不是如此。對於遞歸調用、內聯至另外一個對象中而該對象並沒有提供訪問函數......每一種狀況都會很複雜。不介紹複雜情形是由於:若是你遇到了這樣複雜的情形,那麼就不應運用這種重構手法。繼承

內聯臨時變量

問題:你有一個臨時變量,只被一個簡單表達式賦值一次,而它妨礙了其它的重構手法

解決:將全部對該變量的引用動做,替換爲對它賦值的那個表達式自身。

//重構前  
double basePrice = singleOrder.basePrice();  
return (basePrice > 1000)
//重構後  
return (singleOrder.basePrice() > 1000)

動機

內聯臨時變量多數狀況是做爲「以查詢取代臨時變量」的一部分來進行使用的,而真正的動機是出如今「以查詢取代臨時變量」中。
惟一單獨使用內聯臨時變量的狀況是:你發現某個臨時變量被賦予某個函數調用的返回值。通常來講,這樣的臨時變量是不會形成任何危害的,也能夠放心地放在那兒。
可是,若是這個臨時變量妨礙了其它的重構手法(例如提煉函數),你就應該將其內聯化。

作法

(1)檢查給臨時變量賦值的語句,確保等號右邊的表達式沒有反作用。
(2)若是這個臨時變量並未被聲明爲final,那就將它聲明爲final,而後編譯。(這能夠檢查該臨時變量是否真的只被賦值一次)
(3)找到該臨時變量全部引用點,將它們替換爲「爲臨時變量賦值」的表達式
(4)每次修改後,編譯並測試。
(5)修改完後全部引用點後,刪除該臨時變量的聲明和賦值語句。
(6)編譯,測試

引入解釋性變量

問題:你有一個複雜的表達式

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

//重構前  
if((platform.toUpperCase().indexOf("MAC") > -1) &&  
    (browser.toUpperCase().indexOf("IE") > -1) &&  
    wasInitialized() && resize > 0)  
{  
    //do something  
}
//重構後  
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;  
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;  
final boolean wasResize = resize > 0;  
  
if(isMacOs && isIEBrowser && wasInitialized() && wasResize){  
    //do something  
}

動機

在某些狀況下,表達式可能很是的複雜以致於難以閱讀。這樣,臨時變量能夠幫助你將表達式分解爲比較容易管理的形式。

條件邏輯中,引入解釋性變量就顯得比較有價值:你能夠用這項重構將每一個子句提煉出來,以一個良好命名的臨時變量來解釋對應條件子句的意義。另外一種可能的狀況是,對於那些比較長的算法,能夠運用臨時變量來解釋每一步運算的意義

本文的重構手法是比較常見的手法之一,可是對其的使用又不是那麼的多。由於通常狀況下,咱們均可以使用提煉函數來解釋一段代碼的意義。畢竟臨時變量只有在它所處的那個函數中才有意義,侷限性較大,函數則能夠在對象的整個生命週期中都有用,而且可被其它對象使用。可是,當局部變量使用提煉函數難以進行時,就能夠嘗試使用引入解釋性變量。

作法

(1)聲明一個final型的臨時變量,將待分解之複雜表達式中的一部分動做的運算結果賦值給它。
(2)將表達式中的「運算結果」這一部分,替換爲上述的臨時變量。(若是被替換的這一部分在代碼中重複出現,能夠每次一個,逐一進行替換)
(3)編譯,測試。
(4)重複上述過程,處理其它相似部分。

示例

//重構前  
double price(){  
    // 價格 = basePrice - quantity discount + shipping  
    return _quantity * _itemPrice -  
            Math.max(0, _quantity - 800) * _itemPrice * 0.15 +  
            Math.min(_quantity * _itemPrice * 0.25, 100);  
}

這段代碼仍是比較簡單,不過如今要讓其更加容易理解一些。
首先發現底價(basePrice)等於數量(quantity)乘以單價(item price)。因而能夠把這一部分的計算結果放進一個臨時變量中,同時將Math.min()函數中參數進行一樣替換。

double price(){  
    // 價格 = basePrice - quantity discount + shipping  
    final double basePrice = _quantity * _itemPrice;  
    return basePrice -  
            Math.max(0, _quantity - 800) * _itemPrice * 0.15 +  
            Math.min(basePrice * 0.25, 100);  
}

而後,將批發折扣(quantity discount)的計算提煉出來,並將運算結果賦予臨時變量。

double price(){  
    // 價格 = basePrice - quantity discount + shipping  
    final double basePrice = _quantity * _itemPrice;  
    final double quantityDiscount = Math.max(0, _quantity - 800) * _itemPrice * 0.15;  
    return basePrice -quantityDiscount+  
            Math.min(basePrice * 0.25, 100);  
}

最後,再把搬運費(shipping)計算提煉出來,並將運算結果賦予臨時變量。

//重構後  
double price(){  
    // 價格 = basePrice - quantity discount + shipping  
    final double basePrice = _quantity * _itemPrice;  
    final double quantityDiscount = Math.max(0, _quantity - 800) * _itemPrice * 0.15;  
    final double shipping = Math.min(basePrice * 0.25, 100);  
    return basePrice - quantityDiscount + shipping;  
}

運用提煉函數處理

對於上述代碼,一般不以臨時變量來解釋其動做意圖,而是更喜歡使用提煉函數。

//重構前  
double price(){  
    // 價格 = basePrice - quantity discount + shipping  
    return _quantity * _itemPrice -  
            Math.max(0, _quantity - 800) * _itemPrice * 0.15 +  
            Math.min(_quantity * _itemPrice * 0.25, 100);  
}

如今把底價計算提煉到一個獨立的函數中。

double price(){  
    // 價格 = basePrice - quantity discount + shipping  
    return basePrice() -  
            Math.max(0, _quantity - 800) * _itemPrice * 0.15 +  
            Math.min(basePrice() * 0.25, 100);  
}  
  
private double basePrice(){  
    return _quantity * _itemPrice;  
}

繼續進行提煉,每次提煉一個新的函數。最後獲得代碼以下。

//重構後  
double price(){  
    // 價格 = basePrice - quantity discount + shipping  
    return basePrice() - quantityDiscount() + shipping();  
}  
  
private double basePrice(){  
    return _quantity * _itemPrice;  
}  
  
private double shipping(){  
    return Math.min(basePrice() * 0.25, 100);  
}  
  
private double quantityDiscount(){  
    return Math.max(0, _quantity - 800) * _itemPrice * 0.15;  
}

分解臨時變量

問題:你的程序有某個臨時變量被賦值超過一次,它既不是循環變量,也不被用於收集計算結果

解決:針對每次賦值,創造一個獨立、對應的臨時變量。

//重構前  
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);

動機

在某些狀況下,臨時變量用於保存一段冗長代碼的運算結果,以便稍後使用。
這種臨時變量應該只被賦值一次。
若是它被賦值超過一次,就意味着它們在函數中承擔了一個以上的責任。
若是臨時變量承擔多個責任,它就應該被替換(分解)爲多個臨時變量,使得每個變量只承擔一個責任
同一個臨時變量承擔兩件不一樣的事情,會讓代碼閱讀者糊塗。

作法

(1)在待分解臨時變量的聲明及第一次被賦值處,修改其名稱。
(2)將新的臨時變量聲明爲final。
(3)以該臨時變量的第二次賦值動做爲界,修改此前對該臨時變量的全部引用點,讓它們引用新的臨時變量。
(4)在第二次賦值處,從新聲明原先那個臨時變量。
(5)編譯,測試。
(6)逐次重複上述過程。每次都在聲明處對臨時變量更名,並修改下次賦值以前的引用點。

示例

咱們從一個簡單計算開始:咱們須要計算一個蘇格蘭布丁運動的距離。在起點處,靜止的布丁會受到一個初始力的做用而開始運動。一段時間後,第二個力做用於布丁,讓它再次加速。根據牛頓第二定律,計算布丁運動距離:

 牛頓第二定律 
內容:物體的加速度與所受合外力成正比,跟物體的質量成反比。
表達式:F=ma。   
  物理意義:反映物體運動的加速度大小、方向與所受合外力的關係,且這種關係是瞬時的。

double getDistance(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;  
}

代碼看起來好像有點醜陋。觀察例子中的acc變量是如何被賦值兩次。
acc變量有兩個責任,一是保存第一個力產生的加速度;二是保存兩個力共同產生的加速度。這就是須要分解的東西。

首先,在函數開始修改處修改這個臨時變量的名稱,並將新的臨時變量聲明爲final。而後,把第二次賦值以前對acc變量的全部引用點,所有改用心的臨時變量。最後,在第二次賦值處從新聲明acc變量。

double getDistance(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;  
        double acc = (_primaryForce + _secondaryForce) / _mass;  
        result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;  
    }  
    return result;  
}

新的臨時變量指出,它只承擔原先acc變量的第一個責任。
將它聲明爲final,確保它只被賦值一次
而後,在原先acc變量第二次被賦值處從新聲明acc。
如今,從新編譯並測試,一切都沒有問題。
而後繼續處理acc臨時變量的第二次賦值

double getDistance(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;  
}

以查詢取代臨時變量 手法進行重構

//「以查詢取代臨時變量」手法進行重構  
double getDistance(int time){  
    double result= 0.5 * getPrimaryAcc() * getPrimaryTime(time) * getPrimaryTime(time);  
    if(getSecondaryTime(time) > 0){  
        result += getSeconddistance();  
    }  
    return result;  
}  
  
private double getPrimaryAcc(){  
    return _primaryForce / _mass;  
}  
  
private double getSecondaryAcc(){  
    return (_primaryForce + _secondaryForce) / _mass;  
}  
  
private int getPrimaryTime(int time){  
    return Math.min(time, _delay);  
}  
  
private int getSecondaryTime(int time){  
    return time - _delay;  
}  
  
private double getSeconddistance(){  
    return getPrimaryAcc() *_delay * getSecondaryTime(time)   
            + 0.5 * getSecondaryAcc() * getSecondaryTime(time) * getSecondaryTime(time);  
}

移除對參數的賦值

問題:代碼對一個參數進行賦值
解決:以一個臨時變量取代該參數的位置。

//重構前  
int dicount(int inputVal, int quantity, int yearToDate){  
    if(inputVal > 50) inputVal-=10;  
}
//重構後  
int dicount(final int inputVal, int quantity, int yearToDate){  
    int result = inputVal;  
    if(result > 50) result-=10;  
}

動機

我想你很清楚「對參數賦值」這個說話的意思。
若是把一個名稱爲fool的對象做爲參數傳遞給某個函數,那麼「對參數賦值」意味改變fool,使它引用另外一個對象
可是,若是在「被傳入對象」身上進行什麼操做,那沒問題,咱們常常會這麼作。
這裏只針對「fool被改變而指向另外一個對象」這種狀況來討論:

void test(Object fool){  
    fool.changedBySomeWay(); //that's ok  
    fool=anotherObject; //trouble will appear  
}

咱們之所不這樣作,是由於它下降了代碼的清晰度,並且混用了按值傳遞和按引用傳遞這兩種參數傳遞方式。

JAVA只採用按值進行傳遞

在按值傳遞的狀況下,對參數的任何修改,都不會對調用端形成任何影響
若是你只以參數表示「被傳遞進來的東西」,那麼代碼會清晰的多,由於這種用法在全部語言中都表現出相同的語義。

在JAVA中,通常不要對參數賦值:若是你看到手上的代碼已經這麼作了,就應該使用本文的方法。

作法

(1)創建一個臨時變量,把待處理的參數值賦賦予它。
(2)以「對參數的賦值」爲界,將其後全部對此參數的引用點,所有替換爲「對此臨時變量的引用」。
(3)修改賦值語句,使其改成對新建之臨時變量賦值。
(4)編譯,測試。
(若是代碼的語義是按照引用傳遞的,需在調用端檢查調用後是否還使用了這個參數。也要檢查有多少個按引用傳遞的參數被賦值後又被使用。應該儘可能以return方式返回一個值。若是返回值有多個,可考慮將需返回的一大堆數據變爲對象,或者爲每一個返回值設定一個獨立的函數)

示例

int dicount(int inputVal, int quantity, int yearToDate){  
    if(inputVal > 50) inputVal-=5;  
    if(quantity > 100) quantity-=10;  
    if(yearToDate > 1000) yearToDate-=100;  
    return inputVal;  
}

以臨時變量取代對參數的賦值動做,獲得下列代碼:

int dicount(int inputVal, int quantity, int yearToDate){  
    int result  = inputVal;  
    if(result > 50) result-=5;  
    if(quantity > 100) quantity-=10;  
    if(yearToDate > 1000) yearToDate-=100;  
    return result;  
}

能夠爲參數加上final關鍵詞,強制其遵循「不對參數賦值」這一慣例:

int dicount(final int inputVal, final int quantity, final int yearToDate){  
    int result  = inputVal;  
    if(result > 50) result-=5;  
    if(quantity > 100) quantity-=10;  
    if(yearToDate > 1000) yearToDate-=100;  
    return result;  
}

JAVA的按值傳遞

咱們應該都知道,JAVA使用按值傳遞的函數調用方式,這經常也會使你們迷惑。在全部地點,JAVA都會遵循嚴格按值傳遞:

//JAVA按值的傳遞  
class Params{  
    public static void main(String[] args) {  
        int x = 10;  
        triple(x);  
        System.err.println("x after triple:" + x);  
    }  
      
    private static void triple(int arg){  
        arg = arg * 3;  
        System.err.println("arg in triple:" +arg );  
    }  
}  
//輸出  
//arg in triple:30   
//x after triple:10

上面代碼是使用基本數據類型進參數傳遞,還不至於讓人糊塗。但若是參數中傳遞的是對象,就可能把人弄糊塗。若是在程序中以Date對象表示日期,下列程序所示:

//以對象爲參數  
class Params{  
    public static void main(String[] args) {  
        Date d1 = new Date(2015,1,1);  
        nextDateUpdate(d1);  
        System.err.println("d1 after nextday:" + d1);  
          
        Date d2 = new Date(2015,1,1);  
        nextDateReplace(d2);  
        System.err.println("d2 after nextday:" + d2);//61380864000000  
          
    }  
  
    private static void nextDateUpdate(Date d) {  
        d.setDate(d.getDate()+1);  
        System.err.println("arg in nextday d1 : "+d);  
    }  
  
    private static void nextDateReplace(Date d) {  
        d = new Date(d.getYear(),d.getMonth(),d.getDate()+1);  
        d=null;  
        System.err.println("arg in nextday d2: "+d);  
    }  
}  
  
//輸出  
/* 
 arg in nextday d1 : Tue Feb 02 00:00:00 CST 3915 
 d1 after nextday:   Tue Feb 02 00:00:00 CST 3915 
 arg in nextday d2:  Tue Feb 02 00:00:00 CST 3915 
 d2 after nextday:   Mon Feb 01 00:00:00 CST 3915 
 */

從本質上說,對象的引用是按值傳遞的。由於能夠修改參數對象的內部狀態,但對參數對象從新賦值是沒有意義的。

以函數對象取代函數

問題:你有一個大型函數,其中對局部變量的使用使你沒法採用「提煉函數」這種重構手法

解決:將這個函數放進一個單獨對象中,這樣,局部變量就成了對象的字段,而後就能夠在同一個對象中將這個大型函數分解爲多個小型函數。

//重構前  
class Order....  
    double price(){  
        double basePrice;  
        double secondaryPrice;  
        double thirdaryPrice;  
        //compute()  
        ......  
}
//重構後  
class Order...  
    double price(){  
        return new PriceCalculator(this).compute();  
}  
  
class PriceCalculator{  
    double basePrice;  
    double secondaryPrice;  
    double thirdaryPrice;  
      
    double compute(){  
        //...  
    }  
}

動機

在前面的文章中一直在強調小型函數的優美動人。
只要將相對獨立的代碼從大函數提煉出來,就能夠大大提升代碼的可讀性。

可是局部變量的存在會增長函數分解的難度
若是一個函數中的局部變量氾濫成災,那麼想分解這個函數是很是困難的。
「以查詢替換臨時變量」手法能夠幫助減輕負擔,但有時候仍是會發現根本沒法拆解一個須要拆解的函數。

這種狀況就應該考慮使用函數對象來解決。本文的重構方法會將全部的局部變量都變成函數對象的字段。而後就可使用「提煉函數」創造新的函數,從而將原來的大型函數拆解變小。

作法

(1)創建一個新的類,根據待處理函數的用途爲其命名。
(2)在新類中創建一個final字段,用以保存原先大型函數所在對象。針對原函數的每一個臨時變量和每一個參數,在新類中創建一個對應的字段保留之。
(3)在新類中創建一個構造函數,接收原對象以原函數的全部參數做爲其參數。
(4)在新類中創建一個compute()函數。
(5)將原函數的代碼複製到compute()函數中。若是須要調用源對象的任何函數,經過原對象字段調用。
(6)編譯,測試。
因爲全部局部變量如今都成了字段,因此你能夠任意分解這個大型函數,沒必要傳遞任何參數。

示例

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

爲了把這個函數變爲函數對象,首先須要聲明一個新類。在新類中提供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;

接下來,加入一個構造函數。

Gamm(Account source, int inputVal, int quantity, int year2Date){  
    this._account = source;  
    this.value = inputVal;  
    this.quantity = quantity;  
    this.year2Date = year2Date;  
}

如今能夠把本來函數搬到compute()中了。函數中任何調用Accout類的地方,都必改用_account字段。

int compute(){  
    importValue1 = (value * quantity) + _account.delta();  
    importValue2 = (value * year2Date) + 200;  
    if(year2Date - importValue1 >200)  
        importValue2-=50;  
    importValue3 = importValue2 * 8;  
    //......  
      
    return importValue3 - 2 * importValue1;  
}

而後,修改舊函數,讓它將工做委託給剛完成的這個函數對象。

int gamm(int value, int quantity, int year2Date){  
    return new Gamm(this,value,quantity,year2Date).compute();  
}

以上就是本文重構方法的基本原則。其所帶來的好處是:如今能夠輕鬆地對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;  
}

主要介紹了重構手法——以函數對象取代函數。咱們都不喜歡臨時變量巨多的方法,那隻會讓咱們迷惑。對於局部變量不少的函數,有必要運用本文的重構方法進行處理,將其轉化爲函數對象,那樣就把臨時變量轉爲函數對象的字段,繼而能夠進行其它重構方法。

相關文章
相關標籤/搜索