[.NET]重構之路系列v12 –用 Decorator Pattern 讓職責分離與組合
前言
在 legacy 系統中,總是可以看到職責混淆不清,疊床架屋的物件設計。或是為了重用程式碼,把繼承鏈搞的落落長,導致因為繼承關係而產生強耦合的情況,無法因應需求來變更設計。
這篇文章要介紹的,就是將職責混和的情況分開,讓每個職責的程式碼只有一份,因應需求,透過工廠,來將物件組合起來,達到需求所需要的功能。
原本的程式碼
之前在準備物件導向 training 的 homework 題目時,剛好在噗浪與小朱討論到什麼樣的題目適合讓學員練習。最後,決定設計一個「幫DAO穿衣服」的題目。
下面這段程式碼,就是重構前的程式碼,題目的需求如同 class 上的註解所描述,接著我們針對重構的需求,以及可能面臨到的需求異動,來進行設計。
/// <summary>
/// 原本需求:
/// Product資料異動時,需記錄log,查詢不用
/// 對Product進行CRUD時,需檢查權限
///
/// 重構需求:
/// 1.
/// 將權限驗證與log,從ProductDao獨立出來
/// 讓ProductDao只處理CRUD的需求
///
/// 需求異動:
/// 1.
/// 在測試環境時,不檢查權限與不記錄log。
/// 檢查測試環境條件,請使用Context.IsTestEnvironment
///
/// </summary>
public class ProductDao
{
private Log _log = new Log();
public string Message { get; private set; }
public void Delete(string id)
{
//驗證使用者是否有權限,對此商品ID進行刪除動作
ProductAuthorizationService auth = new ProductAuthorizationService() { ProductId = id, AccessType = AccessType.Delete };
var isValid = auth.Check();
if (isValid)
{
//模擬實際刪除資料
Console.WriteLine("刪除商品ID為{0}的資料", id);
this.Message = "刪除成功";
}
else
{
this.Message = "權限不符,刪除失敗";
}
this._log.Write(this.Message);
}
public void Create(Entity.Product product)
{
//驗證使用者是否有權限,對此商品ID進行新增動作
ProductAuthorizationService auth = new ProductAuthorizationService() { Supplier = product.Supplier, AccessType = AccessType.Insert };
var isValid = auth.Check();
if (isValid)
{
//模擬實際新增資料
Console.WriteLine("新增商品ID為{0}, Name為{1}, Supplier為{2} 的資料", product.Id, product.Name, product.Supplier);
this.Message = "新增成功";
}
else
{
this.Message = "權限不符,新增失敗";
}
this._log.Write(this.Message);
}
public Entity.Product Query(string id)
{
ProductAuthorizationService auth = new ProductAuthorizationService() { ProductId = id, AccessType = AccessType.Query };
var isValid = auth.Check();
if (isValid)
{
//模擬查詢資料
var result = new Product() { Id = id, Name = "測試Name", Supplier = "測試Supplier" };
this.Message = "查詢成功";
return result;
}
else
{
this.Message = "權限不符,查詢失敗";
return null;
}
}
}
原本的Class Diagram
從 class diagram 可以看到,每一個 class 的關係,都是直接相依。而且 ProductDao 的職責除了原本的 CRUD 以外,還與 Log 以及權限驗證模組相耦合。
重構後的Class Diagram
Class diagram 重點(用紅色箭頭標記的就是重要的關係):
- 相依於介面。
- 實際進行 CRUD 的動作,仍在 ProductDao 中,但就只有 CRUD 的動作。
- ProductDao, LogProductDao, AuthrizeProductDao 三個 class 都實作 IProductDao,也就代表著,三個 class 對外都擁有 IProductDao 的行為。
- LogProductDao 以及 AuthrizeProductDao,與 IProductDao 除了實作的關係以外,還有聚合的關係。簡單地說,這兩個物件身上,還放著一個 IProductDao 的物件。
圖上忽略的兩個部分:
- 工廠,仍需要工廠類別,只是不在 class diagram 上標記出相關關係,以簡化與關注重點。
- Program 直接相依於 Product,以介面導向來說,中間是要隔一層介面。但為了關注在作業重點上,而將其他部分簡化。
重構的目的
- 在 Product Entity 中,Product應該只會看的到一個 IProductDao,而且只關注於 IProductDao 介面上所提供的行為。Product Entity 的行為會很乾淨,與對外物件互動的關係也會很乾淨。
- 讓 ProductDao只負責處理 CRUD,LogDao 則只負責加上 log,AuthDao 則只關注在權限驗證。將物件職責分乾淨,同樣的程式碼不會散落一地與重複出現。
- 生成 IProductDao 的動作,以及該如何根據條件來決定組合,則交給 factory 來決定。這樣一來脆弱的 if/else 或變動的條件,就只會存在於 factory,且這樣的變動只會影響物件如何組合,而不會影響到物件內部的職責邏輯。
- 讓物件的相依性降到最低,內聚力拉到最高,就會有彈性,且影響範圍最低,發生需求異動或問題時,可以快速地找到對應物件進行修改,且外部對這樣的修改是無感的。
重構的設計
Product Entity 的設計,只需相依於 IProductDao,也只關注於和 IProductDao 介面的互動,程式碼如下圖:
ProductDao 則只保留、只關注 CRUD,而沒有 log 或驗證權限的職責與邏輯,程式碼如下圖:
LogProductDao 則只關注在,當 ProductDao 的 Create 方法與 Delete 方法被呼叫時,要記錄 log,Query則不需記錄 log。Log 的實作,則相依於 ILog 介面,而不與 Log 的實作細節相依。
需要注意的是,LogProductDao 除了實作 IProductDao 介面以外,建構式還需傳入 IProductDao 來進行實際的 CRUD 動作。簡單地說,LogProductDao 就只是件 IProductDao 的衣服。
程式碼如下圖:
AuthrorizeProductDao 的設計,則與 LogDao 雷同,只關注在權限的驗證,相依於權限驗證的介面,而不直接相依於實際驗證的細節。在驗證權限的職責中,也不關注實際 IProductDao 中資料存取的 CRUD 動作。
程式碼請見下圖(篇幅限制,Query 方法就先略過):
到這邊,基本上物件本身的職責就設計完成了。
ProductDao 只負責實際的資料存取動作,LogProductDao 只負責在 Create 方法與 Delete 方法之後,加上 log 的記錄。而 AuthorizeProductDao ,則只負責在呼叫 Create, Delete 與 Query 方法之前,檢查權限是否合法。
接下來的重頭戲,就在工廠要怎麼決定物件的組合。
Factory 的職責,主要就是生成物件。而在這作業的重點,就在於如何產生 IProductDao 的 instance。
在延伸需求中提到,當為測試環境時,不檢查權限也不記錄 log。在這邊,判斷測試環境的條件為一個 Context 的靜態 property : IsTestEnvironment。
程式碼如下圖:
在上面的程式碼,就是這次作業的精髓:幫 Dao 穿衣服。
在這邊,我們幫 ProductDao 穿上了 LogProductDao 的衣服。接著再穿上了 AuthorizeProductDao 的衣服。然後 assign 給 Product Entity。
Product Entity 實際呼叫 IProductDao 時,是呼叫了穿著多件衣服的 ProductDao ,如下圖所示:
結論
透過不同的衣服,來定義出實作同一個介面,但擁有不同的職責,且物件可重用、可互動、可組合。物件組合方式的決定,則在生成物件的階段,由工廠來決定傳回什麼樣的物件組合。
這樣的設計,碰到下面的幾個需求,都只需要修改「該職責所屬之物件」,且外部物件無感,無感的原因是因為低耦合,無感的目的就是不會互相影響。
- 要加上 log 或權限驗證的條件改變,只需要改變 Factory。
- 實際的 CRUD 資料存取動作改變,只需要改變 ProductDao。
- 針對不同方法,要加上、改變或取消 log 的情況,只要改變 LogProductDao。
- 針對不同方法,要加上、改變或取消權限驗證的情況,只要改變 AuthrorizeProductDao。
- Log 的實作細節要抽換,只需新增一個實作 ILog 介面的類別。
- 權限驗證的實作細節要抽換,只需新增一個實作 IProductAuthorizationService 介面的類別。
單一職責原則、開放封閉原則、依賴反轉原則,組成了這一次重構的基礎。
透過介面的抽象化,一般化關係搭配聚合關係,來完成最最基本的 Decorator 模式,而這樣的方式,也是 AOP 設計的基底。AOP 可參考之前的文章介紹:[Spring.Net]Aop introduction–以performance log為例
下面這張圖則是之前介紹 AOP 概念時,所描繪的說明圖:
備註
- 最後重構後物件組成的順序,如果要按照原本需求的話,應該是new LogProductDao(new AuthorizeProductDao(new ProductDao)); 的順序才對。感謝 Kevin Chang 的提醒。
Sample Code
重構後結果:Session4Sample.zip
blog 與課程更新內容,請前往新站位置:http://tdd.best/