iT邦幫忙

DAY 12
3

30天快速上手TDD系列 第 12

[Day 12]Refactoring - 職責分離

在上一篇文章中,介紹了先透過理解程式碼,加上註解與排版後,讓我們看了程式碼心情不會再這麼不爽。

也因為抽象思考完,用自己的話在註解來描述程式碼的目的與行為,所以可以很輕鬆快速地透過擷取方法的方式,將每一件事抽取成一個function,function的名字就是事情本身的意義。而原本的註解也就是function的API document。

透過一開始這兩個方式讓程式碼變得相當好懂,讓程式碼本身能說話。

接下來就是要更抽象地運用一些OO的原則,來把每一件事情的職責釐清。這系列強調的是,誰都可以學會,所以一樣,只要用3~5分鐘,就保證您也能學會這一招:找出誰,在做什麼事!

上一篇文章:[Day 11]Refactoring - 讓程式碼說話

本系列文章專區
@目前的程式碼
經過重構第二式:說人話,與重構第三式:垃圾分類。

程式碼如下:

protected void btnCalculate_Click(object sender, EventArgs e)
{
    //若頁面通過驗證
    if (this.IsValid)
    {
        //選黑貓,計算出運費,呈現物流商名稱與運費
        if (this.drpCompany.SelectedValue == "1")
        {
            CalculatedByBlackCat();
        }
        //選新竹貨運,計算出運費,呈現物流商名稱與運費
        else if (this.drpCompany.SelectedValue == "2")
        {
            CalculatedByHsinchu();
        }
        //選郵局,計算出運費,呈現物流商名稱與運費
        else if (this.drpCompany.SelectedValue == "3")
        {
            CalculatedByPostOffice();
        }
        //發生預期以外的狀況,呈現警告訊息,回首頁
        else
        {
            var js = "alert('發生不預期錯誤,請洽系統管理者');location.href='http://tw.yahoo.com/';";
            this.ClientScript.RegisterStartupScript(this.GetType(), "back", js, true);
        }
    }
}

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

@重構第四式:誰,做什麼事。
當垃圾分類完後,接下來要進行的動作相當重要。簡單的說,我們要定義出:『誰,做什麼事』,也就是職責。

要定義職責,有一個相當相當重要的原則:『要知道現在所屬的物件為何,並用該物件的角度去看世界』!

就像火影忍者中的身心轉換術,只有當我們把自己放到現在正在執行的物件中(context),才能用對的角度來釐清,事情到底該由誰來負責。

以本篇的例子來說,當下所屬的物件為何?答案是Page,也就是頁面。而頁面要做什麼事?

  1. 蒐集頁面資訊供計算運費。
  2. 呈現所選物流商名稱,以及計算完的運費結果。

至於怎麼計算運費,那不是頁面該煩惱的事,我們交給所屬的物流商來計算運費即可。

回到我們的程式碼,主要function有3個:

  1. CalculatedByBlackCat(): 透過黑貓來計算
  2. CalculatedByHsinchu(): 透過新竹貨運來計算
  3. CalculatedByPostOffice(): 透過郵局來計算

這邊要找出「誰,做什麼事」,有一個相當相當簡單的技巧,相信大家一學就會。針對前面透過人話所整理出來的function,只要找出該function代表的意義中的「主詞」、「動詞」、「受詞」即可。

什麼意思?很簡單:

  1. 主詞:代表類別
  2. 動詞:代表方法
  3. 受詞:通常是方法參數
  4. 形容詞:通常是呼叫物件行為後,物件產生的狀態變化

以上面的例子來說,就變成:

  1. CalculatedByBlackCat():黑貓,計算運費
  2. CalculatedByHsinchu():新竹貨運,計算運費
  3. CalculatedByPostOffice():郵局,計算運費

接著,就直接把我們的人話再寫成程式碼吧!

  1. CalculatedByBlackCat()改成:

            BlackCat blackCat = new BlackCat();
            blackCat.Calculate();
    
  2. CalculatedByHsinchu()改成:

            Hsinchu hsinchu = new Hsinchu();
            hsinchu.Calculate();
    

3.CalculatedByPostOffice()改成:

            PostOffice postOffice = new PostOffice();
            postOffice.Calculate();

