那有什麼天生如此,只是咱們每天堅持。算法
本篇文章主要講解 《重構---改善既有代碼的設計》 這本書中的 第六章從新組織函數中 的知識點,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; }
主要介紹了重構手法——以函數對象取代函數。咱們都不喜歡臨時變量巨多的方法,那隻會讓咱們迷惑。對於局部變量不少的函數,有必要運用本文的重構方法進行處理,將其轉化爲函數對象,那樣就把臨時變量轉爲函數對象的字段,繼而能夠進行其它重構方法。