iT邦幫忙

DAY 15
4

30天快速上手TDD系列 第 15

[Day 15]Refactoring - 食神歸位

  • 分享至 

  • xImage
  •  

前兩篇文章,我們先以當下物件的角度,思考屬於自己的職責是什麼。而不屬於自己職責的部份,該委託給哪個物件來進行。並思考清楚當下物件所需要的,究竟是什麼,接著不必去管相依的物件如何實作,儘管開口叫他做,再跟他要結果即可。

接著,要怎麼確保其他物件的執行結果是我們要的呢?就是建立測試案例,先行建立單元測試。在其他物件的行為中,都還沒有實際的內容時,在我們動手撰寫其內容前,我們已經都思考好,這些物件接受到什麼資訊,該回傳怎麼樣的結果。有了這樣的前提,才能確保我們設計相依物件時,目的就是為了滿足當下物件的需求,目的就是為了通過測試案例。

而相依物件的測試案例怎麼產生呢?因為我們是先撰寫更上層的整合測試(在這例子是Selenium),在整合測試的input中,有一些脈絡可循,可以找到對應相依物件的input值,以及預期的output值。當下物件是頁面,只負責蒐集資訊,呼叫其他物件,呈現結果。

到這,已經是萬事具備,只欠東風。欠什麼東風?就是想辦法通過測試。因為這一系列是重構,所以我們根本不需要額外撰寫太多的程式碼(甚至只需要挪動程式碼),只需要把原本的程式碼,一個蘿蔔一個坑的,擺到它應該放的位置上,接著按個鍵就可以確認這樣的搬移動作是否有錯。

搬code,按下執行測試,我可以肯定你3分鐘就學會了!讚

上一篇文章:[Day 14]Refactoring - 驗貨
本系列文章專區
@目前的程式碼
先來回顧一下目前的物流商物件程式碼,仍然是透過「產生」功能所產生的殼:

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

而頁面的程式碼,則是把計算運費的function都註解掉了,程式碼如下:

    //將各方法內容搬到各個物流商的計算運費方法中

    //private void CalculatedByPostOffice()
    //{
    //    //頁面呈現物流商名稱
    //    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();
    //    }
    //}

    //private void CalculatedByHsinchu()
    //{
    //    //頁面呈現物流商名稱
    //    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();
    //    }

    //}

    //private void CalculatedByBlackCat()
    //{
    //    //頁面呈現物流商名稱
    //    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();
    //    }
    //}

測試程式碼,也就是那該死的九個紅燈,程式碼如下:

        /// <summary>
        ///GetsComapanyName 的測試
        ///</summary>
        [TestMethod()]
        public void GetsComapanyNameTest_v3()
        {
            BlackCat target = new BlackCat();
            string expected = "黑貓";
            string actual;
            actual = target.GetsComapanyName();
            Assert.AreEqual(expected, actual);
        }

        /// <summary>
        ///GetsFee 的測試
        ///</summary>
        [TestMethod()]
        public void GetsFeeTest_v3()
        {
            BlackCat target = new BlackCat();
            double expected = 0F;
            double actual;
            actual = target.GetsFee();
            Assert.AreEqual(expected, actual);
        }

        /// <summary>
        ///Calculate 的測試
        ///</summary>
        [TestMethod()]
        public void CalculateTest_v3()
        {
            //從整合測試的test case,來當做單元測試的test case

            //arrange
            BlackCat target = new BlackCat()
            {
                ShipProduct = new Product
                {
                    IsNeedCool = true,
                    Name = "商品測試1",
                    Size = new Size
                    {
                        Height = 10,
                        Length = 30,
                        Width = 20
                    },
                    Weight = 10
                }
            };

            //act
            target.Calculate();

            
            var expectedName = "黑貓";
            var expectedFee = 200;

            var actualName = target.GetsComapanyName();
            var actualFee = target.GetsFee();

            //assert
            Assert.AreEqual(expectedName, actualName);
            Assert.AreEqual(expectedFee, actualFee);
        }
		
        /// <summary>
        ///Calculate 的測試
        ///</summary>
        [TestMethod()]
        public void CalculateTest_v3()
        {
            Hsinchu target = new Hsinchu()
            {
                ShipProduct = new Product
                {
                    IsNeedCool = true,
                    Name = "商品測試1",
                    Size = new Size
                    {
                        Height = 10,
                        Length = 30,
                        Width = 20
                    },
                    Weight = 10
                }
            };

            //act
            target.Calculate();

            //assert
            var expectedName = "新竹貨運";
            var expectedFee = 254.16;

            var actualName = target.GetsComapanyName();
            var actualFee = target.GetsFee();

            //assert
            Assert.AreEqual(expectedName, actualName);
            Assert.AreEqual(expectedFee, actualFee);
        }

        /// <summary>
        ///GetsComapanyName 的測試
        ///</summary>
        [TestMethod()]
        public void GetsComapanyNameTest_v3()
        {
            Hsinchu target = new Hsinchu();
            string expected = "新竹貨運";
            string actual;
            actual = target.GetsComapanyName();
            Assert.AreEqual(expected, actual);
        }

        /// <summary>
        ///GetsFee 的測試
        ///</summary>
        [TestMethod()]
        public void GetsFeeTest_v3()
        {
            Hsinchu target = new Hsinchu();
            double expected = 0F;
            double actual;
            actual = target.GetsFee();
            Assert.AreEqual(expected, actual);
        }
		
        /// <summary>
        ///Calculate 的測試
        ///</summary>
        [TestMethod()]
        public void CalculateTest_v3()
        {
            PostOffice target = new PostOffice()
            {
                ShipProduct = new Product
                {
                    IsNeedCool = true,
                    Name = "商品測試1",
                    Size = new Size
                    {
                        Height = 10,
                        Length = 30,
                        Width = 20
                    },
                    Weight = 10
                }
            };

            //act
            target.Calculate();

            //assert
            var expectedName = "郵局";
            var expectedFee = 180;

            var actualName = target.GetsComapanyName();
            var actualFee = target.GetsFee();

            //assert
            Assert.AreEqual(expectedName, actualName);
            Assert.AreEqual(expectedFee, actualFee);

        }

        /// <summary>
        ///GetsFee 的測試
        ///</summary>
        [TestMethod()]
        public void GetsFeeTest_v3()
        {
            PostOffice target = new PostOffice();
            double expected = 0F;
            double actual;
            actual = target.GetsFee();
            Assert.AreEqual(expected, actual);
        }

        /// <summary>
        ///GetsComapanyName 的測試
        ///</summary>
        [TestMethod()]
        public void GetsComapanyNameTest_v3()
        {
            PostOffice target = new PostOffice();
            string expected = "郵局";
            string actual;
            actual = target.GetsComapanyName();
            Assert.AreEqual(expected, actual);
        }

