[30天快速上手TDD][Day 13]Refactoring - 告訴我,你要什麼

[30天快速上手TDD][Day 13]Refactoring - 告訴我,你要什麼

前言

在上一篇文章中,透過分離主詞與動詞,定義出其他的物件與對應的行為,將不屬於當下物件的職責,拆分到其他的物件上。

在拆物件的過程中,還是一再強調,我們要把關注點放在當下物件上:「這個物件到底需要哪些功能,才能滿足使用者需求。

這一篇文章,就是把自己放進當下物件後,把自己當大爺。非自己職責的事情,只要出一張嘴就行。概念就這麼簡單,一樣, 3 ~ 5 分鐘,就可以把這個技巧撿起來!

 

目前的程式碼

到上一步「找出誰,做什麼事」為止,已經將對應的物件拆分出來,程式碼如下:


    protected void btnCalculate_Click(object sender, EventArgs e)
    {
        //若頁面通過驗證
        if (this.IsValid)
        {
            //選黑貓,計算出運費,呈現物流商名稱與運費
            if (this.drpCompany.SelectedValue == "1")
            {
                //CalculatedByBlackCat();
                //取得畫面資料

                //計算
                BlackCat blackCat = new BlackCat();
                blackCat.Calculate();

                //呈現
            }
            //選新竹貨運,計算出運費,呈現物流商名稱與運費
            else if (this.drpCompany.SelectedValue == "2")
            {
                //CalculatedByHsinchu();
                //取得畫面資料

                //計算
                Hsinchu hsinchu = new Hsinchu();
                hsinchu.Calculate();

                //呈現
            }
            //選郵局,計算出運費,呈現物流商名稱與運費
            else if (this.drpCompany.SelectedValue == "3")
            {
                //CalculatedByPostOffice();
                //取得畫面資料

                //計算
                PostOffice postOffice = new PostOffice();
                postOffice.Calculate();

                //呈現
            }
            //發生預期以外的狀況,呈現警告訊息,回首頁
            else
            {
                var js = "alert('發生不預期錯誤,請洽系統管理者');location.href='http://tw.yahoo.com/';";
                this.ClientScript.RegisterStartupScript(this.GetType(), "back", js, true);
            }
        }
    }

物流商相關的程式碼如下:


    public class BlackCat
    {
        public void Calculate()
        {
            throw new NotImplementedException();
        }

        public string GetsComapanyName()
        {
            throw new NotImplementedException();
        }
    }

    public class Hsinchu
    {
        public void Calculate()
        {
            throw new NotImplementedException();
        }

        public string GetsComapanyName()
        {
            throw new NotImplementedException();
        }
    }

    public class PostOffice
    {
        public void Calculate()
        {
            throw new NotImplementedException();
        }

        public string GetsComapanyName()
        {
            throw new NotImplementedException();
        }
    }

眼尖的讀者應該發現了,跟原本的程式碼比起來,好像少了一些東西。

是的!到上一步,只將計算運費的邏輯拆出來,沒有傳入計算運費需要的參數,也沒有取得計算的結果,也沒有把計算結果跟物流商名稱,綁定到頁面上,也沒有通過測試。

 

重構第五式:給你錢,趕快做

在前一個動作,只有將計算的動作交給物流商處理,但頁面職責的部份還沒修改。

所謂的『給你錢,趕快做』指的就是,頁面需要什麼資訊,直接跟物流商要。但物流商要動作,可能也需要一些資訊。所以只要給物流商它需要的資訊,它就需要提供我們要的結果。

運用到的設計原則就是:『Tell, Don't Ask!』。

回顧一下,頁面需要什麼?

  1. 物流商的名稱。
  2. 該物流商計算的運費結果。

物流商需要什麼?

  1. 商品資訊。

定義出頁面要什麼,物流商計算運費需要什麼,就可以把程式修改一下:

  1. 頁面把商品資訊獨立出來(頁面 .GetProduct 的方法);
  2. 頁面把呈現結果獨立出來(頁面 .SetResult 的方法);
  3. 給物流商計算運費需要的資訊(頁面在初始化物流商時,對 ShipProduct 的初始化動作);
  4. 跟物流商要名稱(物流商 .GetsCompanyName 的方法);
  5. 跟物流商要運費結果(物流商 .GetsFee 的方法);

把當下物件負責要做的事列出來,不是自己職責以外的部分,就只出一張嘴,叫其他物件做事,再跟它要結果即可。這一招,我都笑稱為「給你錢,趕快做」。

神龍

(可以當作是請神龍幫你做…笑)

整理完之後,「給你錢,趕快做」版本的程式碼如下:

