每個程序員都應該學會分解復雜的方法

jopen 9年前發布 | 7K 次閱讀 程序員

今天,我們要講的重構方法為,提取方法(Extract Method)。這也是我最常用的重構方法之一。

注:雖然代碼示例是用PHP寫的,但相同的概念同樣也適用于其他任何OOP語言。

定義

下面是Martin Fowler給出的官方定義:

如果你有一個可以組合在一起的代碼段。那么將這個代碼片段整合為一個方法,其方法名就用來解釋該方法的目的。

</blockquote>

我認為再也沒有比這更簡單的定義了。此處我唯一想強調的是,方法名。事實上,你命名方法的方式決定了你能從這種重構中受益多少。例 如,methodmoveToPendingList()這個方法名就比mvToPLst()和moveToList()要好。如果你擔心代碼太長,那么 你錯了——我們的目標不是字符最少化,而是讓代碼更易于理解。好的命名方法能夠代替你為這個方法額外添加的注釋。

每個程序員都應該學會分解復雜的方法

為什么要使用重構?

重構很重要。慢慢的,你就會發現,重構帶來的好處比你付出的努力要多得多。最重要的一點是,它從根本上簡化了代碼。此外,重構讓代碼變得更易讀;允許重用;代替了那些令人討厭卻又不得不寫的用來描述代碼作用的注釋。我認為這些理由已經足夠說服你來使用重構了,不是嗎?

提取方法的案例

在你使用Extract Method(提取方法)重構的時候,可能會面臨這三種情況,它們分別是:沒有局部變量,使用局部變量和重新分配局部變量。下面我將一一說明。

舉例

假設,在你的電子商務應用程序中有一個方法,該方法用來打印用戶購物車中包括總價格在內的所有項目的細節。

public function printCartDetails()
{
    // print items in the cart
    echo "Your shopping cart contains the following items:<br>";
    echo "<table>";
    echo "<th>Name</th> <th>Price</th>";
    foreach($this->items as $item)
    {
        echo "<tr>";
        echo "<td>{$item->getName()}</td>";
        echo "<td>\${$item->getPrice()}</td>";
        echo "</tr>";
    }
    echo "</table>";

// calculate the total price
$totalPrice = 0;
foreach($this->items as $item)
    $totalPrice += $item->getPrice();

// print the total price
printf("The total price: $%d", $totalPrice);

}</pre>

請注意我們是如何從類的數組中獲取項目的。該數組包含了一列Item(項目)對象,這些Item對象每一個都有訪問名稱和價格屬性的函數:getName()和getPrice()。

這種方法有許多設計問題,首先方法太長,細節太煩瑣。其次,使用注釋來描述每個代碼片段要做什么,是一種不被認可的壞方法。同時,這也違背了 Single Responsibility Principle(單一功能原則)。因此,我們將這個方法分解為更小的方法,這些更小的方法每個都給一個名稱用來描述它們是做什么的。

讓我們先從負責打印用戶購物車中的項目的代碼片段開始。實際上,這是最簡單的方法提取情況,因為只需要這樣做:

public function printCartDetails()
{
    $this->printItemsInCart();

// calculate the total price
$totalPrice = 0;
foreach($this->items as $item)
    $totalPrice += $item->getPrice();

// print the total price
printf("The total price: $%d", $totalPrice);

}

private function printItemsInCart() { echo "Your shopping cart contains the following items:<br>"; echo "<table>"; echo "<th>Name</th> <th>Price</th>"; foreach($this->items as $item) { echo "<tr>"; echo "<td>{$item->getName()}</td>"; echo "<td>\${$item->getPrice()}</td>"; echo "</tr>"; } echo "</table>"; }</pre>

我們只需要剪切和粘貼代碼段到一個新的私有方法中,然后再從源方法調用它即可。這就是我所謂的沒有局部變量的情況。因為我們提取的代碼不依賴于我們從中提取代碼的方法中的任何局部變量。

這樣我們就不再需要注釋來描述這個代碼片段要做什么,這個提取方法的名字已經告訴了我們。

接下來要提取的是打印總價格。也很容易。這一次我們需要將源方法中的$totalPrice局部變量作為一個參數,傳遞到提取方法中。就像這樣:

public function printCartDetails()
{
    $this->printItemsInCart();

// calculate the total price
$totalPrice = 0;
foreach($this->items as $item)
{
    $totalPrice += $item->getPrice();
}

$this->printTotalPrice($totalPrice);

}

private function printTotalPrice($totalPrice) { printf("The total price: $%d", $totalPrice); }</pre>

而這種情況就是使用局部變量。因為提取出的方法需要使用來自于源方法的一個局部變量(在這個例子中就是$totalPrice)來顯示總價格。很簡單,是不是?

現在,讓我們提取最后一個負責計算總價的方法。如果你有仔細看的話,你會發現,它修改了源方法中的局部變量($totalPrice)。此外,之后 還使用了本地變量。因此,我們不能簡單地不做任何修改地剪切和粘貼完全相同的代碼到新方法中:我們得根據新版本的提取方法來重新分配局部變量。而且我們只 需要返回修改后的變量就可以辦到。就像這樣:

public function printCartDetails()
{
    $this->printItemsInCart();

$totalPrice = 0;

$totalPrice = $this->calculateTotalPrice($totalPrice);

$this->printTotalPrice($totalPrice);

}

private function calculateTotalPrice($totalPrice) {

foreach($this->items as $item)
{
    $totalPrice += $item->getPrice();
}

return $totalPrice;

}</pre>

不錯,但還可以提高。如果我們只是用類似于那樣的文本值初始化局部變量(即這里的$totalPrice)的話,那么就沒有必要在源方法中保留它,因此我們可以將初始化放到提取方法中。

public function printCartDetails()
{
    $this->printItemsInCart();

$totalPrice = $this->calculateTotalPrice();

$this->printTotalPrice($totalPrice);

}

private function calculateTotalPrice() { $totalPrice = 0;

foreach($this->items as $item)
{
    $totalPrice += $item->getPrice();
}

return $totalPrice;

}</pre>

但是,如果初始化依賴于源方法的值,那么我們就需要在提取方法之外保留那個局部變量,然后像之前那樣傳遞。例如:

public function printCartDetails($previousAmount)
{
    $this->printItemsInCart();

$totalPrice = previousAmount * 1.1;

$totalPrice = $this->calculateTotalPrice($totalPrice);

$this->printTotalPrice($totalPrice);

}

private function calculateTotalPrice($totalPrice) { $result = $totalPrice;

foreach($this->items as $item)
{
    $result += $item->getPrice();
}

return $result;

}</pre>

對比

下面讓我們將改進之后的公共方法printCartDetails()與改進之前做一個對比。

之前:

public function printCartDetails()
{
    // print items in the cart
    echo "Your shopping cart contains the following items:<br>";
    echo "<table>";
    echo "<th>Name</th> <th>Price</th>";
    foreach($this->items as $item)
    {
        echo "<tr>";
        echo "<td>{$item->getName()}</td>";
        echo "<td>\${$item->getPrice()}</td>";
        echo "</tr>";
    }
    echo "</table>";

// calculate the total price
$totalPrice = 0;
foreach($this->items as $item)
    $totalPrice += $item->getPrice();

// print the total price
printf("The total price: $%d", $totalPrice);

}</pre>

之后:

public function printCartDetails()
{
    $this->printItemsInCart();

$totalPrice = $this->calculateTotalPrice();

$this->printTotalPrice($totalPrice);

}</pre>

很明顯,改進之后容易理解多了!只需要5秒我就知道這段代碼要做什么:首先打印用戶購物車中的項目,然后它計算總價格并打印出來。就是這么簡單。

請注意,我們并不關心這段代碼如何打印購物車的詳細信息。我們只關心代碼要做什么。再次重申:我們關心的“what”而不是“how”。如果你想了解“how”的詳細信息,那么你就去看如何實現這件事的方法。

總結

以上就是一個非常簡單的提取方法重構的例子。提取方法重構是如此強大又易于使用,所以,我建議你從今天開始就使用到你的代碼中。

歡迎評論。

譯文鏈接: http://www.codeceo.com/article/programmer-break-method.html
英文原文: Break Your Method Into Smaller Ones
翻譯作者: 碼農網 – 小峰

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!