定義完『誰,做什麼事』的程式碼版本如下:

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

你說我發呆隨便亂瞎掰,這樣的程式碼根本就編譯不過,當然啦,因為我還沒發功啊!

當定義出物件與行為後,接著可以透過Visual Studio的『產生』功能,來自動產生對應的物流商Class以及計算的function。

在Class上按下產生,如下圖:

在方法上,按下產生,如下圖:

就可以看到class與function都被自動產生了,黑貓類別的程式碼如下:

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

這個動作,可以參考小弟之前撰寫的重構之路第三篇:[ASP.NET]重構之路系列v3 – 跨專案使用類別庫。

到這邊要注意一下!這時執行測試,會出現紅燈,因為我們雖將物件職責分離完成,可以編譯成功,但還沒有完成物件的內容。

請大家一定要感受一下這種top-down的設計方式,透過工具的輔助,可以讓我們把精神放在滿足使用者的需求上,而不必多花心思在實作細節中。最後的實作細節,只要把單元完成,功能就可以完整的串起來,而不是一頭埋進去實作細節,想像了許多不存在的需求,最後才要把各式各樣形狀的物件,兜成太空梭、潛水艇、噴射機等等形狀,come on...使用者只是想要台腳踏車!

記得,站在當下物件的角度去思考與設計,把精神放在當下物件本身要處理的事情即可。不屬於自己職責的,就透過「找出誰,請它做什麼事」,就可以完成我們高層的抽象設計了。

@小結
在物件導向的設計中,物件(也就是類別)是一個最基本的元素。

這也是為什麼介紹設計模式(design pattern)時,總是會輔以類別圖(class diagram)來進行說明。因為設計模式,就是把一些最常見的問題,建立一個被tuning過最普遍、風險最低、最原型的解決方式,獨立於實作以外,所以什麼樣的語言都可以套用與實作。因為重點是problem-solution。

設計模式也只是基於一些最基本的物件導向原則,以及實務經驗跟實務需求,所公認或建議遵循的設計方式。而拆解物件的基本原則,就是職責的區分,這也是我認為在軟體開發中,最抽象的部分。

根據單一職責原則(Single Responsibility Principle, SRP),一個物件就是一個職責,要避免一個物件擁有太多職責,也要避免一個職責分散在太多物件上。但這一直是我認為,設計中最難以區分的一個地方。單一職責原則,可以參考我之前的文章:[ASP.NET]91之ASP.NET由淺入深 不負責講座 Day17 – 單一職責原則

好在,我們懂人類的語言,而我們說的話,不管是什麼語言,總是會有主詞、動詞、形容詞、受詞等等...透過文法的剖析,可以協助我們快速的找出相關的物件。(其實如果各位有接觸過domain model,或使用Domain-Driven Development(DDD),應該可以很快速的找出domain entity,但DDD對我來說,還是太抽象了,所以我選擇用主詞、動詞來區分)

再重複一次,到現在的重構步驟:
前提:一定要先建立測試,確保重構後的結果正確。

  1. 註解加上人話。
  2. 人話變成function
  3. 找出人話中的主詞與動詞
  4. 主詞變成類別(也就是物件)
  5. 動詞變成方法 (也就是行為)

到這邊,還是一環扣著一環,一塊小蛋糕般的輕鬆寫意。

這一個動作把職責分開,物件定義出來,行為突顯出來。看起來雖然沒什麼,卻是後續系統設計與架構調整的重要起步,因為,物件導向的原則與設計模式,都是基於物件的基礎。


上一篇
[Day 11]Refactoring - 讓程式碼說話
下一篇
[Day 13]Refactoring - 告訴我,你要什麼
系列文
30天快速上手TDD31

1 則留言

0
ted99tw
iT邦高手 1 級 ‧ 2012-10-20 01:06:30

hatelove提到:
就像火影忍者中的身心轉換術

沙發
好好玩哦,居然有火影忍者,金係太奇妙了!!
讚讚讚
灑花灑花灑花

pajace2001 iT邦研究生 1 級 ‧ 2012-10-20 02:26:56 檢舉

沙發被搶走了XD
灑灑花也不錯啦~
灑花灑花灑花灑花

我要留言

立即登入留言