代碼重構:函數重構的 7 個小技巧


代碼重構:函數重構的 7 個小技巧

重構的范圍很大,有包括類結構、變量、函數、對象關系,還有單元測試的體系構建等等。

在這一章,我們主要分享重構函數的 7 個小技巧。🧰

在重構的世界里,幾乎所有的問題都源於過長的函數導致的,因為:

  • 過長的函數包含太多信息,承擔太多職責,無法或者很難復用
  • 錯綜復雜的邏輯,導致沒人願意去閱讀代碼,理解作者的意圖

對於過長函數的處理方式,在 《重構》中作者推薦如下手法進行處理:

1:提煉函數

示例一

我們看先一個示例,原始代碼如下:

void printOwing(double amout) {
  printBanner();
  // Print Details
  System.out.println("name:" + _name);
  System.out.println("amount:" + _amount);
}

Extract Method 的重構手法是將多個 println() 抽離到獨立的函數中(函數需要在命名上,下點功夫),這里對抽離的函數命名有 2 個建議:

  • 保持函數盡可能的小,函數越小,被復用的可能性越大
  • 良好的函數命名,可以讓調用方的代碼看起來上注釋(結構清晰的代碼,其實並不是很需要注釋)

將 2 個 println() 方法抽離到 printDetails() 函數中:

void printDetails(double amount) {
  System.out.println("name:" + _name);
  System.out.println("amount:" + _amount);
}

當我們擁有 printDetails() 獨立函數后,那么最終 printOwing() 函數看起來像:

void printOwing(double amout) {
  printBanner();
  printDetails(double amount);
}

示例二

示例一可能過於簡單,無法表示 Extract Method 的奇妙能力,我們通過一個更復雜的案例來表示,代碼如下:

void printOwing() {
  Enumeration e = _orders.elements();
  double oustanding = 0.0

  // print banner
  System.out.println("*******************")
  System.out.println("***Customer Owes***")
  System.out.println("*******************")

  // calculate outstanding
  while(e.hasMoreElements()){
    Order each = (Order)e.nextElement();
    outstanding += each.getAmount();
  }

  // print details
  System.out.println("name:" + _name);
  System.out.println("amount:" + outstanding); 
}

首先審視一下這段代碼,這是一段過長的函數(典型的糟糕代碼的代表),因為它企圖去完成所有的事情。但通過注釋我們可以將它的函數提煉出來,方便函數復用,而且 printOwing() 代碼結構也會更加清晰,最終版本如下:

void printOwing(double previousAmount) {
  printBaner();   // Extract print banner
  double outstanding = getOutstanding(previousAmount * 1.2)   // Extract calculate outstanding
  printDetails(outstanding)   // print details
}

printOwing() 看起來像注釋的代碼,對於閱讀非常友好,然后看看被 Extract Method 被提煉的函數代碼:

void printBanner() {
  System.out.println("*******************")
  System.out.println("***Customer Owes***")
  System.out.println("*******************")  
}

double getOutstanding(double initialValue) {
  double result = initialValue;   // 賦值引用對象,避免對引用傳遞
  Enumeration e = _orders.elements();
  while(e.hasMoreElements()){
    Order each = (Order)e.nextElement();
    result += each.getAmount();
  }
  return result;
}

void printDetails(double outstanding) {
  System.out.println("name:" + _name);
  System.out.println("amount:" + outstanding); 
}

總結

提煉函數是最常用的重構手法之一,就是將過長函數按職責拆分至合理范圍,這樣被拆解的函數也有很大的概率被復用到其他函數內

2:移除多余函數

當函數承擔的職責和內容過小的時候,我們就需要將兩個函數合並,避免系統產生和分布過多的零散的函數

示例一

假如我們程序中有以下 2 個函數,示例程序:

int getRating() {
  return (moreThanFiveLateDeliveries()) ? 2 : 1;
}

boolean moreThanFiveLateDeliveries() {
  return _numberOfLateDeliveries > 5;
}

moreThanFiveLateDeliveries() 似乎沒有什么存在的必要,因為它僅僅是返回一個 _numberOfLateDeliveries 變量,我們就可以使用 Inline Method 內聯函數 來重構它,修改后的代碼如下:

int getRating() {
  return (_numberOfLateDeliveries > 5) ? 2 : 1;
}

注意事項:

  • 如果 moreThanFiveLateDeliveries() 已經被多個調用方引用,則不要去修改它

總結

Inline Method 內聯函數 就是邏輯和職責簡單的,並且只被使用 1 次的函數進行合並和移除,讓系統整體保持簡單和整潔

3:移除臨時變量

先看示例代碼:

示例一

