1.引言
在針對大型的復雜領域進行建模時,聚合、實體和值對象之間的依賴關系可能會變得十分復雜。在某個對象中為了確保其依賴對象的有效實例被創建,需要深入了解對象實例化邏輯,我們可能需要加載其他相關對象,且可能為了保持其他對象的領域不變性增加了額外的業務邏輯,這樣即打破了領域的單一責任原則(SRP),又增加了領域的復雜性。
那如何去創建復雜的領域對象呢?因為復雜的領域對象的生命周期可能需要協調才能進行創建。 這個時候,我們就可以引入創建類模式——工廠模式來幫忙,將對象的使用與創建分開,將對象的創建邏輯明確地封裝到工廠對象中去。
2. DDD中的工廠
我們有必要先理清工廠和工廠模式。
DDD中工廠的主要目標是隱藏對象的復雜創建邏輯;次要目標就是要清楚的表達對象實例化的意圖。
而工廠模式是計模式中的創建類模式之一。借助工廠模式我們可以很好實現DDD中領域對象的創建。
而針對工廠模式的實現主要有四種方式:
- 簡單工廠:簡單實用,但違反開放封閉;
- 工廠方法:開放封閉,單一產品;
- 抽象工廠:開放封閉,多個產品;
- 反射工廠:可以最大限度的解耦。
具體實現可以參考創建相似對象,就交給『工廠模式』吧。
3.封裝內部結構
當需要為聚合添加元素時,我們不能暴露聚合的結構。我們以添加商品到購物車為例,來講解如何一步一步的使用工廠模式。
一般來說,添加到購物車需要幾個步驟:
- 加載用戶購物車
- 獲取商品稅率
- 創建新的購物車子項
相關的應用層代碼如下:
namespace Application {
public class AddProductToBasket {
// ......
public void Add (Product product, Guid basketId) {
var basket = _basketRepository.FindBy (basketId);
var rate = TaxRateService.ObtainTaxRateFor (product.Id, country.Id);
var item = new BasketItem (rate, product.Id, product.price);
basket.Add (item);
// ...
}
}
}
在以上代碼中,應用服務需要了解如何創建BasketItem
(購物車子項)的詳細邏輯。而這不應該時應用服務的職責,應用服務的職責在於協調。我們嘗試做以下改變來避免暴露聚合的內部結構。
namespace Application {
public class AddProductToBasket {
// ......
public void Add (Product product, Guid basketId) {
var basket = _basketRepository.FindBy (basketId);
basket.Add (product);
// ...
}
}
}
namespace DomainModel {
public class Basket {
// ......
public void Add (Product product) {
if (Contains (product))
GetItemFor (product).IncreaseItemQuantitBy (1);
else {
var rate = TaxRateService.ObtainTaxRateFor (product.Id,
country.Id);
var item = new BasketItem (rate, product.Id, product.price);
_items.Add (item);
}
}
}
}
以上代碼展示了Basket
(購物車)對象提供一個Add
方法,用來完成添加商品到購物車的業務邏輯,對應用服務隱藏了購物車如何存儲商品的細節。另外購物車聚合能夠確保其內部集合的完整性,因為它可以確保領域的不變性。通過這種方式,完成了職責的切換,現在的應用服務要簡單的多。
然而,卻引入了一個新的問題。為了根據商品創建有效的購物車子項,購物車需要提供一個有效的稅率。為了創建這個稅率,它要依賴一個TaxRateService
(稅率服務)。獲取創建購物車子項依賴的稅率,這並不屬於購物車的職責。而按照上面的實現,購物車承擔了第二責任,因為它必須始終了解如何創建有效的購物車子項以及在哪里去獲取有效的稅率。
為了避免購物車承擔額外的職責和隱藏購物車子項的內部結構。下面我們引入一個工廠對象來封裝購物車子項的創建,包括獲取正確的稅率。
namespace DomainModel {
public class Basket {
// ......
public void Add (Product product) {
if (Contains (product))
GetItemFor (product).IncreaseItemQuantitBy (1);
else
_items.Add (BasketItemFactory.CreateItemFor (product,
deliveryAddress));
}
}
public class BasketItemFactory {
public static void CreateBasketFrom (Product product, Country country) {
var rate = TaxRateService.ObtainTaxRateFor (product.Id, country.Id);
return new BasketItem (rate, product.Id, product.price);
}
}
}
引入工廠模式后,購物車的職責單一了,且隔離了來自購物車子項的變化,比如當稅率變化時,或購物車子項需要其他信息創建時,都不會影響到購物車的相關邏輯。
4.隱藏創建邏輯
考慮這樣的需求:訂單創建成功后,進行發貨處理時,要求根據訂單的商品和收件人信息選擇合適的快遞方式。比如默認發順豐,順豐無法送達的選擇中國郵政。
根據這個需求,我們可以抽象出一個Kuaidi
(快遞)對象用來封裝快遞信息,和一個Delivery
(發貨)對象用來封裝發貨信息(貨物、收件人信息、快遞等)。創建Delivery
的職責我們可以放到Order
中去,但針對Order
來說它並不知道要創建(選擇)哪一種Kuaidi
(快遞)。所以,我們可以創建一個KuaidiFactory
工廠負責Kuaidi
對象的創建。
namespace DomainModel {
public class Order {
// ...
public Delivery CreateFor (IEnumerable<Item> items, destination) {
var kuaidi = KuaidiFactory.GetKuaidiFor (items,
destination.Country);
var delivery = new Delivery (items, destination, kuaidi);
SetAsDispatched (items, delivery);
return delivery;
}
}
public class KuaidiFactory {
public static Kuaidi GetKuaidiFor (IEnumerable<Item> deliveryItems,
DeliveryAddress destination) {
if (Shunfeng.CanDeliver (deliveryItems, destination)) {
return new Shunfeng (deliveryItems, destination);
} else {
return new EMS (deliveryItems, destination);
}
}
}
}
如上代碼所示,工廠類中我們封裝了快遞的選擇邏輯。
當要創建的對象類型有多個選擇,且客戶端並不關心創建類型的選擇時,我們可以在領域層使用工廠中去定義邏輯去決定要創建對象的類型。
5.聚合中的工廠方法
提到工廠,並不是都需要需要創建獨立的工廠類來負責對象的創建。一個工廠方法也可以存在於一個聚合中。
比如這樣一項需求,顧客可以將購物車中的商品移到願望清單中去。
第一,這個動作是發生在購物車上的,所以我們可以毫不猶豫的在購物車中定義該行為。第二,將商品添加到願望清單中去,就需要創建一個願望清單子項。
namespace DomainModel {
public class Basket {
// .....
public WishListItem MoveToWishList (Product product) {
//首先檢查購物車中是否包含此商品
if (BasketContainsAnItemFor (product)) {
//從購物車中獲取該商品對應的子項
var basketItem = GetItemFor (product);
//調用工廠方法根據購物車子項創建願望清單子項
var wishListItem = WishListItemFactory.CreateFrom (basketItem);
//從購物車中移除購物車子項
RemoveItemFor (basketItem);
return wishListItem;
}
}
}
}
從上面可以看出Basket
暴露一個方法用於將BasketItem
轉換為WishListItem
。返回的WishListItem
是WishList
聚合根的實體。另外一點我們之所以在Basket
中調用工廠去創建WishListItem
對象,是因為Basket
包含了創建願望清單子項所需的全部信息。在創建了WishListItem
之后,對於Basket
對象來說它的任務就完成了。
6.使用工廠重建對象
在項目中,如果沒有借助ORM進行數據模型與領域模型之間的映射,或者通過Web服務從一個老舊系統中獲取領域對象,都需要我們對領域對象進行重建以滿足領域的不變性。使用工廠來重建領域對象相對來說要比直接創建要復雜。
考慮這樣的場景:顧客可以在已購訂單中點擊再次購買按鈕,所有訂單項全部重新添加到購物車中去。
這個場景就屬於購物車對象的重建,跟直接創建購物車對象就不同了。因為將訂單中的所有子項恢復到購物車中去,我們就需要額外確保領域的不變性。比如訂單子項對應的商品現在是否下架,如果下架我們是直接拋出異常,還是仍舊創建一個鎖定的購物車子項,標記其為已下架狀態?
namespace DomainModel {
public class Order {
// ......
public Basket AddToCartFromOrder (Guid id) {
OrderDTO rawData = ExternalService.ObtainOrder (id.ToString ());
var basket = BasketFactory.ReconstituteBasketFrom (rawData);
return basket;
}
}
namespace DomainModel {
public class BasketFactory {
// ...
public static Basket ReconstituteBasketFrom (OrderDTO rawData) {
Basket basket;
// ...
foreach (var orderItem in rawData.Items) {
//是否下架
if (!ProductServie.IsOffTheShelf (orderItem.ProductId)) {
var newBasketItem = newBasketItem (orderItem.ProductId, orderItem.Qty);
basket.Add (newBasketItem);
} else {
throw new Exception ("訂單中該商品已下架,無法重新購買!");
}
}
// .....
return basket;
}
}
}
7.總結
對象創建不是一個領域的關注點,但它確實存在於應用程序的領域層中。通過使用工廠可以有效的保證領域模型的干凈整潔,以確保領域模型的對現實的准確表達。使用工廠具有以下好處:
- 工廠將領域對象的使用和創建分離。
- 通過使用工廠類,可以隱藏創建復雜領域對象的業務邏輯。
- 工廠類可以根據調用者的需要,創建相應的領域對象。
- 工廠方法可以封裝聚合的內部狀態。
然而,並不是任何需要實例化對象的地方都要使用工廠。只有當用工廠比使用構造函數更有表現力時,或存在多個構造函數容易造成混淆時,或者對要創建對象所依賴的對象不關心時,才選用工廠進行對象的創建。
參考資料:
《Patterns, Principles, and Practices of Domain-Driven Design》