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

来源:http://www.cnblogs.com/Zachary-Fan/archive/2016/11/16/6041374.html
-Advertisement-
Play Games

閱讀目錄 前言 怎麼賣 領域服務的使用 回到現實 結語 閱讀目錄 前言 怎麼賣 領域服務的使用 回到現實 結語 一、前言 上篇中我們講述了“把商品賣給用戶”中的商品和用戶的初步設計。現在把剩餘的“賣”這個動作給做了。這裡提醒一下,正常情況下,我們的每一步業務設計都需要和領域專家進行溝通,儘可能的符合 ...


閱讀目錄

一、前言

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

 

二、怎麼賣

  如果在沒有結合當前上下文的情況下,用通用語言來表述,我們很容易把代碼寫成下麵的這個樣子(其中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_Fan
出處:http://www.cnblogs.com/Zachary-Fan/p/6041374.html

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 個人理解: spring Aop 是什麼:面向切麵編程,類似於自定義攔截操作,支持攔截之前操作@Before,攔截之後操作@After,攔截環繞操作@Around。 什麼情況下使用spring Aop:舉例如下 code案例: applicationContext.xml 配置文件 maven po ...
  • 方法屬於誰 方法要麼屬於類,要麼屬於對象 static修飾的方法屬於類 沒有static修飾的方法屬於對象 方法只能定義在類裡面,不能獨立定義 不能獨立的執行方法,要麼通過類調用,要麼通過方法調用 一個類里,一個方法調用另一個方法,看似沒有調用者,實際上對於非static方法使用this調用,sta ...
  • 一、spring aop execution表達式說明 在使用spring框架配置AOP的時候,不管是通過XML配置文件還是註解的方式都需要定義pointcut"切入點" 例如定義切入點表達式 execution(* com.sample.service.impl..*.*(..)) executi ...
  • 在網上找了很多方法,終於找到了一個,記錄之。 JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean(); factory.setServiceClass(Service1Soap.class);// 設置請求介面 factory.setA... ...
  • 1、錯誤類型:PHP致命錯誤 Error type: PHP Fatal error Fatal error: Cannot redeclare (a) (previously declared in (b)) in (c) on line (d) 2、錯誤描述: 該錯誤報告表示你正企圖對已經定義過 ...
  • urllib requets selenium的應用場景 cookie識別用戶身份和記錄用戶狀態 driver.get_cookies() 獲得cookie信息 add_cookie(cookie_dict) 向cookie添加會話信息 delete_cookie(name) 刪除特定(部分)的co ...
  • 上篇說到了 RabbitMQ 的安裝。 這次要在講案例之前,需要安裝PHP的AMQP擴展。不然可能會報以下兩個錯誤。 1.Fatal error: Class 'AMQPConnection' not found 2. Fatal error: Uncaught exception 'AMQPCon ...
  • RabbitMQ: 1、是實現AMQP(高級消息隊列協議)的消息中間件的一種。 2、主要是為了實現系統之間的雙向解耦而實現的。當生產者大量產生數據時,消費者無法快速消費,那麼需要一個中間層。保存這個數據。 一般提到 RabbitMQ 和消息,都會用到以下一些專有名詞: (1)生產(Producing ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...