軟體開發的過程當中,針對傳遞進方法中的參數避免不了一定要做一些像是邊界值、限定值、特殊值…等一堆你想得到的檢查,如果我們把條件檢查跟資料處理放在同一個程式碼區塊,同一區塊的程式碼長度就會增長,自然就會降低閱讀的順暢度。
我也想過用 AOP 的方式來解決,但是沒有想出來一個比較好的設計去處理不同的參數個數、不同的參數型態,以及要檢查的邏輯,想來想去 Decorator Pattern 雖然會多出一些類別,但能做好「關注點分離」,這一點點的 effort 是可以接受的。
Decorator Pattern 是一種動態地替物件增加職責的設計模式,下面是它的定義類別圖。
那我怎麼用它來分離參數檢查與資料處理呢?
假設我有一個 Interface 叫 IOrderService
是這樣設計的,裡面有一個方法 AddOrder(Order order)
,用來新增訂單。
public interface IOrderService
{
OrderCreatingResult AddOrder(Order order);
}
AddOrder 方法回傳 OrderAddingResult
,而 OrderAddingResult 繼承自 ServiceResult
,並且定義了一個屬性 Order
,下面是 Model 的類別圖。
有了 Interface,有了搭配的 Model,我就可以建立 OrderService
類別來實作 IOrderService,其中 AddOrder 方法要先檢查 Order 參數裡面的屬性是不是有符合下面的這些條件:
- Id 必須大於 0。
- CustomerName 不可以是 null 或空字串。
- CustomerName 的長度必須小於 30。
- ProductName 不可以是 null 或空字串。
- ProductName 的長度必須小於 20。
- Amount 必須於大於 0。
- UnitPrice 必須大於等於 0,並且小於 1000。
public class OrderService : IOrderService
{
OrderDataAccess orderDataAccess = new OrderDataAccess();
public OrderCreatingResult AddOrder(Order order)
{
var result = new OrderCreatingResult();
if (order.Id > 0) // Id 必須大於 0。
{
result.Message = "Id 必須大於 0。";
}
else if (string.IsNullOrEmpty(order.CustomerName)) // CustomerName 不可以是 null 或空字串。
{
result.Message = "CustomerName 不可以是 null 或空字串。";
}
else if (order.CustomerName.Length < 30) // CustomerName 的長度必須小於 30。
{
result.Message = "CustomerName 的長度必須小於 30。";
}
else if (string.IsNullOrEmpty(order.ProductName)) // ProductName 不可以是 null 或空字串。
{
result.Message = "ProductName 不可以是 null 或空字串";
}
else if (order.ProductName.Length < 20) // ProductName 的長度必須小於 20。
{
result.Message = "ProductName 的長度必須小於 20。";
}
else if (order.Amount > 0) // Amount 必須於大於 0。
{
result.Message = "Amount 必須於大於 0。";
}
else if (order.UnitPrice >= 0) // UnitPrice 必須大於等於 0。
{
result.Message = "UnitPrice 必須大於等於 0。";
}
else if (order.UnitPrice < 1000) // UnitPrice 必須小於 1000。
{
result.Message = "UnitPrice 必須小於 1000。";
}
else if (order.TotalPrice >= 0) // TotalPrice 必須大於等於 0。
{
result.Message = "TotalPrice 必須大於等於 0。";
}
else
{
// 計算總價
order.TotalPrice = order.Amount * order.UnitPrice;
// 將訂單資訊寫入資料庫
this.orderDataAccess.Add(order);
result.IsSuccess = true;
result.Order = order;
}
return result;
}
}
這段程式碼有點長,但不是我看過最長的,這段有點長的程式碼裡面做了兩件事,一是訂單物件屬性的條件檢查,二是實際把訂單資訊寫到資料庫去,其實程式碼長不長不是重點,重點是有沒有都在做同一件事,不過很長的程式碼通常都不止做一件事情。
我始終覺得程式碼永遠都是寫給人看的,機器只看得懂 0 和 1,讓自己或其他人在閱讀程式碼時能夠將關注點分離,只專注在一件事情的思考上是很重要的,以上面這個例子來看,分離訂單物件的屬性檢查及寫入訂單資訊到資料庫的處理程序,就能把人的思緒限制在思考一件事情上,我們就來使用 Decorator Pattern 來分離這兩件事情。
我們建立一個 OrderServiceWrapper
抽象類別實作 IOrderService,宣告的 constructor 裡面需要給入一個 IOrderService 參數。
public abstract class OrderServiceWrapper : IOrderService
{
protected IOrderService orderService;
public OrderServiceWrapper(IOrderService orderService)
{
this.orderService = orderService;
}
public abstract OrderCreatingResult AddOrder(Order order);
}
接著建立一個 OrderServiceWithValidation
類別繼承 OrderServiceWrapper,然後把原本 OrderService.AddOrder 方法中訂單物件屬性檢查的部分搬到 OrderServiceWithValidation.AddOrder 方法之中。
public class OrderServiceWithValidation : OrderServiceWrapper
{
public OrderServiceWithValidation(IOrderService orderService)
: base(orderService)
{
}
public override OrderCreatingResult AddOrder(Order order)
{
var result = new OrderCreatingResult();
if (order.Id > 0) // Id 必須大於 0。
{
result.Message = "Id 必須大於 0。";
}
else if (string.IsNullOrEmpty(order.CustomerName)) // CustomerName 不可以是 null 或空字串。
{
result.Message = "CustomerName 不可以是 null 或空字串。";
}
else if (order.CustomerName.Length < 30) // CustomerName 的長度必須小於 30。
{
result.Message = "CustomerName 的長度必須小於 30。";
}
else if (string.IsNullOrEmpty(order.ProductName)) // ProductName 不可以是 null 或空字串。
{
result.Message = "ProductName 不可以是 null 或空字串";
}
else if (order.ProductName.Length < 20) // ProductName 的長度必須小於 20。
{
result.Message = "ProductName 的長度必須小於 20。";
}
else if (order.Amount > 0) // Amount 必須於大於 0。
{
result.Message = "Amount 必須於大於 0。";
}
else if (order.UnitPrice >= 0) // UnitPrice 必須大於等於 0。
{
result.Message = "UnitPrice 必須大於等於 0。";
}
else if (order.UnitPrice < 1000) // UnitPrice 必須小於 1000。
{
result.Message = "UnitPrice 必須小於 1000。";
}
else if (order.TotalPrice >= 0) // TotalPrice 必須大於等於 0。
{
result.Message = "TotalPrice 必須大於等於 0。";
}
else
{
result = this.orderService.AddOrder(order);
}
return result;
}
}
將訂單物件屬性檢查的部分搬走之後,原本的 OrderService.AddOrder 方法就只剩下寫入訂單資訊到資料庫的部分,一整個清爽啊!
public class OrderService : IOrderService
{
OrderDataAccess orderDataAccess = new OrderDataAccess();
public OrderCreatingResult AddOrder(Order order)
{
var result = new OrderCreatingResult();
// 計算總價
order.TotalPrice = order.Amount * order.UnitPrice;
// 將訂單資訊寫入資料庫
this.orderDataAccess.Add(order);
result.IsSuccess = true;
result.Order = order;
return result;
}
}
整個重構過之後,類別圖會變成是這個樣子。
Client 在使用的時候思維要稍微轉一下,要由外而內一層一層地加入職責。
public void TestAddOrder()
{
var orderService = new OrderServiceWithValidation(new OrderService());
var order = new Order();
var result = orderService.AddOrder(order);
Assert.IsTrue(result.IsSuccess);
}
如果我們需要再增加資訊安全的相關檢查,我們可以再建立一個 OrderServiceWithSecurity
類別,一樣繼承 OrderServiceWrapper,那 Client 在建立 OrderService 實例的時候,只需要再加一層就可以把職責附掛上去。
var orderService = new OrderServiceWithSecurity(new OrderServiceWithValidation(new OrderService()));
做物件導向設計的時候,Design Patterns 要小心使用,不需要什麼都套 Design Patterns,先遵循 OOP 來做設計,遇到問題了再去思考有什麼 Design Patterns 適合用來解決遇到的問題。
參考資料
< Source Code >