[30天快速上手TDD][Day 19]Refactoring - The End is the Beginning
前言
從 [Day 9] 開始,一直到 [Day 18] ,我們從最初不知道從哪開始重構,到現在程式碼變得高內聚、低耦合、可擴充、可讀、可維護,而且有了相關的測試保護,不再需要擔心受怕,因為別人改了某一個地方,導致我們的程式壞了。
這一篇文章,是本重構系列的總結,將帶著各位讀者驗收一下這幾篇重構的成果。
最後將帶出,重構的循環,其實可視為是 TDD 的循環之一。就像兩個齒輪一般,互相搭配運轉。
重構前程式碼
讓我們先來回顧一下,一開始要重構的程式碼,是什麼樣子呢?重構前的程式碼如下:
protected void btnCalculate_Click(object sender, EventArgs e)
{
if (this.IsValid)
{
if (this.drpCompany.SelectedValue == "1")
{
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();
}
}
else if (this.drpCompany.SelectedValue == "2")
{
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();
}
}
else if (this.drpCompany.SelectedValue == "3")
{
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();
}
}
else
{
var js = "alert('發生不預期錯誤,請洽系統管理者');location.href='http://tw.yahoo.com/';";
this.ClientScript.RegisterStartupScript(this.GetType(), "back", js, true);
}
}
}
重構後的程式碼
經過一系列重構的步驟之後,最後也要請各位讀者記得,重構完成後(嚴格來說,應該是過程中),記得把不必要的程式碼清除,尤其是被註解掉的程式碼,並且把 class 與 function 相關的 API Document 補上。我們最終的程式碼如下,讀者可以體會一下,現在的程式碼,是不是自己會說話:
protected void btnCalculate_Click(object sender, EventArgs e)
{
//若頁面通過驗證
if (this.IsValid)
{
//取得畫面資料
var product = this.GetProduct();
ILogistics logistics = FactoryRepository.GetILogistics(this.drpCompany.SelectedValue, product);
if (logistics != null)
{
logistics.Calculate();
var companyName = logistics.GetsComapanyName();
var 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>
/// 呈現結果
/// </summary>
/// <param name="companyName"></param>
/// <param name="fee"></param>
private void SetResult(string companyName, double fee)
{
this.lblCompany.Text = companyName;
this.lblCharge.Text = fee.ToString();
}
/// <summary>
/// 取得畫面資料
/// </summary>
/// <returns></returns>
private Product GetProduct()
{
var result = new Product
{
Name = this.txtProductName.Text.Trim(),
Weight = Convert.ToDouble(this.txtProductWeight.Text),
Size = new Size()
{
Length = Convert.ToDouble(this.txtProductLength.Text),
Width = Convert.ToDouble(this.txtProductWidth.Text),
Height = Convert.ToDouble(this.txtProductHeight.Text)
},
IsNeedCool = this.rdoNeedCool.SelectedValue == "1"
};
return result;
}
可以看到,在 context 端邏輯分明,程式碼就像會說故事一般,有節奏地將要執行的步驟與目的都說的一清二楚。
檢視結果
從三個角度來檢視我們重構完的設計:
Smell Good? Isn't It?
很簡單,也很美好,不是嗎?
總結
幾點重要的總結:
- 請用身體記住重構循環的四個步驟:綠燈、重構、紅燈、填入。
- 只要綠燈,就代表已經滿足需求。
- 一次只做一件事。
- 讓程式碼自己會說話,補上 API document,去除多餘的 comment 。
- 曾經進入過重構循環的程式,隨時可以再重構、隨時可以再修改,只要綠燈,隨時可以 deploy 。
你還記得嗎?
最後提出一個問題:『在這整個重構過程後,有讀者記得三間物流商是怎麼計算運費的嗎?』
相信絕大部分的讀者答案都是不記得。
這樣就對了,這就是抽象。不需要把頭埋入細節中,把精神關注在物件的行為、職責以及互動上,才是重點。
延伸概念
下圖是 TDD 的循環:
感覺很熟悉,對吧?
其實,我們的重構循環,就是 TDD 的 Refactor 的細部動作,請見下圖:
如果沒有重構的能力,那 TDD 出來的成品,只是一坨可以正確執行的垃圾。 TDD 的重點在可以正確執行出期望的結果,而在很多時候,我們也只需要可以執行出正確的結果即可。
The End is the Beginning,每一個重構的結束,都是為了下一個 TDD 循環的開始。當重構完通過測試後,給自己個獎勵,take a break…
享受並掌握一下,這樣的開發節奏、律動與循環,建議可以搭配 蕃茄鐘工作法 與 Scrum 的流程,將更能掌握屬於自己與團隊的 心流(flow)狀態,更加事半功倍。
最後最後,引用一下 Ruddy 老師的一段話,用來作整系列重構文章的原則:『重構,應該針對需要的部份重構,且適可而止。』
Ruddy Lee:
常常有工程師把只會被執行個幾回就一輩子不會再被執行到的程式;寫得完美的一蹋糊塗,真是太愛乾淨了。真是愛做白工… 還不如早一點睡來得有價值,起碼會活的健康些,程式只要在他被需要的生命週期內活得好好的,就ok了! 這就是done了。
筆者個人的重構底限,就是 SOLID原則,DRY原則,KISS原則,YAGNI原則。而目的就是為了滿足使用者需求。期望讓程式碼更好理解、更好擴充、更好維護。
下一篇文章開始,我們將開始談 ATDD 與 BDD 。接著用個簡單的例子,從使用者需求這源頭開始,示範如何從頭開始 TDD 。
Sample Code
附上本系列文章的Sample Code(包括所有版本與測試) :sample code.zip
blog 與課程更新內容,請前往新站位置:http://tdd.best/