iT邦幫忙

DAY 19
3

30天快速上手TDD系列 第 19

[Day 19]Refactoring - The End is the Beginning

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

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

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

上一篇文章:[Day 18]Refactoring - Factory Pattern
本系列文章專區
@前言
讓我們先來回顧一下,一開始要重構的程式碼,是什麼樣子呢?重構前的程式碼如下:

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

@重構後的程式碼
經過一系列重構的步驟(每一個技巧幾乎只要3分鐘就可以學會)之後,最後也要請各位讀者記得,重構完成後(嚴格來說,應該是過程中),記得把不必要的程式碼清除(尤其是被註解掉的程式碼),並且把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後,未來增加物流商,符合開放封閉原則)

  2. 複雜度(從14降到4)

  3. 程式碼覆蓋率(從0%到92.86%)

Smell Good? Isn't It?

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

@總結
幾點重要的總結:

  1. 請用身體記住重構循環的四個步驟:綠燈、重構、紅燈、填入。
  2. 只要綠燈,就可以deploy!
  3. 一次只做一件事。
  4. 讓程式碼自己會說話,只補API Document,去除多餘的comment。
  5. 曾經進入過重構循環的程式,隨時可以再重構、隨時可以再修改,只要綠燈,隨時可以deploy。
  6. 只要大家都有著『在這程式變美之前,我不能睡』的信念,程式碼就可以得到淨化(救贖)。

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

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

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

@延伸概念
下圖是TDD的循環:

感覺很熟悉,對吧?

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

如果沒有重構的能力,那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


上一篇
[Day 18]Refactoring - Factory Pattern
下一篇
[Day 20]ATDD - User Requirement
系列文
30天快速上手TDD31

2 則留言

0
ted99tw
iT邦高手 1 級 ‧ 2012-10-27 15:39:10

撒尿失丸......偷笑偷笑偷笑

我要留言

立即登入留言