[30天快速上手TDD][Day 17]Refactoring - Strategy Pattern
前言
在上篇文章中,我們將各個物流商的物件,抽象化出來一個物流商的介面,這個介面提供了當下頁面物件所需要的功能:
- 計算運費
- 取得運費結果
- 取得物流商名稱
雖然頁面物件仍與物流商物件直接相依,但在 context 端已經是「使用介面」,而不管各物流商物件背後的實作了。
這篇文章,標題雖然帶著「Strategy Pattern」,也就是策略模式,但不熟 Design Patterns 的讀者朋友不用擔心,保持著心中無招即可。我們只需要把程式碼的壞味道用最自然的方式重構,您就會體會到 Strategy Pattern 的樣子、目的、用法, Strategy Pattern 將會自動的浮現出來。
記得,雖是心中無招,但仍有心法,也就是 OO 的 SOLID 原則,是我們重構的底限。
只是重構一個判斷式,把一樣的東西留著,不一樣的東西抽成 function ,我想... 3 分鐘應該還是很夠用了
目前的程式碼
為方便閱讀重構前後的程式碼比較,這邊先列出截至目前為止,我們的頁面程式碼如下所示:
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 ,使用場景端)所關心的職責,如下圖所示:
讀者朋友們,從程式碼去閱讀這個計算運費按鈕的邏輯,去體會一下,程式碼會說話的感覺:
- 如果頁面 Validation 通過驗證
- 取得頁面上商品資訊
- 取得對應的物流商
- 請物流商計算運費
- 取得物流商名稱
- 取得運費結果
- 將名稱與運費結果呈現到頁面上
不同的部分,則是要想辦法限縮到最小範圍,也就是:究竟這個條件,只會影響哪些東西不同。相同的部分,請放到判斷式以外。如下圖所示:
不同的部分,指的是「畫面上選擇哪一間物流商」,而這個判斷,只會影響要使用哪一個物流商物件。而所有的物流商物件,都符合「物流商介面」(不論是繼承或實作,都是 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 當出發點,在重構的過程中,實踐並體會出,由原始程式碼演變成某一種或多種設計模式所搭配設計的最終結果。如此一來,您才真的能體會到設計模式的髓。(因為設計模式的演變過程,絕大部分也正是從重構而來)
當您已經能完全體會且累積了許多相同問題領域的重構手法後,面對這篇文章範例這類的問題,心中無想,就會自然而然的使出策略模式來解決。
最後搞笑一下,下面是大家很熟稔的一段台詞:
張三豐:無忌,你有九陽神功護體,學什麼武功都特別快,太極拳只重其義,不重其招,你忘記所有招式,就練成太極拳了!
張三豐:你記住了沒有?
張無忌:沒記住!
張三豐:這套叫什麼拳?
張無忌:不知道!
張三豐:你老爸姓什麼呢?
張無忌:我忘了!
張三豐:好!你只要記住把這兩渾蛋打成廢人就行了!
(圖片來源:youtube: http://www.youtube.com/watch?v=FaGUA-hUsys)
基本上就是這樣,物件導向的基本意義、目的、精神與原則,就像這邊的九陽神功,有九陽神功護體,學什麼 pattern 都快。
不必強記這是什麼 pattern ,只要記得:可以解決你的問題,滿足使用者的需求就行了!
blog 與課程更新內容,請前往新站位置:http://tdd.best/