iT邦幫忙

DAY 17
4

30天快速上手TDD系列 第 17

[Day 17]Refactoring - Strategy Pattern

在上篇文章中,我們將各個物流商的物件,抽象化出來一個物流商的介面,這個介面提供了當下頁面物件所需要的功能:

  1. 計算運費
  2. 取得運費結果
  3. 取得物流商名稱

雖然頁面物件仍與物流商物件直接相依,但在context端已經是「使用介面」,而不管各物流商物件背後的實作了。

這篇文章,標題雖然帶著「Strategy Pattern」,也就是策略模式,但不熟Design Patterns的讀者朋友不用擔心,保持著心中無招即可。我們只需要把程式碼的壞味道用最自然的方式重構,您就會體會到Strategy Pattern的樣子、目的、用法,Strategy Pattern將會自動的浮現出來。

記得,雖是心中無招,但仍有心法,也就是OO的SOLID原則,是我們重構的底限。

只是重構一個判斷式,把一樣的東西留著,不一樣的東西抽成function,我想...3分鐘應該還是很夠用了 偷笑

上一篇文章:[Day 16]Refactoring - 介面導向
本系列文章專區

PS: 拖著殘廢的右手(肩旋轉袖肌腱炎),手抬不起來也要把鍵盤放在大腿上,硬生生把這一篇文章寫完...真心希望對各位讀者在開發上可以有所幫助。哭
@目前的程式碼
為方便閱讀重構前後的程式碼比較,這邊先列出截至目前為止,我們的頁面程式碼如下所示:

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();
            ILogistics logistics = new BlackCat() { ShipProduct = product };
            logistics.Calculate();
            companyName = logistics.GetsComapanyName();
            fee = logistics.GetsFee();


        }
        //選新竹貨運,計算出運費
        else if (this.drpCompany.SelectedValue == "2")
        {
            //計算
            //Hsinchu hsinchu = new Hsinchu() { ShipProduct = product };
            //hsinchu.Calculate();
            //companyName = hsinchu.GetsComapanyName();
            //fee = hsinchu.GetsFee();

            ILogistics logistics = new Hsinchu() { ShipProduct = product };
            logistics.Calculate();
            companyName = logistics.GetsComapanyName();
            fee = logistics.GetsFee();
        }
        //選郵局,計算出運費
        else if (this.drpCompany.SelectedValue == "3")
        {
            //計算
            //PostOffice postOffice = new PostOffice() { ShipProduct = product };
            //postOffice.Calculate();
            //companyName = postOffice.GetsComapanyName();
            //fee = postOffice.GetsFee();

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

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

@前言
重構到這,其實已經很充足了,職責已經分離,也透過介面來降低耦合,也有對應的整合測試與單元測試。

不過如同一開始重構的時機點所說,當我們為了需求或bug而修改功能時,其實可以再思考一下,這樣類似的需求會不會再發生。這樣的情況,有沒有合適的pattern可以解決我們的需求與問題。

首先切換回人話模式,眼前的功能需求,用人話來描述就是:『不同物流商,使用對應的計價方法』。用Design Pattern的用詞來說,就是:『根據條件,決定對應的演算法』。也就是策略模式(strategy pattern)。

雖然提到了策略模式,但不熟Design Patterns的讀者朋友不用擔心,我們只需要把程式碼的壞味道用最自然的方式重構,您就會體會到Strategy Pattern的樣子、目的、用法,Strategy Pattern將會自動的浮現出來。

@重構第九式:運用Design Pattern-策略模式
上面已經提到了,這段程式碼一言以蔽之,就是「不同物流商,使用對應的計價方法」,讓我們回過頭來看現在的程式碼,有哪些部分是相同的,哪些部分是不同的,如下圖所示:

可以看到經過抽象地使用介面之後,紅色方塊中的程式碼,已經是一模一樣了。不同的部分,是黃色方塊中的程式碼,也就是上面人話描述的「選擇不同物流商時,要使用不同的計價方法」。

如同DRY(Don't Repeat Yourself)設計原則所說,在設計系統時,應避免同樣一樣事,卻有著重複的程式碼的情況。一式多份,代表需求異動時,需要變更多份,代表不符合單一職責原則(SRP),也代表著可能會有漏改的情況。

以這例子來說,聰明如各位讀者,肯定知道,怎麼把相同的部分與不同的部分,抽到一個function中,只需要讓不同的部分變成參數傳入即可。

重構後的程式碼如下所示:

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();
        //    ILogistics logistics = new BlackCat() { ShipProduct = product };
        //    logistics.Calculate();
        //    companyName = logistics.GetsComapanyName();
        //    fee = logistics.GetsFee();


        //}
        ////選新竹貨運,計算出運費
        //else if (this.drpCompany.SelectedValue == "2")
        //{
        //    //計算
        //    //Hsinchu hsinchu = new Hsinchu() { ShipProduct = product };
        //    //hsinchu.Calculate();
        //    //companyName = hsinchu.GetsComapanyName();
        //    //fee = hsinchu.GetsFee();

        //    ILogistics logistics = new Hsinchu() { ShipProduct = product };
        //    logistics.Calculate();
        //    companyName = logistics.GetsComapanyName();
        //    fee = logistics.GetsFee();
        //}
        ////選郵局,計算出運費
        //else if (this.drpCompany.SelectedValue == "3")
        //{
        //    //計算
        //    //PostOffice postOffice = new PostOffice() { ShipProduct = product };
        //    //postOffice.Calculate();
        //    //companyName = postOffice.GetsComapanyName();
        //    //fee = postOffice.GetsFee();

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

        ILogistics logistics = this.GetILogistics(this.drpCompany.SelectedValue, product);
        if (logistics != null)
        {
            logistics.Calculate();
            companyName = logistics.GetsComapanyName();
            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>
/// 將ILogistics的instance,交給工廠來決定
/// </summary>
/// <param name="p"></param>
/// <param name="product"></param>
/// <returns></returns>
private ILogistics GetILogistics(string company, Product product)
{
    if (company == "1")
    {
        return new BlackCat() { ShipProduct = product };
    }
    else if (company == "2")
    {
        return new Hsinchu() { ShipProduct = product };
    }
    else if (company == "3")
    {
        return new PostOffice() { ShipProduct = product };
    }
    else
    {            
        return null;
    }
}

相同的部分,也就是頁面(在這為context,使用場景端)所關心的職責,如下圖所示:

讀者朋友們,從程式碼去閱讀這個計算運費按鈕的邏輯,去體會一下,程式碼會說話的感覺:

  1. 如果頁面Validation通過驗證
  2. 取得頁面上商品資訊
  3. 取得對應的物流商
  4. 請物流商計算運費
  5. 取得物流商名稱
  6. 取得運費結果
  7. 將名稱與運費結果呈現到頁面上

不同的部分,則是要想辦法限縮到最小範圍,也就是:究竟這個條件,只會影響哪些東西不同。相同的部分,請放到判斷式以外。如下圖所示:

不同的部分,指的是「畫面上選擇哪一間物流商」,而這個判斷,只會影響要使用哪一個物流商物件。而所有的物流商物件,都符合「物流商介面」(不論是繼承或實作,都是is-A的關係)。

到這邊,就只是透過一個function,將不同的部分放到參數中,以決定回傳哪一個物流商物件。相同的部分,則放到判斷式之外,用來描述context的流程與商業邏輯。

恭喜您,這就是策略模式。

如wiki上的描述:

the strategy pattern (also known as the policy pattern) is a particular software design pattern, whereby algorithms can be selected at runtime

也就是,在執行階段時,可以依據不同情況選擇不同的演算法。

來看一下wiki上strategy pattern的class diagram:

在這個例子裡,我們的程式碼若畫成class diagram,就是按照這樣的pattern所設計。如下圖所示:

@小結
策略模式,難嗎?如果您已經把程式碼重構成這副模樣,相信我,你真的不必懂「策略模式」這四個字。因為我們重構用的就只是最基本的物件導向精神與設計原則。

但,這也不代表著開發人員就不需要瞭解或學習設計模式。設計模式,就像UML一樣,除了可以拿來當作特定類型問題的guidance藍圖,也很常拿來溝通。當開發人員或分析設計人員,針對某一個情境、需求或問題時,可能只需要用「策略模式」四個字,就可以讓每個人心裡面有著基本的class model,並快速的mapping到眼前的情境。

想像一下,以這例子,每個人眼前面對的是重構前的程式碼,一個人提出:我們可以透過「策略模式」來重構,來把重複的程式碼降到最低,職責分離,並且對擴充開放,對修改封閉。這時,如果學習並瞭解過策略模式,大家腦袋裡基本上就會把頁面放到context,把抽象職責相同的部分淬練出一個介面,讓每個物件不同的實作細節封裝起來,頁面只需要透過介面,就能保持一致。

心中無招,就能不被設計模式的框框給設限住。但無招不代表亂七八糟,而是掌握最基本的精神、原則,針對眼前的問題,使用者的需求來解決。

讀者朋友可以試試,當碰到一個問題或需求時,先別去尋找哪一個pattern適用,而是透過這一系列的方式,先動手重構。直到您覺得重構完成了,接著去看這樣的問題,適合用哪一種pattern,接著比對您的設計與GoF原生的design pattern,有何異同。

接著用心去體會,不同的地方,是否屬於自己情境或問題下,需客製化或變形的部分。還是單純設計的冗贅,不夠精簡、精準。

如果是後者,恭喜你,你趁機學到了自己之前的盲點,再下一次的需求,您就更能使出pattern中的精妙之處。

如果是前者,恭喜你,您可以理解在自己的問題領域中,除了最原生的問題解決了,還更彈性地符合了使用者的需求。

去體會箇中差異,才能活學活用。設計模式,只是一些常見的問題領域,所衍生出常見的模式解決方式,它是一種最普遍、最抽象、最基礎的解決方式,不要去強求自己的設計所產生出來的class diagram一定要跟原著或wiki上圖形一模一樣,但絕對要能清楚說出來,為何不一樣。

最後,在重構中設計模式的確是一種很方便、快速、好用的手法,但這邊要強調的是,開發人員應該要能由需求、問題、legacy code當出發點,在重構的過程中,實踐並體會出,由原始程式碼演變成某一種或多種設計模式所搭配設計的最終結果。如此一來,您才真的能體會到設計模式的髓。(因為設計模式的演變過程,絕大部分也正是從重構而來)

當您已經能完全體會且累積了許多相同問題領域的重構手法後,面對這篇文章範例這類的問題,心中無想,就會自然而然的使出策略模式來解決。

最後搞笑一下,下面是大家很熟稔的一段台詞:

張三豐:無忌,你有九陽神功護體,學什麼武功都特別快,太極拳只重其義,不重其招,你忘記所有招式,就練成太極拳了!

張三豐:你記住了沒有?
張無忌:沒記住!
張三豐:這套叫什麼拳?
張無忌:不知道!
張三豐:你老爸姓什麼呢?
張無忌:我忘了!
張三豐:好!你只要記住把這兩渾蛋打成廢人就行了!

基本上就是這樣,物件導向的基本意義、目的、精神與原則,就像這邊的九陽神功,有九陽神功護體,學什麼pattern都快。

不必強記這是什麼pattern,只要記得:可以解決你的問題,滿足使用者的需求就行了!


上一篇
[Day 16]Refactoring - 介面導向
下一篇
[Day 18]Refactoring - Factory Pattern
系列文
30天快速上手TDD31

2 則留言

0
nestor
iT邦新手 4 級 ‧ 2012-10-25 14:52:11

沙發91大這系列文章會不會像賽大一樣有完整版阿....臉紅筆記

就是91 iT邦研究生 4 級 ‧ 2012-10-25 19:18:07 檢舉

不知道怎麼弄成完整版,畢竟30天的技術文,不像賽大的文文情並茂啊...

但是很感謝各位的支持 :)

0
ted99tw
iT邦高手 1 級 ‧ 2012-10-25 15:24:01

哭哭哭

這加菲貓真的粉行,拜讀+強推!!!

讚讚讚

我要留言

立即登入留言