如何一步一步用DDD設計一個電商網站(四)—— 把商品賣給用戶


一、前言

  上篇中我們講述了“把商品賣給用戶”中的商品和用戶的初步設計。現在把剩余的“賣”這個動作給做了。這里提醒一下,正常情況下,我們的每一步業務設計都需要和領域專家進行溝通,盡可能的符合通用語言的表述。這里的領域專家包括但不限於當前開發團隊中對這塊業務最了解的開發人員、系統實際的使用人等。

 

二、怎么賣

  如果在沒有結合當前上下文的情況下,用通用語言來表述,我們很容易把代碼寫成下面的這個樣子(其中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)。堅持用心打磨每一篇高質量原創。歡迎掃描右側的二維碼~。

定期發表原創內容:架構設計丨分布式系統丨產品丨運營丨一些思考。

 

如果你是初級程序員,想提升但不知道如何下手。又或者做程序員多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注我的公眾號「跨界架構師」,回復「技術」,送你一份我長期收集和整理的思維導圖。

如果你是運營,面對不斷變化的市場束手無策。又或者想了解主流的運營策略,以豐富自己的“倉庫”。歡迎關注我的公眾號「跨界架構師」,回復「運營」,送你一份我長期收集和整理的思維導圖。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM