上篇中咱們講述了「把商品賣給用戶」中的商品和用戶的初步設計。如今把剩餘的「賣」這個動做給作了。這裏提醒一下,正常狀況下,咱們的每一步業務設計都須要和領域專家進行溝通,儘量的符合通用語言的表述。這裏的領域專家包括但不限於當前開發團隊中對這塊業務最瞭解的開發人員、系統實際的使用人等。
若是在沒有結合當前上下文的狀況下,用通用語言來表述,咱們很容易把代碼寫成下面的這個樣子(其中DomainRegistry只是一個簡單的工廠,解耦應用層與其餘具體實現的依賴,內部也可使用IOC容器來實現):
var user = DomainRegistry.UserService().GetUser(userId); if (user == null) { return Result.Fail("未找到用戶信息"); } var product = DomainRegistry.ProductService().GetProduct(productId); if (product == null) { return Result.Fail("未找到產品信息"); } user.Buy(product, quantity); return null;
初步來看,好像很合理。這裏表達出的是「用戶購買了商品」這個語義。而後繼續往下寫,咱們會發現購買了以後應該怎麼辦呢,要把東西放到購物車啊。這裏又出現了購物車,我認爲購物車是咱們銷售子域中的一個核心概念,它也是整個用戶購買過程當中變化最頻繁的一個對象。咱們來梳理一下,一個最簡單的購物車至少包含哪些東西:
A.一個購物車必須是屬於一個用戶的。
B.一個購物車內必然包含購買的商品的相關信息。
首先咱們思考一下如何在咱們的購物車中表達出用戶的概念,購物車須要知道用戶的全部信息嗎?答案在大部分場景下應該是否認的,由於在用戶挑選商品並加到購物車的這個過程當中,整個購物車是不穩定的,那麼其實在用戶想要進行結算之前,咱們只須要知道這個購物車是誰的,僅此而已。那麼這裏咱們已經排除了一種方式是購物車直接持有User的引用。因此說對於購物車來講,在咱們排除爲性能而進行數據冗餘的狀況下,咱們只須要保持一個用戶惟一標識的引用便可。
購物車明細和商品之間的關係也是同樣,每次須要從遠程上下中獲取到最新的商品信息(如價格等),故也僅需保持一個惟一標識的引用。
結合上一篇講的,咱們目前已經出現瞭如下幾個對象,見【圖1,點擊圖片查看原圖】。
【圖1】
下面貼上購物車和購物車明細的簡單實現。
public class Cart : Infrastructure.DomainCore.Aggregate { private readonly List<CartItem> _cartItems; public Guid CartId { get; private set; } public Guid UserId { get; private set; } public DateTime LastChangeTime { get; private set; } public Cart(Guid cartId, Guid userId, DateTime lastChangeTime) { if (cartId == default(Guid)) throw new ArgumentException("cartId 不能爲default(Guid)", "cartId"); if (userId == default(Guid)) throw new ArgumentException("userId 不能爲default(Guid)", "userId"); if (lastChangeTime == default(DateTime)) throw new ArgumentException("lastChangeTime 不能爲default(DateTime)", "lastChangeTime"); this.CartId = cartId; this.UserId = userId; this.LastChangeTime = lastChangeTime; this._cartItems = new List<CartItem>(); } public void AddCartItem(CartItem cartItem) { var existedCartItem = this._cartItems.FirstOrDefault(ent => ent.ProductId == cartItem.ProductId); if (existedCartItem == null) { this._cartItems.Add(cartItem); } else { existedCartItem.ModifyQuantity(existedCartItem.Quantity + cartItem.Quantity); } } }
public class CartItem : Infrastructure.DomainCore.Entity { public Guid ProductId { get; private set; } public int Quantity { get; private set; } public decimal Price { get; private set; } public CartItem(Guid productId, int quantity, decimal price) { if (productId == default(Guid)) throw new ArgumentException("productId 不能爲default(Guid)", "productId"); if (quantity <= 0) throw new ArgumentException("quantity不能小於等於0", "quantity"); if (quantity < 0) throw new ArgumentException("price不能小於0", "price"); this.ProductId = productId; this.Quantity = quantity; this.Price = price; } public void ModifyQuantity(int quantity) { this.Quantity = quantity; } }
回到咱們最上面的代碼中的「user.Buy(product, quantity);」 的問題。在DDD中主張的是清晰的業務邊界,在這裏,咱們目前的定義致使的結果是User與Cart產生了強依賴,讓User內部須要知道過多的Cart的細節,而這些是User不該該知道的。這裏還有一個問題是在領域對象內部去訪問倉儲(或者調用遠程上下文的接口)來獲取數據並非一種提倡的方式,他會致使事務管理的混亂。固然有人會說,把Cart做爲一個參數傳進來,這看上去是個好主意,解決了在領域對象內部訪問倉儲的問題,然而看一下接口的定義,用戶購買商品和購物車?仍是用戶購買商品而且放入到購物車?這樣來看這個方法作的事情彷佛過多了,違背了單一職責原則。
其實在大部分語義中使用「用戶」做爲一個主體對象,看上去也都還挺合理的,然而細細的去思考當前上下文(系統)的核心價值,會發現「用戶」有時並非核心,固然好比是一個CRM系統的話核心便是「用戶」。
總結一下這種方式的缺點:
A.領域對象之間的耦合太高,項目中的對象容易造成蜘蛛網結構的引用關係。
B.須要在領域對象內部調用倉儲,不利於最小化事務管理。
C.沒法清晰的表達出通用語言的概念。
從新思考這個方法。「購買」這個概念更合理的描述是在銷售過程當中所發生的一個操做過程。在咱們電商行業下,能夠表述爲「用戶購買了商品」和「商品被加入購物車」。這時候須要領域服務出場了,由它來表達出「用戶購買商品」這個概念最爲合適不過了。其實就是把應用層的代碼搬過來了,如下是對應的代碼:
public class UserBuyProductDomainService { public CartItem UserBuyProduct(Guid userId, Guid productId, int quantity) { var user = DomainRegistry.UserService().GetUser(userId); if (user == null) { throw new ApplicationException("未能獲取用戶信息!"); } var product = DomainRegistry.ProductService().GetProduct(productId); if (product == null) { throw new ApplicationException("未能獲取產品信息!"); } return new CartItem(productId, quantity, product.SalePrice); } }
領域中的服務表示一個無狀態的操做,它用於實現特定於某個領域的任務。當某個操做不適合放在聚合和值對象上時,最好的方式即是使用領域服務了。
1.列舉幾個領域服務適用場景
A.執行一個顯著的業務操做過程。
B.對領域對象進行轉換。
C.以多個領域對象做爲輸入進行計算,結果產生一個值對象。
D.隱藏技術細節,如持久化與緩存之間的依存關係。
2.不要把領域服務做爲「銀彈」。過多的非必要的領域服務會使項目從面向對象變成面向過程,致使貧血模型的產生。
3.能夠不給領域服務建立接口,若是須要建立則須要放到相關聚合、實體、值對象的同一個包(文件夾)中。服務的實現能夠不只限於存在單個項目中。
按照這樣設計以後咱們的應用層代碼變爲:
1 var cartItem = _userBuyProductDomainService.UserBuyProduct(userId, productId, quantity); 2 var cart = DomainRegistry.CartRepository().GetOfUserId(userId); 3 if (cart == null) 4 { 5 cart = new Cart(DomainRegistry.CartRepository().NextIdentity(), userId, DateTime.Now); 6 } 7 cart.AddCartItem(cartItem); 8 DomainRegistry.CartRepository().Save(cart);
這裏的第5行用到了一個倉儲(資源庫)CartRepository,倉儲算是DDD中比較好理解的概念。在DDD中倉儲的基本思想是用面向集合的方式來體現,也就是至關於你在和一個List作操做,因此切記不能把任何的業務信息泄露到倉儲層去,它僅用於數據的存儲。倉儲的廣泛使用方式以下:
A.包含保存、刪除、指定條件的查詢(固然在大型項目中能夠考慮採用CQSR來作,把查詢和數據操做分離)。
B.只爲聚合建立資源庫
C.一般資源庫與聚合式 1對1的關係,然而有時,當2個或者多個聚合位於同一個對象層級中時,它們能夠共享同一個資源庫。
D.資源庫的接口定義和聚合放在相同的模塊中,實現類放在另外的包中(爲了隱藏對象存儲的細節)。
回到代碼中來,標紅的那部分也能夠用一個領域服務來實現,隱藏「若是一個用戶沒有購物車的狀況下新建一個購物車」的業務細節。
public class GetUserCartDomainService { public Cart GetUserCart(Guid userId) { var cart = DomainRegistry.CartRepository().GetOfUserId(userId); if (cart == null) { cart = new Cart(DomainRegistry.CartRepository().NextIdentity(), userId, DateTime.Now); DomainRegistry.CartRepository().Save(cart); } return cart; } }
這樣應用層就真正變成了一個講故事的人,清晰的表達出了「用戶購買商品的整個過程」,把商品購物車的商品轉換成購物車明細 --> 獲取用戶的購物車 --> 添加購物車明細到購物車中 --> 保存購物車。
public Result Buy(Guid userId, Guid productId, int quantity) { var cartItem = _userBuyProductDomainService.UserBuyProduct(userId, productId, quantity); var cart = _getUserCartDomainService.GetUserCart(userId); cart.AddCartItem(cartItem); DomainRegistry.CartRepository().Save(cart); return Result.Success(); }
這是最簡單的購買流程,後續咱們會慢慢充實整個購買的業務,包括會員價、促銷等等。我仍是保持每一篇內容的簡短,這樣能夠最大限度地保證不被其餘平常雜事影響每週的更新計劃。但願你們諒解:)
本文的源碼地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo4。
做者:Zachary
出處:https://zacharyfan.com/archives/134.html
▶關於做者:張帆(Zachary,我的微信號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。歡迎掃描右側的二維碼~。
按期發表原創內容:架構設計丨分佈式系統丨產品丨運營丨一些思考。
若是你是初級程序員,想提高但不知道如何下手。又或者作程序員多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注個人公衆號「跨界架構師」,回覆「技術」,送你一份我長期收集和整理的思惟導圖。
若是你是運營,面對不斷變化的市場一籌莫展。又或者想了解主流的運營策略,以豐富本身的「倉庫」。歡迎關注個人公衆號「跨界架構師」,回覆「運營」,送你一份我長期收集和整理的思惟導圖。