每个程序员都应该学会的重构方法

今天,我们要讲的重构方法为,提取方法(Extract Method)。这也是我最常用的重构方法之一。

注:虽然代码示例是用PHP写的,但相同的概念同样也适用于其他任何OOP语言。

定义

下面是Martin Fowler给出的官方定义:

如果你有一个可以组合在一起的代码段。那么将这个代码片段整合为一个方法,其方法名就用来解释该方法的目的。

我 认为再也没有比这更简单的定义了。此处我唯一想强调的是,方法名。事实上,你命名方法的方式决定了你能从这种重构中受益多少。例 如,methodmoveToPendingList()这个方法名就比mvToPLst()和moveToList()要好。如果你担心代码太长,那么 你错了——我们的目标不是字符最少化,而是让代码更易于理解。好的命名方法能够代替你为这个方法额外添加的注释。

每个程序员都应该学会的重构方法

为什么要使用重构?

重构很重要。慢慢的,你就会发现,重构带来的好处比你付出的努力要多得多。最重要的一点是,它从根本上简化了代码。此外,重构让代码变得更易读;允许重用;代替了那些令人讨厌却又不得不写的用来描述代码作用的注释。我认为这些理由已经足够说服你来使用重构了,不是吗?

提取方法的案例

在你使用Extract Method(提取方法)重构的时候,可能会面临这三种情况,它们分别是:没有局部变量,使用局部变量和重新分配局部变量。下面我将一一说明。

举例

假设,在你的电子商务应用程序中有一个方法,该方法用来打印用户购物车中包括总价格在内的所有项目的细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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(单一功能原则)。因此,我们将这个方法分解为更小的方法,这些更小的方法每个都给一个名称用来描述它们是做什么的。

让我们先从负责打印用户购物车中的项目的代码片段开始。实际上,这是最简单的方法提取情况,因为只需要这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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局部变量作为一个参数,传递到提取方法中。就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)。此外,之后还使用了 本地变量。因此,我们不能简单地不做任何修改地剪切和粘贴完全相同的代码到新方法中:我们得根据新版本的提取方法来重新分配局部变量。而且我们只需要返回 修改后的变量就可以办到。就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)的话,那么就没有必要在源方法中保留它,因此我们可以将初始化放到提取方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}

但是,如果初始化依赖于源方法的值,那么我们就需要在提取方法之外保留那个局部变量,然后像之前那样传递。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()与改进之前做一个对比。

之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}

之后:

1
2
3
4
5
6
7
8
public function printCartDetails()
{
    $this->printItemsInCart();
 
    $totalPrice = $this->calculateTotalPrice();
 
    $this->printTotalPrice($totalPrice);
}

很明显,改进之后容易理解多了!只需要5秒我就知道这段代码要做什么:首先打印用户购物车中的项目,然后它计算总价格并打印出来。就是这么简单。

请注意,我们并不关心这段代码如何打印购物车的详细信息。我们只关心代码要做什么。再次重申:我们关心的“what”而不是“how”。如果你想了解“how”的详细信息,那么你就去看如何实现这件事的方法。

总结

以上就是一个非常简单的提取方法重构的例子。提取方法重构是如此强大又易于使用,所以,我建议你从今天开始就使用到你的代码中。

推荐阅读:

十八大编程法则提升程序员效率

有关 Java 的 10 条编程技巧

相关推荐