double basePrice = anOrder.basePrice();
return basePrice > 1000;

使用 Inline Temp Variable 來內聯 basePrice 變量,代碼如下:

return anOrder.basePrice() > 1000;

總結

如果函數內的臨時變量,只被引用和使用一次,那么它就應該被內聯和移除,避免產生過多冗余代碼,從而影響閱讀

4:函數替代表達式

如果你的程序依賴一段表達式來進行邏輯判斷,那么你可以利用一段函數封裝表達式,來讓計算過程更加靈活的被復用

示例一

double basePrice = _quantity * _itemPrice;
if (basePrice > 1000) {
  return basePrice * 0.95;
} else {
  return basePrice * 0.98;
}

在示例一,我們可以把 basePrice 的計算過程封裝起來,這樣其他函數調用也更方便,重構后示例如下:


if (basePrice() > 1000) {
  return basePrice() * 0.95;
} else {
  return basePrice() * 0.98;
}

// 抽取 basePrice() 計算過程
double basePrice() {
  return _quantity * _itemPrice;
}

以上程序比較簡單,不太能看出函數替代表達式的效果,我們換一個更負責的看看,先看一段獲取商品價格的程序:

double getPrice() {
  final int basePrice = _quantity * _itemPrice;
  final double discountFactor;
  if (basePrice > 1000) {
    discountFactor = 0.95;
  } else {
    discountFactor = 0.98;
  }
  return basePrice * discountFactor;
}

如果我們使用 函數替代表達式 的重構手法,那么程序最終讀起來可能就像:

double getPrice() {
  // 讀起來像不像注釋 ? 這里的代碼還需要寫注釋嗎?
  return basePrice() * discountFactor();
}

至於 basePrice()、discountFactor() 是怎么拆解的,這里回憶一下 提煉函數 的內容,以下放出提煉的代碼:

int basePrice() {
  return _quantity * _itemPrice;
}

double discountFactor() {
  final double discountFactor;
  return basePrice() > 1000 ? 0.95 : 0.98;
}

總結

使用函數替代表達式替代表達式,對於程序來說有以下幾點好處:

  1. 封裝表達式的計算過程,調用方無需關心結果是怎么計算出來的,符合 OOP 原則
  2. 當計算過程發生改動,也不會影響調用方,只要修改函數本身即可

5:引入解釋變量

當你的程序內部出現大量晦澀難懂的表達式,影響到程序閱讀的時候,你需要 引入解釋變量 來解決這個問題,不然代碼容易變的腐爛,從而導致失控。另外引入解釋變量也會讓分支表達式更好理解。

示例一

我們先看一段代碼(我敢保證這段代碼你看的肯定會很頭疼。。。💆)

if (platform.tpUpperCase().indexOf("MAC") > -1 && browser.toUpperCase().indexOf("IE") > -1 && 
wasInitialized() && resize > 0) {
    // do something ....
}

使用 引入解釋變量 的方法來重構它的話,會讓你取起來有不同的感受,代碼如下:

final boolean isMacOs = platform.tpUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;

if (isMacOs && isIEBrowser && wasInitialized() && wasResized()) {
  // do something ...
}

這樣做還有一個好處就是,在 Debug 程序的時候你可以提前知道每段表達式的結果,不必等到執行到 IF 的時候再推算

示例二

其實 引入解釋變量 ,只是解決問題的方式之一,復習我們剛才提到的 提煉函數也能解決這個問題,我們再來看一段容易引起生理不適的代碼 😤:

double price() {
// 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);
}

我們使用 Extract Method 提煉函數處理代碼后,那么它讀起來就像是這樣:

double price() {
  return basePrice() - quantityDiscount() + shipping();
}

有沒有感受到什么叫好的代碼就像好的文章?👩‍🌾 這樣的代碼根本不用寫注釋了,當然把被提煉的函數也放出來:

private double quantityDiscount() {
  return Math.max(0, _quantity - 500) * _itemPrice * 0.05;
}

private double shipping() {
  return Math.min(_quantity * _itemPrice * 0.1, 100.0);
}

private double basePrice() {
  return (_quantity * _itemPrice);
}

總結

當然大多數場景是可以使用 Extract Method 提煉函數來替代引入解釋變量來解決問題,但這並不代表 引入解釋變量 這種重構手法就毫無用處,我們還是可以根據一些特定的場景來找到它的使用場景:

  • 當 Extract Method 提煉函數使用成本比較高,並且難以進行時……
  • 當邏輯表達式過於復雜,並且只使用一次的時候(如果會被復用,推薦使用 提煉函數 方式)

6:避免修改函數參數

