天天看點

重新組織你的函數之一 :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);

}

動機(Motivation)

Extract Method是我最常用的重構手法之一。當我看見一個過長的函數或者一段需要注釋才能讓人了解用途的代碼,我就會将這段代碼放進一個獨立函數中。

有數個原因造成我喜歡簡短而有良好命名的函數。首先,如果每個函數的粒度都很小(finely grained),那麼函數之間彼此複用的機會就更大;其次,這會使高層函數位讀起來就像一系列注釋;再者,如果函數都是細粒度,那麼函數的覆寫(overridden)也會更容易些。

的确,如果你習慣看大型函數,恐怕需要一段時間才能适應這種新風格。而且隻有當你能給小型函數很好地命名時,它們才能真正起作用,是以你需要在函數名稱下點功夫。人們有時會問我,一個函數多長才算合适?在我看來,長度不是問題,關鍵在于函數名稱和函數本體之間的語義距離(semantic distance )。如果提煉動作 (extracting )可以強化代碼的清晰度,那就去做,就算函數名稱比提煉出來的代碼 還長也無所謂。

作法(Mechanics)

· 創造一個新函數,根據這個函數的意圖來給它命名(以它「做什麼」來命名, 而不是以它「怎樣做」命名)。
Ø 即使你想要提煉(extract )的代碼非常簡單,例如隻是一條消息或一個函數調用,隻要新函數的名稱能夠以更好方式昭示代碼意圖,你也應該提煉它。但如果你想不出一個更有意義的名稱,就别動。
· 将提煉出的代^碼從源函數(source)拷貝到建立的目标函數(target)中。
· 仔細檢查提煉出的代碼,看看其中是否引用了「作用域(scope)限于源函數」的變量(包括局部變量和源函數參數)。
· 檢查是否有「僅用于被提煉碼」的臨時變量(temporary variables )。如果有,在目标函數中将它們聲明為臨時變量。
· 檢查被提煉碼,看看是否有任何局部變量(local-scope variables )的值被它改變。如果一個臨時變量值被修改了,看看是否可以将被提煉碼處理為一個查詢(query),并将結果指派給相關變量。如果很難這樣做,或如果被修改的 變量不止一個,你就不能僅僅将這段代碼原封不動地離煉出來。你可能需要先使用Split Temporary Variable,然後再嘗試提煉。也可以使用Replace Temp with Query 将臨時變量消滅掉(請看「範例」中的讨論)。
· 将被提煉碼中需要讀取的局部變量,當作參數傳給目标函數。
· 處理完所有局部變量之後,進行編譯。
· 在源函數中,将被提煉碼替換為「對目标函數的調用」。
Ø 如果你将任何臨時變量移到目标函數中,請檢查它們原本的聲明式是否在被提煉碼的外圍。如果是,現在你可以删除這些聲明式了。
· 編譯,測試。

範例(examples):無局部變量(No Local Variables)

在最簡單的情況下,Extract Method 易如反掌。請看下列函數:

  void printOwing() {

      Enumeration e = _orders.elements();

      double outstanding = 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);

  }

我們可以輕松提煉出「列印banner」的代碼。我隻需要剪切、粘貼、再插入一個函數調用動作就行了:

  void printOwing() {

      Enumeration e = _orders.elements();

      double outstanding = 0.0;

      printBanner();

      // 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);

  }

  void printBanner() {

      // print banner

      System.out.println ("**************************");

      System.out.println ("***** Customer Owes ******");

      System.out.println ("**************************");

  }

範例(Examples):有局部變量(Using Local Variables)

果真這麼簡單,這個重構手法的困難點在哪裡?是的,就在局部變量,包括傳進源函數的參數和源函數所聲明的臨時變量。局部變量的作用域僅限于源函數,是以當我使用Extract Method 時,必須花費額外功夫去處理這些變量。某些時候它們甚至可能妨礙我,使我根本無法進行這項重構。

局部變量最簡單的情況是:被提煉碼隻是讀取這些變量的值,并不修改它們。這種情況下我可以簡單地将它們當作參數傳給目标函數。是以如果我面對下列函數:

  void printOwing() {

      Enumeration e = _orders.elements();

      double outstanding = 0.0;

      printBanner();

      // 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);

  }

我就可以将「列印詳細資訊」這一部分提煉為「帶一個參數的函數」:

  void printOwing() {

      Enumeration e = _orders.elements();

      double outstanding = 0.0;

      printBanner();

      // calculate outstanding

      while (e.hasMoreElements()) {

          Order each = (Order) e.nextElement();

          outstanding += each.getAmount();

      }

      printDetails(outstanding);

  }

  void printDetails (double outstanding) {

      System.out.println ("name:" + _name);

      System.out.println ("amount" + outstanding);

  }