頁面的部分:


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

        var companyName = "";
        double fee = 0;

        //選黑貓,計算出運費
        if (this.drpCompany.SelectedValue == "1")
        {
            //計算
            BlackCat blackCat = new BlackCat() { ShipProduct = product };
            blackCat.Calculate();
            companyName = blackCat.GetsComapanyName();
            fee = blackCat.GetsFee();
        }
        //選新竹貨運,計算出運費
        else if (this.drpCompany.SelectedValue == "2")
        {
            //計算
            Hsinchu hsinchu = new Hsinchu() { ShipProduct = product };
            hsinchu.Calculate();
            companyName = hsinchu.GetsComapanyName();
            fee = hsinchu.GetsFee();
        }
        //選郵局,計算出運費
        else if (this.drpCompany.SelectedValue == "3")
        {
            //計算
            PostOffice postOffice = new PostOffice() { ShipProduct = product };
            postOffice.Calculate();
            companyName = postOffice.GetsComapanyName();
            fee = postOffice.GetsFee();
        }
        //發生預期以外的狀況,呈現警告訊息,回首頁
        else
        {
            var js = "alert('發生不預期錯誤,請洽系統管理者');location.href='http://tw.yahoo.com/';";
            this.ClientScript.RegisterStartupScript(this.GetType(), "back", js, true);
        }

        //呈現結果
        this.SetResult(companyName, fee);
    }

}

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

可以看到頁面只負責:

  1. 取得畫面上商品資訊;
  2. 初始化物流商物件;
  3. 取得運費結果;
  4. 取得物流商名稱;
  5. 呈現運費結果與物流商名稱;

而這些,就是為了滿足使用者的需求所需要的功能,不多不少。

物流商的物件,則因應頁面物件的需求,所產生對應的行為。這邊還只關注在頁面的 context 端,所以物流商的程式碼,仍僅透過前面文章所介紹的 Visual Studio 產生的功能,產生出物件的空殼。

程式碼如下:


    public class BlackCat
    {
        public void Calculate()
        {
            throw new NotImplementedException();
        }


        public Product ShipProduct { get; set; }

        public string GetsComapanyName()
        {
            throw new NotImplementedException();
        }

        public double GetsFee()
        {
            throw new NotImplementedException();
        }
    }

    public class Hsinchu
    {
        public void Calculate()
        {
            throw new NotImplementedException();
        }

        public Product ShipProduct { get; set; }

        public string GetsComapanyName()
        {
            throw new NotImplementedException();
        }

        public double GetsFee()
        {
            throw new NotImplementedException();
        }
    }

    public class PostOffice
    {
        public void Calculate()
        {
            throw new NotImplementedException();
        }

        public Product ShipProduct { get; set; }

        public string GetsComapanyName()
        {
            throw new NotImplementedException();
        }

        public double GetsFee()
        {
            throw new NotImplementedException();
        }
    }

 

小結

這一式到這,其實頁面責任已了。頁面要做的事情都做完了,只要把物流商的肉補起來,這一整頁的功能就完成了。

這個開發方式,相當重要。頁面,其實就是一個物件,這也就代表著,所有物件的開發,都可以遵循這樣的方式,不管細節、不管外部實作如何,迅速抽象地完成使用者需求。

還記得測試中提到的嗎?測試就是模擬外部使用這個物件。外部使用端,指的可能是使用者,也有可能是另一個物件。

以這例子來說,頁面的外部使用端,就是使用者,所以我們透過 Selenium 測試來保護與驗證。而物流商的使用者,就是頁面這個物件。所以,下一步,我們就要建立相關的測試,來確保物流商的執行結果是正確的。

這樣的開發方式,也是為什麼 TDD 的開發方式,可以擁有快速、步伐小而實在的節奏。因為設計,不再只是看到一大坨程式碼細節,而是思考:

  1. 怎麼透過物件來組合出相關的功能;
  2. 怎麼拆分物件,則需要抽象思考物件的職責;
  3. 怎麼確認物件的職責,則需要用人類的思考來區分,而不是程式碼,所以需要人話;
  4. 人話是根據程式碼註解而來;
  5. 程式碼註解是根據一開始理解程式碼的目的而來;
  6. 程式碼的目的,是為了滿足使用者需求。

注意到了嗎?一路回推,我們回到最原本的一句老話:程式碼存在的目的,不是給開發人員寫爽的,而是為了滿足使用者需求。

動手做之前,一定要確保自己瞭解了使用者的需求,並且確保自己的所有動作,都不偏離這個目的。

 

補充

為什麼在頁面職責的部分,有一個「建立物流商物件」的動作要被 highlight 出來呢?

有興趣的讀者朋友們,可以思考一下。這在前面測試的文章有提到,在後面的重構也會再次提到這個部分。筆者就在這邊,先賣個關子。

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

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

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