@重構第七式:食神歸位
既然已經定義好,誰該做什麼事,也定義好大家應該有的產出結果,接下來就是要進行重構循環的『填入』動作。這個動作的重點在於,要想辦法讓紅燈(包括單元測試與整合測試),變成綠燈。現在在循環中,所處的階段如下圖所示:

我們將原本頁面上計算運費的方法內容,分別搬到所屬的物流商計算運費的方法裡面。

食神歸位的版本,以黑貓為例(頁面程式沒有改變,就不在此列出):

public class BlackCat 
{
    private double _fee;
    private readonly string _companyName = "黑貓";

    public Product ShipProduct { get; set; }

    public void Calculate()
    {
        ////頁面呈現物流商名稱
        //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();
        //}

        var weight = this.ShipProduct.Weight;

        //計算運費邏輯
        if (weight > 20)
        {
            this._fee = 500;
        }
        else
        {
            //頁面呈現計算的運費結果
            var fee = 100 + weight * 10;
            this._fee = fee;
        }
    }        

    public string GetsComapanyName()
    {
        return this._companyName;
    }

    public double GetsFee()
    {
        return this._fee;
    }
}

物流商的相關程式碼都歸位後,檢視一下執行單元測試與整合測試結果。

單元測試那九個紅燈,都變成綠燈了,請見下圖:

原本Selenium的紅燈,也變成綠燈了,請見下圖:

很好,我們又回到了綠燈了。

@小結
這一篇所提到的動作,看起來似乎沒啥重點,但是這是一個循環的最後收尾動作。在重構中,基本上根本不會改動到原本的商業邏輯,絕大部分(也最有效)的重構動作,都是小幅變更一些壞味道的程式碼,例如排版、註解、重新命名跟擷取方法,接下來才是中等程度的重構,例如獨立物件職責、擷取介面、透過pattern解決需求所需要的彈性。

到這邊,要提醒一下讀者,在重構的循環中,只要是綠燈的情況,就代表可以deploy到正式環境。也就代表不管我們改了什麼,程式仍可如預期般的執行出正確的結果。

以此為原則,各位讀者在進行重構的時候,就可以考量需求與資源,來調整要重構的細度需要到哪。

設計時遵循著這樣的順序:

  1. 先滿足使用上的需求
  2. 需求也就是測試案例
  3. 滿足測試案例也就是測試程式要通過
  4. 測試程式通過就代表production code滿足需求。

只要綠燈,基本上就可以deploy,前提是測試案例要足夠代表使用者的所有需求。否則系統功能還是會跟下圖的起司一樣:

重構一個重點:適可而止

這是避免over design的一大原則,重構到一個極致,用了一堆design pattern和特殊的架構設計,如果無法滿足可讀性、可維護性,再有彈性也沒人看的懂、改的動。

心裡一定要記住YAGNI原則! You ain't gonna need it!


上一篇
[Day 14]Refactoring - 驗貨
下一篇
[Day 16]Refactoring - 介面導向
系列文
30天快速上手TDD31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
ted99tw
iT邦高手 1 級 ‧ 2012-10-23 12:01:21

沙發

食神歸位,趕緊膜拜先!!!

簽名簽名簽名

我要留言

立即登入留言