必要的話,你可以用這種手法處理多個局部變量。

如果局部變量是個對象,而被提煉碼調用了會對該對象造成修改的函數,也可以如法炮制。你同樣隻需将這個對象作為參數傳遞給目标函數即可。隻有在被提煉碼真的對一個局部變量指派的情況下,你才必須采取其他措施。

範例(Examples):對局部變量再指派(Reassigning a Local Variable)

如果被提煉碼對局部變量指派,問題就變得複雜了。這裡我們隻讨論臨時變量的問題。如果你發現源函數的參數被指派,應該馬上使用Remove Assignments to Parameters。

被指派的臨時變量也分兩種情況。較簡單的情況是:這個變量隻在被提煉碼區段中使用。果真如此,你可以将這個臨時變量的聲明式移到被提煉碼中,然後一起提煉出去。另一種情況是:被提煉碼之外的代碼也使用了這個變量。這又分為兩種情況: 如果這個變量在被提煉碼之後未再被使用,你隻需直接在目标函數中修改它就可以了;如果被提煉碼之後的代碼還使用了這個變量,你就需要讓目标函數傳回該變量改變後的值。我以下列代碼說明這幾種不同情況:

   void printOwing() {

       Enumeration e = _orders.elements();

       double outstanding = 0.0;

       printBanner();

       // calculate outstanding

       while (e.hasMoreElements()) {

           Order each = (Order) e.nextElement();

           outstanding += each.getAmount();

       }

       printDetails(outstanding);

   }

現在我把「計算」代碼提煉出來:

   void printOwing() {

       printBanner();

       double outstanding = getOutstanding();

       printDetails(outstanding);

   }

   double getOutstanding() {

       Enumeration e = _orders.elements();

       double outstanding = 0.0;

       while (e.hasMoreElements()) {

           Order each = (Order) e.nextElement();

           outstanding += each.getAmount();

       }

       return outstanding;

   }

Enumeration變量 e隻在被提煉碼中用到,是以我可以将它整個搬到新函數中。double變量outstanding在被提煉碼内外都被使用到,是以我必須讓提煉出來的新函數傳回它。編譯測試完成後,我就把回傳值改名,遵循我的一貫命名原則:

   double getOutstanding() {

       Enumeration e = _orders.elements();

       double result = 0.0;

       while (e.hasMoreElements()) {

           Order each = (Order) e.nextElement();

          result = each.getAmount();

       }

       return result;

   }

本例中的outstanding變量隻是很單純地被初始化為一個明确初值,是以我可以隻在新函數中對它初始化。如果代碼還對這個變量做了其他處理,我就必須将它的值作為參數傳給目标函數。對于這種變化,最初代碼可能是這樣:

   void printOwing(double previousAmount) {

       Enumeration e = _orders.elements();

       double outstanding = previousAmount * 1.2;

       printBanner();

       // calculate outstanding

       while (e.hasMoreElements()) {

           Order each = (Order) e.nextElement();

           outstanding += each.getAmount();

       }

       printDetails(outstanding);

   }

提煉後的代碼可能是這樣:

   void printOwing(double previousAmount) {

       double outstanding = previousAmount * 1.2;

       printBanner();

       outstanding = getOutstanding(outstanding);

       printDetails(outstanding);

   }

   double getOutstanding(double initialValue) {

       double result = initialValue;

       Enumeration e = _orders.elements();

       while (e.hasMoreElements()) {

           Order each = (Order) e.nextElement();

           result += each.getAmount();

       }

       return result;

   }

編譯并測試後,我再将變量outstanding初始化過程整理一下:

   void printOwing(double previousAmount) {

       printBanner();

       double outstanding = getOutstanding(previousAmount * 1.2);

       printDetails(outstanding);

   }

這時候,你可能會問:『如果需要傳回的變量不止一個,又該怎麼辦呢?』

你有數種選擇。最好的選擇通常是:挑選另一塊代碼來提煉。我比較喜歡讓每個函 數都隻傳回一個值,是以我會安排多個函數,用以傳回多個值。如果你使用的語言支援「輸出式參數」(output parameters),你可以使用它們帶回多個回傳值。但我還是盡可能選擇單一傳回值。

臨時變量往往為數衆多,甚至會使提煉工作舉步維艱。這種情況下,我會嘗試先運用 Replace Temp with Query 減少臨時變量。如果即使這麼做了提煉依舊困難重重,我就會動用 Replace Method with Method Object,這個重構手法不在乎代碼中有多少臨時變量,也不在乎你如何使用它們。

繼續閱讀