[30天快速上手TDD][Day 19]Refactoring - The End is the Beginning

[30天快速上手TDD][Day 19]Refactoring - The End is the Beginning

前言

從 [Day 9] 開始,一直到 [Day 18] ,我們從最初不知道從哪開始重構,到現在程式碼變得高內聚、低耦合、可擴充、可讀、可維護,而且有了相關的測試保護,不再需要擔心受怕,因為別人改了某一個地方,導致我們的程式壞了。

這一篇文章,是本重構系列的總結,將帶著各位讀者驗收一下這幾篇重構的成果。

最後將帶出,重構的循環,其實可視為是 TDD 的循環之一。就像兩個齒輪一般,互相搭配運轉。

 

重構前程式碼

讓我們先來回顧一下,一開始要重構的程式碼,是什麼樣子呢?重構前的程式碼如下:

protected void btnCalculate_Click(object sender, EventArgs e)
{
    if (this.IsValid)
    {
        if (this.drpCompany.SelectedValue == "1")
        {
            this.lblCompany.Text = "黑貓";
            var weight = Convert.ToDouble(this.txtProductWeight.Text);
            if (weight > 20)
            {
                this.lblCharge.Text = "500";
            }
            else
            {
                var fee = 100 + weight * 10;
                this.lblCharge.Text = fee.ToString();
            }
        }
        else if (this.drpCompany.SelectedValue == "2")
        {
            this.lblCompany.Text = "新竹貨運";
            var length = Convert.ToDouble(this.txtProductLength.Text);
            var width = Convert.ToDouble(this.txtProductWidth.Text);
            var height = Convert.ToDouble(this.txtProductHeight.Text);

            var size = length * width * height;

            //長 x 寬 x 高(公分)x 0.0000353
            if (length > 100 || width > 100 || height > 100)
            {
                this.lblCharge.Text = (size * 0.0000353 * 1100 + 500).ToString();
            }
            else
            {
                this.lblCharge.Text = (size * 0.0000353 * 1200).ToString();
            }
        }
        else if (this.drpCompany.SelectedValue == "3")
        {
            this.lblCompany.Text = "郵局";

            var weight = Convert.ToDouble(this.txtProductWeight.Text);
            var feeByWeight = 80 + weight * 10;

            var length = Convert.ToDouble(this.txtProductLength.Text);
            var width = Convert.ToDouble(this.txtProductWidth.Text);
            var height = Convert.ToDouble(this.txtProductHeight.Text);
            var size = length * width * height;
            var feeBySize = size * 0.0000353 * 1100;

            if (feeByWeight < feeBySize)
            {
                this.lblCharge.Text = feeByWeight.ToString();
            }
            else
            {
                this.lblCharge.Text = feeBySize.ToString();
            }
        }
        else
        {
            var js = "alert('發生不預期錯誤,請洽系統管理者');location.href='http://tw.yahoo.com/';";
            this.ClientScript.RegisterStartupScript(this.GetType(), "back", js, true);
        }
    }
}

 

重構後的程式碼

經過一系列重構的步驟之後,最後也要請各位讀者記得,重構完成後(嚴格來說,應該是過程中),記得把不必要的程式碼清除,尤其是被註解掉的程式碼,並且把 class 與 function 相關的 API Document 補上。我們最終的程式碼如下,讀者可以體會一下,現在的程式碼,是不是自己會說話:

protected void btnCalculate_Click(object sender, EventArgs e)
{
    //若頁面通過驗證
    if (this.IsValid)
    {
        //取得畫面資料
        var product = this.GetProduct();                

        ILogistics logistics = FactoryRepository.GetILogistics(this.drpCompany.SelectedValue, product);
        if (logistics != null)
        {
            logistics.Calculate();
            var companyName = logistics.GetsComapanyName();
            var fee = logistics.GetsFee();

            //呈現結果
            this.SetResult(companyName, fee);
        }
        else
        {
            var js = "alert('發生不預期錯誤,請洽系統管理者');location.href='http://tw.yahoo.com/';";
            this.ClientScript.RegisterStartupScript(this.GetType(), "back", js, true);
        }            
    }
}

