[30天快速上手TDD][Day 12]Refactoring - 職責分離
前言
在上一篇文章中,介紹了先透過理解程式碼,加上註解與排版後,讓我們看了程式碼心情不會再這麼不爽。
也因為抽象思考完,用自己的話在註解來描述程式碼的目的與行為,所以可以很輕鬆快速地透過擷取方法的方式,將每一件事抽取成一個 function , function 的名字就是事情本身的意義。而原本的註解也就是 function 的 API document 。
透過一開始這兩個方式讓程式碼變得相當好懂,讓程式碼本身能說話。
接下來就是要更抽象地運用一些 OO 的原則,來把每一件事情的職責釐清。這系列強調的是,誰都可以學會,所以一樣,只要用 3 ~ 5 分鐘,就保證您也能學會這一招:找出誰,在做什麼事!
目前的程式碼
經過重構第二式:說人話,與重構第三式:垃圾分類。
程式碼如下:
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 ,也就是頁面。而頁面要做什麼事?
- 蒐集頁面資訊供計算運費。
- 呈現所選物流商名稱,以及計算完的運費結果。
至於怎麼計算運費,那不是頁面該煩惱的事,我們交給所屬的物流商來計算運費即可。
回到我們的程式碼,主要 function 有 3 個:
- CalculatedByBlackCat() : 透過黑貓來計算
- CalculatedByHsinchu() : 透過新竹貨運來計算
- CalculatedByPostOffice() : 透過郵局來計算
這邊要找出「誰,做什麼事」,有一個相當相當簡單的技巧,相信大家一學就會。針對前面透過人話所整理出來的 function ,只要找出該 function 代表的意義中的「主詞」、「動詞」、「受詞」即可。
什麼意思?很簡單:
- 主詞:代表類別;
- 動詞:代表方法;
- 受詞:通常是方法參數;
- 形容詞:通常是呼叫物件行為後,物件產生的狀態變化。
以上面的例子來說,就變成:
- CalculatedByBlackCat():黑貓,計算運費
- CalculatedByHsinchu():新竹貨運,計算運費
- CalculatedByPostOffice():郵局,計算運費
接著,就直接把我們的人話再寫成程式碼吧!
-
CalculatedByBlackCat() 改成:
BlackCat blackCat = new BlackCat(); blackCat.Calculate();
-
CalculatedByHsinchu() 改成:
Hsinchu hsinchu = new Hsinchu(); hsinchu.Calculate();
-
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...使用者只是想要台腳踏車!
Top-down 的設計方式,還有另外一種實踐方式,稱為曳光彈開發方式(Tracer Bullet Development, TBD),這個開發方式,在軟體專案成功的管理之道《Ship it! A Practical Guide to Successful Software Projects》,以及程序員修煉之道:從小工到專家《The Pragmatic Programmer: From Journeyman to Master》這兩本書中都有提到,讀者也可以看一下原作者對 Tracer Bullets 的解釋:Tracer Bullets and Prototypes ,這個方式跟這整個 TDD 系列在撰寫 production code 時,有點不謀而合,建議讀者可以花時間去了解與實際演練一下。
TBD 簡單的說,就是用最快速的方式,建立出 working prototype ,透過 interface 與 stub object 來做出類似 hard-code 的系統,過程中有一個重點,就是職責分離,透過相依於 interface 來切分職責,並將實作細節獨立出來,接著再透過 stub / mock 來建立測試案例,最後再實作細節與重構,整個系統的發展就會沿著一開始使用曳光彈所劃過的軌跡前進。
記得,站在當下物件的角度去思考與設計,把精神放在當下物件本身要處理的事情即可。不屬於自己職責的,就透過「找出誰,請它做什麼事」,就可以完成我們高層的抽象設計了。
小結
在物件導向的設計中,物件(也就是類別)是一個最基本的元素。
這也是為什麼介紹設計模式(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 對我來說,還是太抽象了,所以我選擇用主詞、動詞來區分)
再重複一次,到現在的重構步驟:
前提:一定要先建立測試,確保重構後的結果正確。
- 註解加上人話;
- 人話變成 function ;
- 找出人話中的主詞與動詞;
- 主詞變成類別(也就是物件);
- 動詞變成方法 (也就是行為);
到這邊,還是一環扣著一環,一塊小蛋糕般的輕鬆寫意。
這一個動作把職責分開,物件定義出來,行為突顯出來。看起來雖然沒什麼,卻是後續系統設計與架構調整的重要起步,因為,物件導向的原則與設計模式,都是基於物件的基礎。
blog 與課程更新內容,請前往新站位置:http://tdd.best/