<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • 代碼重構:函數重構的 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]
        }
      }
    }
    

    總結

    建議:

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

    參考文獻:

    posted @ 2021-10-09 18:33  小二十七  閱讀(498)  評論(0編輯  收藏  舉報
    国产美女a做受大片观看