/// <summary>
/// 呈現結果
/// </summary>
/// <param name="companyName"></param>
/// <param name="fee"></param>
private void SetResult(string companyName, double fee)
{
    this.lblCompany.Text = companyName;
    this.lblCharge.Text = fee.ToString();
}

/// <summary>
/// 取得畫面資料
/// </summary>
/// <returns></returns>
private Product GetProduct()
{
    var result = new Product
    {
        Name = this.txtProductName.Text.Trim(),
        Weight = Convert.ToDouble(this.txtProductWeight.Text),
        Size = new Size()
        {
            Length = Convert.ToDouble(this.txtProductLength.Text),
            Width = Convert.ToDouble(this.txtProductWidth.Text),
            Height = Convert.ToDouble(this.txtProductHeight.Text)
        },
        IsNeedCool = this.rdoNeedCool.SelectedValue == "1"
    };

    return result;
}

可以看到,在 context 端邏輯分明,程式碼就像會說故事一般,有節奏地將要執行的步驟與目的都說的一清二楚。

 

檢視結果

從三個角度來檢視我們重構完的設計:

  1. UML(套用 strategy pattern 後,未來增加物流商,符合開放封閉原則)
    UML
  2. 複雜度(從 14 降到 4 )
    複雜度
  3. 程式碼覆蓋率(從 0% 到 92.86% )
    覆蓋率

 

Smell Good? Isn't It?

smell good

很簡單,也很美好,不是嗎?

 

總結

幾點重要的總結:

  1. 請用身體記住重構循環的四個步驟:綠燈、重構、紅燈、填入。
  2. 只要綠燈,就代表已經滿足需求。
  3. 一次只做一件事。
  4. 讓程式碼自己會說話,補上 API document,去除多餘的 comment 。
  5. 曾經進入過重構循環的程式,隨時可以再重構、隨時可以再修改,只要綠燈,隨時可以 deploy 。

 

你還記得嗎?

最後提出一個問題:『在這整個重構過程後,有讀者記得三間物流商是怎麼計算運費的嗎?

相信絕大部分的讀者答案都是不記得。

這樣就對了,這就是抽象。不需要把頭埋入細節中,把精神關注在物件的行為、職責以及互動上,才是重點。

 

延伸概念

下圖是 TDD 的循環:

TDD

感覺很熟悉,對吧?

其實,我們的重構循環,就是 TDD 的 Refactor 的細部動作,請見下圖:

TDD與重構

如果沒有重構的能力,那 TDD 出來的成品,只是一坨可以正確執行的垃圾。 TDD 的重點在可以正確執行出期望的結果,而在很多時候,我們也只需要可以執行出正確的結果即可。

The End is the Beginning,每一個重構的結束,都是為了下一個 TDD 循環的開始。當重構完通過測試後,給自己個獎勵,take a break…

享受並掌握一下,這樣的開發節奏、律動與循環,建議可以搭配 蕃茄鐘工作法Scrum 的流程,將更能掌握屬於自己與團隊的 心流(flow)狀態,更加事半功倍。

最後最後,引用一下 Ruddy 老師的一段話,用來作整系列重構文章的原則:『重構,應該針對需要的部份重構,且適可而止。』

Ruddy Lee:
常常有工程師把只會被執行個幾回就一輩子不會再被執行到的程式;寫得完美的一蹋糊塗,真是太愛乾淨了。真是愛做白工… 還不如早一點睡來得有價值,起碼會活的健康些,程式只要在他被需要的生命週期內活得好好的,就ok了! 這就是done了。

筆者個人的重構底限,就是 SOLID原則DRY原則KISS原則YAGNI原則。而目的就是為了滿足使用者需求。期望讓程式碼更好理解、更好擴充、更好維護。

下一篇文章開始,我們將開始談 ATDD 與 BDD 。接著用個簡單的例子,從使用者需求這源頭開始,示範如何從頭開始 TDD 。

 

Sample Code

附上本系列文章的Sample Code(包括所有版本與測試) :sample code.zip

對敏捷開發有興趣的朋友,可以參考我的粉絲專頁:91敏捷開發之路

對 TDD 課程有興趣的朋友,課程內容、大綱與學員心得,可以參考 skilltree 的公開課程:自動測試與 TDD 實務開發

若需要聯絡我,可以透過粉絲專頁私訊或是側欄的關於我。