重構 - 改善既有程式的設計 - Large Method
April 09, 2020這篇文章討論《重構 - 改善既有程式的設計》裡的3.2 - Large Method
圖片以及程式碼來源自重構 - 改善既有程式的設計
過長函式
一個函式不應該太長 一般來說超過十行就很值得考慮重構
起因
寫程式比讀程式簡單得多 所以通常一有新增需求 人們就直接把程式碼加在原本的函式裡
所以函式越變越大 味道越變越怪
解法
你應該更積極的去分解函式 有一個原則可以遵循:
當你感覺需要註釋來說明什麼的時候 你就把這區塊拉近另一個函式 並以用途(非實作方法)來命名新函式 即使只是一行的程式 只要函式名稱可以清楚的說明用途 就該這麼做
要不要分離出新的函式的判斷基準 不在於函式的長度夠不夠長 而是這個函式 做什麼 和 如何做的semantic distance(語意距離)
“做什麼” 是比 “如何做” 還要高一層的抽象 所以如果這兩者的語意距離很遠 那就應該分離出個新的函式
好 開始聊常見的解法
Decompose Conditional 分解條件式
把你複雜的if-else 條件判斷分離成小函式
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
}
else {
charge = quantity * summerRate;
}
變成這樣
if (isSummer(date)) {
charge = summerCharge(quantity);
}
else {
charge = winterCharge(quantity);
}
Extract Method
void printOwing(double amount) {
printBanner();
//print details
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
當你需要特別寫註解 就很值得分離出來 把註解的內容當作函式名稱
void printOwing(double amount) {
printBanner();
printDetails(amount);
}
void printDetails (double amount) {
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
但你仔細看會看到 當你需要Extract Method的時候 常常需要把local variable(amount)一起傳進那個新的函式 這樣子可讀性可能也很難提升
所以我們可以用Replace Temp with Query, Introduce Parameter Object 和 Preserve Whole Object來讓程式簡潔一點
Replace Temp with Query
原本你需要一個區域變數
double calculateTotal() {
double basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
}
else {
return basePrice * 0.98;
}
}
現在你用一個函式取代
double calculateTotal() {
if (basePrice() > 1000) {
return basePrice() * 0.95;
}
else {
return basePrice() * 0.98;
}
}
double basePrice() {
return quantity * itemPrice;
}
為什麼要這樣呢 因為暫時性的區域變數就只是出現一下 如果一個類別裡有很多地方要用 會導致你的類別有很多很長的函式 這麼做之後 類別裡的所有人都可以直接拿到這個值
所以在我們Extract Method的例子裡 我們就不用把amount
傳來傳去 我們就可以在printDetails()
裡呼叫getAmount()
Introduce Parameter Object
如果參數間的關係很強 可以把參數們包成一個參數物件
Preserve Whole Object
如果你把一個物件裡的若干資料拿出來傳給別人
int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);
那你不如整個物件傳進去
boolean withinPlan = plan.withinRange(daysTempRange);
好處是參數列更穩定 如果你想要多傳一個物件裡的參數 你公開的interface甚至不用改
壞處就是增加了兩個函式之間的dependency
大招: Replace Method with Method Object
如果以上的方式都不行 代表說你有太多的區域變數需要傳來傳去 讓你很難Extract Method 也可能是你Extract Method之後程式碼的好讀性也沒上升
大招就是 把這個函式放進一個單獨物件之中 這樣的話每個區域變數就變成物件之內的欄位(fields) 那之後你愛怎麼用都行
來個例子
class Order {
// ...
public double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// A lot of computation.
// Hard to refactor because the above three parameters are passed back and forth
}
}
因為區域變數太多 傳來傳去很醜 我們就直接多加一個類別PriceCalculator
class Order {
// ...
public double price() {
return new PriceCalculator(this).compute();
}
}
class PriceCalculator {
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;
public PriceCalculator(Order order) {
// Initialize primaryBasePrice/secondaryBasePrice/tertiaryBasePrice
}
public double compute() {
// Do whatever you want without passing parameters
}
}
有梗吧