[ASP.NET]重構之路系列v6 –抽象來看程式是否符合DRY原則
前言
之前有提到,應該要抽象地去思考與設計程式。面對既存在的程式碼也是如此,如果看到的只是『一行一行』的程式碼,那就只是『見山是山』的程度。
許多好的軟體公司,都有code review的機制,code review基本上的方式,就是設計者應該要能『解釋』自己的程式碼是在做什麼。這個解釋,應該是用人話解釋,而不是解釋一行一行的程式碼語法。這個『用人話解釋』的過程,就是在抽象地解釋程式碼。這是很重要的經驗跟練習,當用人話說不清楚時,通常代表:
- programmer不知道自己在寫什麼,也很有可能是copy/paste,或是亂抄範例
- 程式寫的不好解釋,缺少可讀性
- 程式太冗長,抽象等級參差不齊
這篇文章,我們會舉個簡單的例子,用抽象地方式來review我們的code,進而進行重構。或許不是很貼近現實,但我想抓住那個感覺就夠了。
需求說明
我們手上有兩段code,分別是兩個Dao各自的方法:
1. FirstBankDao.UpdateOrInsert()
public class FirstBankDao
{
/// <summary>
/// 先檢查是否存在這筆資料,若不存在則呼叫新增,若存在則呼叫修改。
/// </summary>
/// <param name="id"></param>
/// <param name="name"></param>
public void UpdateOrInsert(Transaction data)
{
if (this.SelectCount(data) == 0)
{
this.Insert(data);
}
else
{
this.Update(data);
}
}
}
2. ChinaTrustDao.Modify()
public class ChinaTrustDao
{
public IChinaTrustBranchDao ChinaTrustBranchDao { get; set; }
/// <summary>
/// 檢查資料是否存在,如果不存在,則呼叫ChinaTrustBranchDao的新增,若存在,則呼叫修改。
/// </summary>
/// <param name="id"></param>
public void Modify(Transaction data)
{
string getCountSql = @"your select count sqlstament";
int count = this.ExecuteSql(getCountSql);
if (count == 0)
{
//ChinaTrustBranchDao可能是一個WebService
this.ChinaTrustBranchDao.Insert(data);
}
else
{
string updateSql = @"your update sqlstament";
this.Update(updateSql);
}
}
}
這兩個class的方法內容,有一樣嗎?如果一行一行來看,幾乎完全不一樣。但是,從註解來看呢?這邊的註解是描述方法內容要做哪些事情,也就是抽象地說明方法的意義與作法。我們將註解裡面的重點抓出來看,有幾個關鍵字:
- 資料是否存在
- 不存在,就新增相關資料
- 存在,就修改相關資料
對這兩個方法的意義來說,上面這三個關鍵字是同樣的,而且判斷邏輯也一樣,他們是同一件事,但內容全然不同。那現在這樣的作法有不妥之處嗎?我認為沒有不妥的前提是:
- 這樣的動作,就只有這兩個class的這兩個方法用到
- 這部分邏輯以後不可能會有需求異動
- 以後需求異動,這兩者不會同時異動,也就是意義上是拆開的。
不過,PM對我們說:「未來可能還會新增其他的Dao,也是用相同的方式處理資料。而且我希望這樣的邏輯,以後修改的話,所有相關的程式邏輯都要一起變更。例如以後改成先刪除再新增,而不是先檢查再決定要新增或修改。」。面對PM這樣的要求,我們該如何設計呢?我們用的方法是Design pattern裡面的Template Method。(為什麼叫做Template?因為骨頭都是一樣的,但細節內容可以不一樣)
設計步驟
步驟一:
替我們的Template命個名字,這樣的動作,通常畫面上就叫做『存檔』,那我們就叫Save吧。這個Save的方法,這兩個Dao都要能夠重用,而且未來要新增其他同樣操作的Dao要很便利。因此,我們定義一個父類別來供其他類別繼承。
步驟二:
Template的骨頭建好了,但是要做的事情,子類別都不一樣,應該將內容交給子類別來決定。所以通常只有兩種作法,要嘛宣告成abstract讓子類別繼承後override,要嘛拉到interface上,讓實作的class自行決定內容。這邊,我們只能用abstract。為什麼?不是interface比較抽象嗎?如果什麼都可以用interface來取代abstract的話,那abstract這個關鍵字可能早就消失了。
因為我們要有固定的template,我們想把邏輯定義好,內容才交給子類別決定。interface是不能有方法內容的,所以interface無法幫我們定義出一個固定的邏輯來重複使用。(一個系統重用程度看Abstract,抽象程度看Interface)
這邊要補充說明一下,可以限制Save()不可以被子類別覆寫,來限制所有子類別都需遵守abstract class所定義的邏輯。而Template method中,用到的abstract method,可自行根據需求來定義對應的visibility,不過不能宣告成private,因為它需要被子類別繼承覆寫。
步驟三:
接著讓我們的兩個Dao來繼承AbstractDao,然後將原本Modify的內容移至對應的位置。
搬完code的樣子:
步驟四:
另一個Dao也如法炮製。
步驟五:
將兩個方法的名字透過重構,重新命名成Save,讓所有原本有用到此方法,都改成呼叫Save(),然後再把子類別的Save方法刪掉,讓呼叫子類別的Save方法時,會自動呼叫到AbstractDao裡面的Save()。(這邊有個前提是相容性的問題,這裡重新命名,已經在使用這顆dll的系統並不會跟著被重新命名。)
如此一來,以後要用這個邏輯就只需要呼叫Save就可以了。我們Save的方法,在需要增加新的Dao時,只需要繼承AbstractDao即可。
結論
這邊我們使用了Template Method來重構我們的『邏輯』,以避免重複邏輯的程式碼被分散,未來需求異動得修改很多份,也避免了新的類別需要用到同樣的邏輯得重複開發。
有興趣的人也可以參考『重構-改善既有程式的設計』中的11.10,Form Template Method,寫得比我文章詳細很多。
blog 與課程更新內容,請前往新站位置:http://tdd.best/