在上一篇文章中,介紹了先透過理解程式碼,加上註解與排版後,讓我們看了程式碼心情不會再這麼不爽。
也因為抽象思考完,用自己的話在註解來描述程式碼的目的與行為,所以可以很輕鬆快速地透過擷取方法的方式,將每一件事抽取成一個function,function的名字就是事情本身的意義。而原本的註解也就是function的API document。
透過一開始這兩個方式讓程式碼變得相當好懂,讓程式碼本身能說話。
接下來就是要更抽象地運用一些OO的原則,來把每一件事情的職責釐清。這系列強調的是,誰都可以學會,所以一樣,只要用3~5分鐘,就保證您也能學會這一招:找出誰,在做什麼事!
上一篇文章:[Day 11]Refactoring - 讓程式碼說話
本系列文章專區
@目前的程式碼
經過重構第二式:說人話,與重構第三式:垃圾分類。
程式碼如下:
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個:
這邊要找出「誰,做什麼事」,有一個相當相當簡單的技巧,相信大家一學就會。針對前面透過人話所整理出來的function,只要找出該function代表的意義中的「主詞」、「動詞」、「受詞」即可。
什麼意思?很簡單:
以上面的例子來說,就變成:
接著,就直接把我們的人話再寫成程式碼吧!
CalculatedByBlackCat()改成:
BlackCat blackCat = new BlackCat();
blackCat.Calculate();
CalculatedByHsinchu()改成:
Hsinchu hsinchu = new Hsinchu();
hsinchu.Calculate();
3.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...使用者只是想要台腳踏車!
記得,站在當下物件的角度去思考與設計,把精神放在當下物件本身要處理的事情即可。不屬於自己職責的,就透過「找出誰,請它做什麼事」,就可以完成我們高層的抽象設計了。
@小結
在物件導向的設計中,物件(也就是類別)是一個最基本的元素。
這也是為什麼介紹設計模式(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對我來說,還是太抽象了,所以我選擇用主詞、動詞來區分)
再重複一次,到現在的重構步驟:
前提:一定要先建立測試,確保重構後的結果正確。
到這邊,還是一環扣著一環,一塊小蛋糕般的輕鬆寫意。
這一個動作把職責分開,物件定義出來,行為突顯出來。看起來雖然沒什麼,卻是後續系統設計與架構調整的重要起步,因為,物件導向的原則與設計模式,都是基於物件的基礎。
hatelove提到:
就像火影忍者中的身心轉換術
好好玩哦,居然有火影忍者,金係太奇妙了!!
沙發被搶走了
灑灑花也不錯啦~