[30天快速上手TDD][Day 13]Refactoring - 告訴我,你要什麼
前言
在上一篇文章中,透過分離主詞與動詞,定義出其他的物件與對應的行為,將不屬於當下物件的職責,拆分到其他的物件上。
在拆物件的過程中,還是一再強調,我們要把關注點放在當下物件上:「這個物件到底需要哪些功能,才能滿足使用者需求。」
這一篇文章,就是把自己放進當下物件後,把自己當大爺。非自己職責的事情,只要出一張嘴就行。概念就這麼簡單,一樣, 3 ~ 5 分鐘,就可以把這個技巧撿起來!
目前的程式碼
到上一步「找出誰,做什麼事」為止,已經將對應的物件拆分出來,程式碼如下:
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);
}
}
}
物流商相關的程式碼如下:
public class BlackCat
{
public void Calculate()
{
throw new NotImplementedException();
}
public string GetsComapanyName()
{
throw new NotImplementedException();
}
}
public class Hsinchu
{
public void Calculate()
{
throw new NotImplementedException();
}
public string GetsComapanyName()
{
throw new NotImplementedException();
}
}
public class PostOffice
{
public void Calculate()
{
throw new NotImplementedException();
}
public string GetsComapanyName()
{
throw new NotImplementedException();
}
}
眼尖的讀者應該發現了,跟原本的程式碼比起來,好像少了一些東西。
是的!到上一步,只將計算運費的邏輯拆出來,沒有傳入計算運費需要的參數,也沒有取得計算的結果,也沒有把計算結果跟物流商名稱,綁定到頁面上,也沒有通過測試。
重構第五式:給你錢,趕快做
在前一個動作,只有將計算的動作交給物流商處理,但頁面職責的部份還沒修改。
所謂的『給你錢,趕快做』指的就是,頁面需要什麼資訊,直接跟物流商要。但物流商要動作,可能也需要一些資訊。所以只要給物流商它需要的資訊,它就需要提供我們要的結果。
運用到的設計原則就是:『Tell, Don't Ask!』。
回顧一下,頁面需要什麼?
- 物流商的名稱。
- 該物流商計算的運費結果。
物流商需要什麼?
- 商品資訊。
定義出頁面要什麼,物流商計算運費需要什麼,就可以把程式修改一下:
- 頁面把商品資訊獨立出來(頁面 .GetProduct 的方法);
- 頁面把呈現結果獨立出來(頁面 .SetResult 的方法);
- 給物流商計算運費需要的資訊(頁面在初始化物流商時,對 ShipProduct 的初始化動作);
- 跟物流商要名稱(物流商 .GetsCompanyName 的方法);
- 跟物流商要運費結果(物流商 .GetsFee 的方法);
把當下物件負責要做的事列出來,不是自己職責以外的部分,就只出一張嘴,叫其他物件做事,再跟它要結果即可。這一招,我都笑稱為「給你錢,趕快做」。
(可以當作是請神龍幫你做…笑)
整理完之後,「給你錢,趕快做」版本的程式碼如下:
頁面的部分:
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();
}
//選新竹貨運,計算出運費
else if (this.drpCompany.SelectedValue == "2")
{
//計算
Hsinchu hsinchu = new Hsinchu() { ShipProduct = product };
hsinchu.Calculate();
companyName = hsinchu.GetsComapanyName();
fee = hsinchu.GetsFee();
}
//選郵局,計算出運費
else if (this.drpCompany.SelectedValue == "3")
{
//計算
PostOffice postOffice = new PostOffice() { ShipProduct = product };
postOffice.Calculate();
companyName = postOffice.GetsComapanyName();
fee = postOffice.GetsFee();
}
//發生預期以外的狀況,呈現警告訊息,回首頁
else
{
var js = "alert('發生不預期錯誤,請洽系統管理者');location.href='http://tw.yahoo.com/';";
this.ClientScript.RegisterStartupScript(this.GetType(), "back", js, true);
}
//呈現結果
this.SetResult(companyName, fee);
}
}
/// <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 端,所以物流商的程式碼,仍僅透過前面文章所介紹的 Visual Studio 產生的功能,產生出物件的空殼。
程式碼如下:
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();
}
}
public class Hsinchu
{
public void Calculate()
{
throw new NotImplementedException();
}
public Product ShipProduct { get; set; }
public string GetsComapanyName()
{
throw new NotImplementedException();
}
public double GetsFee()
{
throw new NotImplementedException();
}
}
public class PostOffice
{
public void Calculate()
{
throw new NotImplementedException();
}
public Product ShipProduct { get; set; }
public string GetsComapanyName()
{
throw new NotImplementedException();
}
public double GetsFee()
{
throw new NotImplementedException();
}
}
小結
這一式到這,其實頁面責任已了。頁面要做的事情都做完了,只要把物流商的肉補起來,這一整頁的功能就完成了。
這個開發方式,相當重要。頁面,其實就是一個物件,這也就代表著,所有物件的開發,都可以遵循這樣的方式,不管細節、不管外部實作如何,迅速抽象地完成使用者需求。
還記得測試中提到的嗎?測試就是模擬外部使用這個物件。外部使用端,指的可能是使用者,也有可能是另一個物件。
以這例子來說,頁面的外部使用端,就是使用者,所以我們透過 Selenium 測試來保護與驗證。而物流商的使用者,就是頁面這個物件。所以,下一步,我們就要建立相關的測試,來確保物流商的執行結果是正確的。
這樣的開發方式,也是為什麼 TDD 的開發方式,可以擁有快速、步伐小而實在的節奏。因為設計,不再只是看到一大坨程式碼細節,而是思考:
- 怎麼透過物件來組合出相關的功能;
- 怎麼拆分物件,則需要抽象思考物件的職責;
- 怎麼確認物件的職責,則需要用人類的思考來區分,而不是程式碼,所以需要人話;
- 人話是根據程式碼註解而來;
- 程式碼註解是根據一開始理解程式碼的目的而來;
- 程式碼的目的,是為了滿足使用者需求。
注意到了嗎?一路回推,我們回到最原本的一句老話:程式碼存在的目的,不是給開發人員寫爽的,而是為了滿足使用者需求。
動手做之前,一定要確保自己瞭解了使用者的需求,並且確保自己的所有動作,都不偏離這個目的。
補充
為什麼在頁面職責的部分,有一個「建立物流商物件」的動作要被 highlight 出來呢?
有興趣的讀者朋友們,可以思考一下。這在前面測試的文章有提到,在後面的重構也會再次提到這個部分。筆者就在這邊,先賣個關子。
blog 與課程更新內容,請前往新站位置:http://tdd.best/