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