雖然不同的編程語言的函數參數傳遞會區分:“按值傳遞”、“按引用傳遞”的兩種方式(Java 語言的傳遞方式是按值傳遞),這里不就討論兩種傳遞方式的區別,相信大家都知道。

示例一

我們不應該直接對 inputVal 參數進行修改,但是如果直接修改函數的參數會讓人搞混亂這兩種方式,如下以下代碼:

int discount (int inputVal) {
  if (inputVal > 50) {
    intputVal -= 2;
  }
  return intputVal;
}

如果是在 引用傳遞 類型的編程語言里,discount() 函數對於 intputVal 變量的修改,甚至還會影響到調用方。所以我們正確的做法應該是使用一個臨時變量來處理對參數的修改,代碼如下:

int discount (int inputVal) {
  int result = inputVal;
  if (inputVal > 50) {
    result -= 2;
  }
  return result;
}

辯證的看待按值傳遞

眾所周知在按值傳遞的編程語言中,任何對參數的任何修改,都不會對調用端造成任何影響。但是如何不加以區分,這種特性依然會讓你感到困惑😴,我們先看一段正常的代碼:

public class Param {
    public static void main(String[] args) {
        int x = 5;
        triple(x);
        System.out.println("x after triple: " + x);
    }

    private static void triple (int arg) {
        arg = arg * 3;
        System.out.println("arg in triple: " + arg);
    }
}

這段代碼不容易引起困惑,習慣按值傳遞的小伙伴,應該了解它的輸出會如下:

arg in triple: 15
x after triple: 5

但是如果函數的參數是對象,你可能就會覺得困惑了,我們再看一下代碼,把函數對象改為對象試試:

public class Param {
    public static void main(String[] args) {
        Date d1 = new Date("1 Apr 98");
        nextDateUpdate(d1);
        System.out.println("d1 after nextDay:" + d1);
        Date d2 = new Date("1 Apr 98");
        nextDateReplace(d2);
        System.out.println("d2 after nextDay:" + d2);
    }

    private static void nextDateUpdate(Date arg) {
        // 不是說按值傳遞嗎?怎么這里修改對象影響外部了。。
        arg.setDate(arg.getDate() + 1);;
        System.out.println("arg in nextDay: " + arg);
    }

    private static void nextDateReplace(Date arg) {
        // 嘗試改變對象的引用,又不生效。。what the fuck ?
        arg = new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
        System.out.println("arg in nextDay: " + arg);
    }
}

最終輸出如下,有沒有被弄的很迷糊 ?🤣:

arg in nextDay: Thu Apr 02 00:00:00 CST 1998
d1 after nextDay:Thu Apr 02 00:00:00 CST 1998
arg in nextDay: Thu Apr 02 00:00:00 CST 1998
d2 after nextDay:Wed Apr 01 00:00:00 CST 1998

總結

對於要修改的函數變量,乖乖的使用臨時變量,避免造成不必要的混亂

7:替換更優雅的函數實現

示例一

誰都有年少無知,不知天高地厚和輕狂的時候,那時候的我們就容易寫下這樣的代碼:

String foundPerson(String[] people) {
  for (int i = 0; i < perple.length; i++) {

    if (peole[i].equals("Trevor")) {
      return "Trevor";
    }
    if (peole[i].equals("Jim")) {
      return "Jim";
    }
    if (peole[i].equals("Phoenix")) {
      return "Phoenix";
    }

    // 弊端:如果加入新人,又要寫很多重復的邏輯和代碼
    // 這種代碼寫起來好無聊。。而且 CV 大法也容易出錯
  }
}

那時候我們代碼寫的不好,還不自知,但隨着我們的能力和經驗的增改,我們回頭看看自己的代碼,這簡直是一坨 💩 但是年輕人嘛,總歸要犯一些錯誤,佛說:知錯能改善莫大焉。現在我們變牛逼 🐂 了,對於曾經的糟糕代碼肯定不能不聞不問,所以的重構就是,在不更改輸入和輸出的情況下,給他替換一種更優雅的實現,代碼如下:

String foundPerson(String[] people) {
  // 加入新人,我們擴展數組就好了
  List condidates = Arrays.asList(new String[] {"Trevor", "Jim", "Phoenix"});
  // 邏輯代碼不動,不容易出錯
  for (int i = 0; i <= people.length; i++) {
    if (condidates.equals(people[i])) {
      return people[i]
    }
  }
}

總結

建議:

  • 在我們回顧曾經的代碼的時候,如果你有更好的實現方案(保證輸入輸出相同的前提下),就應該直接替換掉它
  • 記得通過單元測試后,再提交代碼(不想被人打的話)

參考文獻:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM