每個程式設計師都應該學會分解複雜的方法

2015-10-15    分類:其他、程式設計開發、首頁精華3人評論發表於2015-10-15

本文由碼農網 – 小峰原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

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

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

定義

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

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

我認為再也沒有比這更簡單的定義了。此處我唯一想強調的是,方法名。事實上,你命名方法的方式決定了你能從這種重構中受益多少。例如,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);
}

請注意我們是如何從類的陣列中獲取專案的。該陣列包含了一列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>";
}

我們只需要剪下和貼上程式碼段到一個新的私有方法中,然後再從源方法呼叫它即可。這就是我所謂的沒有區域性變數的情況。因為我們提取的程式碼不依賴於我們從中提取程式碼的方法中的任何區域性變數。

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

接下來要提取的是列印總價格。也很容易。這一次我們需要將源方法中的$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);
}

而這種情況就是使用區域性變數。因為提取出的方法需要使用來自於源方法的一個區域性變數(在這個例子中就是$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;
}

不錯,但還可以提高。如果我們只是用類似於那樣的文字值初始化區域性變數(即這裡的$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;
}

但是,如果初始化依賴於源方法的值,那麼我們就需要在提取方法之外保留那個區域性變數,然後像之前那樣傳遞。例如:

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;
}

對比

下面讓我們將改進之後的公共方法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);
}

之後:

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

    $totalPrice = $this->calculateTotalPrice();

    $this->printTotalPrice($totalPrice);
}

很明顯,改進之後容易理解多了!只需要5秒我就知道這段程式碼要做什麼:首先列印使用者購物車中的專案,然後它計算總價格並列印出來。就是這麼簡單。

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

總結

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

歡迎評論。

譯文連結:http://www.codeceo.com/article/programmer-break-method.html
英文原文:Break Your Method Into Smaller Ones
翻譯作者:碼農網 – 小峰
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章