[30天快速上手TDD][Day 11]Refactoring - 讓程式碼說話
前言
上一篇文章,介紹了重構的第一步,就是建立測試。跨出了這第一步,才能確保後面的重構動作不會影響到結果。這也是為什麼本系列文章,需要先介紹測試的技巧、目的以及方式。
重構先有了測試保護後,接下來就是想辦法理解程式碼,並且讓程式碼說話,這篇文章會介紹第二式:說人話,以及第三式:垃圾分類。
相信我,您絕對可以在 3 ~ 5 分鐘內理解這兩招。
目前的程式碼
要重構的程式碼,原始模樣:
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);
}
}
}
重構第二式:說人話
當把我們預期的行為錄製完之後,我們要開始對系統進行淨化的動作了。(相信我,每一招都是『易如反掌』,你絕對可以做到)
可以看一下我們原本重構的目標,那就是一坨散落一地攤著的 code 。很有可能有著奇怪的邏輯判斷式,複雜的演算法,亂七八糟的排版,甚至是一些已經過期的註解,都有可能是引誘你上當的陷阱跟地雷。但在咒罵前人之餘,我們先靜下心來。
因為淨化程式碼的第一步,就是說人話:面對程式碼,試著用人類的語言,去描述程式碼在做什麼事。正如同小說需要角色來說話,系統也需要程式碼來說話。
以這例子來說,上圖的程式碼,就可以用一句話來表達:『計算運費』。
所以,我們先為原本的程式加上人話。不需要修改任何程式碼,只需要理解程式碼的目的、行為後,為程式碼加上人類語言的註解,並且將每一件事(不是每一行 code ,而是每一段 code )排版分開來。
要注意的是,不需要每一行程式都加上人話,而是針對事情、行為來描述。
經過這一步,加上註解與排版的程式碼如下所示:
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);
}
}
}
這邊只加上了四行註解,分別是:
- 選了哪一間物流商。
- 計算出運費。
- 呈現物流商的名稱與運費結果。
- 防呆的處理。
別忘嚕,我們最終的目標是,讓程式碼自己會說話!
說人話的效益
這時候,其實開發人員的腦袋,已經大致上釐清這一大段程式碼的基本目的為何,裡面每一件事情抽象地來說,要做什麼事情。
這是很重要的一步,只有腦袋先想清楚了,接著透過註解、排版,來讓下次腦袋不用這麼辛苦,還得重新解譯一次這一坨鳥 code 。
重構第三式:垃圾分類
程式碼本身的現況,仍然跟一坨垃圾沒什麼兩樣,又髒又臭。
但有了上一步「人話的描述」之後,就可以簡單的為其進行「垃圾分類」的動作。
什麼是垃圾分類?就是下圖這樣(笑):
簡單的說,就是重構中的『擷取方法』。
透過 Visual Studio 的輔助,只需要:
- 把原本人話所描述的區塊選取後;
- 按滑鼠右鍵,選擇「重構」;
- 選擇「擷取方法」
如下圖所示:
接著把「人話的意義」當做「新方法的名稱」,這個動作就算完成了。如下圖所示:
垃圾分類完的版本
經過[重構]>[擷取方法]後,程式碼如下:
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();
}
}
由於有修改程式了,所以請記得跑一下 Selenium 的測試腳本,以確保垃圾分類完後,結果仍如預期般正確。如下圖所示:
很好,程式碼經過擷取方法後,仍然通過 UI 面的測試(這個測試其實可以稱作 Acceptance Testing ,也可以稱作 Integration Testing ),符合使用者的需求。
到這邊的手法,讀者朋友也可以參考小弟之前寫過的文章:[ASP.NET]重構之路系列v1 – UI, Business logic, Data access概念分開
小結
到目前為止,整個系列的文章,幾乎都是一環扣著一環。
先介紹測試如何進行,是因為重構的第一步,就是測試。
而有了測試的保護之後,就是讓開發人員發揮「抽象設計」的能力。透過第二式:說人話,促使開發人員在面對亂七八糟的 legacy code 時,可以先用心思考,每一段程式碼的目的與意義。套一句 Ruddy 老師的話,開發人員應該把自己拉到三萬英尺的高空,去觀看這段程式碼。
重構最容易出現的問題,就是開發人員把自己陷入程式碼的細節中,就像進入了八卦陣,當局者迷,走不出來。當沒有測試保護時,改了一堆細節之後,可能是一塌糊塗,也可能是瞎貓碰上死耗子,也可能根本沒測到問題,但程式碼其實已經出錯了。
透過第二式的排版,並加上註解,來輔助開發人員一一釐清程式碼。還有個美妙的地方是,當第二式完成後,其實一行程式碼都沒改到,但是開發人員卻不會像一開始這麼爆怒了。為什麼?因為他面對的,是自己改過的 code 了,人嘛…這時候就會覺得自己排版過,加過註解的程式碼,比剛剛那陀垃圾要好看、好懂十幾倍。
只要能讓自己靜下心來思考,又對未來的可讀性、維護性有幫助,又沒什麼風險與成本,不要小看了這一步的效益。另外,第三式可是承接著第二式而來。
有了前面的排版與註解,開發人員腦袋中也已經瞭解了要重構的程式碼全貌與目的(記得,要跳脫細節),接下來,只需要接著上一步的動作,將每一件事,也就是每一段程式碼,透過擷取方法,把人話的註解,變成一個個的 function 。透過工具的輔助,就可以輕鬆完成。
這時候,這些人話註解,就會成為每一個 function 的 API document ,在 C# 中,也就是 /// 區塊,例如 <summary> 。
每一件事,每一段程式碼,都變成了一句話,每一句話,都變成了一個function。
更讚的是,基本上,我們還沒動手異動程式碼的邏輯。但是,整個原本程式碼的區塊,卻變成光看程式碼的命名,就能闡述整段程式碼的目的跟過程。(筆者真的幻想有一天,把整段程式碼貼到 google 翻譯時,可以透過發音就把故事說出來的感覺)
讓我再貼一次剛剛第二式+第三式的結果:
可以感受一下節奏,當按了計算運費的按鈕後:
- 若頁面通過驗證,則
- 當選[黑貓]時,計算出運費,呈現物流商名稱與運費;
- 當選[新竹貨運]時,計算出運費,呈現物流商名稱與運費;
- 當選[郵局]時,計算出運費,呈現物流商名稱與運費;
- 當發生預期以外的狀況時,呈現警告訊息,回首頁。
很棒,不是嗎?
很簡單,對吧?
最後要叮嚀的是,擷取方法有可能會導致一些參數或回傳值有所變化,但基本上絕不會影響到原本程式碼的邏輯。但已經有了自動化測試的保護,就不要客氣,給自己個獎勵,改完程式,按下測試,就可以讓自己稍微休息一下,喝杯咖啡,上個廁所。
重構的節奏就是,建立綠燈,移動程式,按下測試,打完收工。接著繼續往下一步前進。
這樣的節奏,會使開發人員很容易進入「flow」的狀態,不容易累,也不容易寫錯程式而導致重工。建議可以搭配「蕃茄鐘工作法」,讓自己集中精神在每個 25 分鐘。
亞里斯多德說過:「卓越不是行為,而是習慣。」 在軟體專案成功的管理之道《Ship it! A Practical Guide to Successful Software Projects》一書中也提到,要刻意的去培養好的習慣,而不是讓自己不經意的被壞習慣纏身。
上述的心法、手法,都是一種習慣的養成,每一步都可以讓你更加輕鬆、進入狀態、提高生產力與品質,建議各位讀者可以多多練習,這一整個系列的手法與開發方式,最後就會是您的習慣,最後就會成就一個卓越的系統。
blog 與課程更新內容,請前往新站位置:http://tdd.best/