如何一步一步用DDD設計一個電商網站(十二)—— 提交並生成訂單


本系列所有文章

如何一步一步用DDD設計一個電商網站(一)—— 先理解核心概念

如何一步一步用DDD設計一個電商網站(二)—— 項目架構

如何一步一步用DDD設計一個電商網站(三)—— 初涉核心域

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

如何一步一步用DDD設計一個電商網站(五)—— 停下腳步,重新出發

如何一步一步用DDD設計一個電商網站(六)—— 給購物車加點料,集成售價上下文

如何一步一步用DDD設計一個電商網站(七)—— 實現售價上下文

如何一步一步用DDD設計一個電商網站(八)—— 會員價的集成

如何一步一步用DDD設計一個電商網站(九)—— 小心陷入值對象持久化的坑

如何一步一步用DDD設計一個電商網站(十)—— 一個完整的購物車

如何一步一步用DDD設計一個電商網站(十一)—— 最后的准備

如何一步一步用DDD設計一個電商網站(十二)—— 提交並生成訂單

如何一步一步用DDD設計一個電商網站(十三)—— 領域事件擴展

 

 

閱讀目錄

 

一、前言

  之前的十一篇把用戶購買商品並提交訂單整個流程上的中間環節都過了一遍。現在來到了這最后一個環節,提交訂單。單從業務上看,這個動作的背后包含了多個業務操作,根據用戶填寫的訂單信息生成訂單、扣除使用的余額和積分、使用選擇的禮券等等。其中涉及到多個上下文的操作,包括新引入的訂單上下文,那么如何同時與多個上下文進行數據的寫入操作是本篇主要想討論的問題。

 

二、解決數據一致性的方案

  分布式系統中的多個子系統之間的同時寫入問題,也就是所謂的數據一致性問題。講解決數據一致性方案的文章比較多,我就不贅述了,其中的根本是CAP理論,大家可自行百度/Google下。總結一下一般在分布式場景中無非就是兩種方式來解決:2階段提交的強一致性(選擇CP)或者最終一致性(選擇AP)。2階段提交大家都懂,是性能殺手,阻塞式的操作會導致整個系統的瓶頸提早到來。最終一致性是非阻塞式的異步機制,通過消息體在多個系統內流轉,並各自根據消息體來處理不同的業務,並且最終一致性有很多種形式來實現,這里暫不展開討論。

 

三、回到DDD

  在DDD中實現最終一致性需要引入一個之前一直沒提到的概念:領域事件。

  問1:什么是領域事件?

  答:領域事件是領域的一部分,表示領域中所發生的事情。

  問2:它存在的作用是?

  答:①作為實現最終一致性的載體

    ②解耦

    ③通過事件讓不同的上下文分散處理下游業務,減少對數據的反向獲取。處理單元更小化。  

    ④對開閉原則(OCP:Open-Closed Principle)最好體現。

  問3:那么我們如何運用到DDD中?

  答:①哪怕是同一個上下文中的不同聚合也需要通過領域事件來進行同步。

    ②把領域事件設計成聚合,但是其中的大部分代表事件發生與過去的部分屬性應該為只讀。設計為聚合擁有了唯一標識這樣便於跟蹤事件、持久化和跨限界上下文交互。

    ③使用發布 —— 訂閱的方式來處理事件,降低耦合。

    ④有時,有必要使用領域服務來注冊事件訂閱方。這樣的動機可能和讓應用服務來注冊訂閱方一樣,但是此時我們可能有特定於領域的原因。

    ⑤領域事件的一個經驗法則是這樣的:領域事件中所包含的信息應該滿足80%的消費方,雖然對於很多消費方來說,這些信息是多余的。

 

四、設計

  根據上面的描述,設計了以下的幾個對象進行實現領域事件的發布和訂閱,如下圖1:

                  【圖1】

  DomainEventBus是一個單例。事件(繼承自DomainEvent)的發布全部經由它來處理,分發失敗的時候會拋出一個DistributeExceptionEvent的事件,由調用方決定后續的處理方式。另外事件訂閱者(繼承自DomainEventSubscriber)也通過DomainEventBus來注冊訂閱。類型依賴圖如下圖2:

                      【圖2】

 

五、實現

  為了能夠比較直觀的表達當前這個提交訂單業務操作的處理流程,我粗略畫了個時序圖,如下圖3。

                          【圖3】

  這里的事件發布是訂單上下文內的一個組件,是一個進程內操作。另外事件具體發布的目的地由不同的訂閱者控制,暫時就列出了2個。

  好了根據上面的時序圖描述,下面貼出其中的核心代碼:

  1.事件訂閱

            var types = Assembly.Load("Mall.Domain.Order.DomainEventSubscribers").GetTypes().Where(ent => !ent.IsGenericType && ent.GetInterface(typeof(IDomainEventSubscriber).FullName) != null).ToList();
            foreach (var type in types)
            {
                var subscriberInstance = Activator.CreateInstance(AppDomain.CurrentDomain, type.Assembly.FullName, type.FullName).Unwrap();
                var subscriber = (IDomainEventSubscriber)subscriberInstance;
                DomainEventBus.Instance().Subscribe(subscriber);
            }

  2.和2個對訂單創建事件的訂閱者

    public class OrderCreatedSubscriberPaymentContext : DomainEventSubscriber<OrderCreated>
    {
        public override void HandleEvent(OrderCreated domainEvent)
        {
            //TODO anything

            throw new NotImplementedException();
        }
    }
    public class OrderCreatedSubscriberSellingPriceContext : DomainEventSubscriber<OrderCreated>
    {
        public override void HandleEvent(OrderCreated domainEvent)
        {
            //TODO anything
            throw new System.NotImplementedException();
        }
    }

  3.事件發布

        public Result Create(OrderRequest orderRequest)
        {
            if (!string.IsNullOrWhiteSpace(orderRequest.CouponId))
            {
                var couponResult = DomainRegistry.SellingPriceService().IsCouponCanUse(orderRequest.CouponId, orderRequest.OrderTime);
                if (!couponResult.IsSuccess)
                    return Result.Fail(couponResult.Msg);
            }

            var orderId = DomainRegistry.OrderRepository().NextIdentity();
            var order = Domain.Order.Aggregate.Order.Create(orderId, orderRequest.UserId, orderRequest.Receiver,
                orderRequest.CountryId, orderRequest.CountryName, orderRequest.ProvinceId, orderRequest.ProvinceName,
                orderRequest.CityId, orderRequest.CityName, orderRequest.DistrictId, orderRequest.DistrictName,
                orderRequest.Address, orderRequest.Mobile, orderRequest.Phone, orderRequest.Email,
                orderRequest.PaymentMethodId, orderRequest.PaymentMethodName, orderRequest.ExpressId,
                orderRequest.ExpressName, orderRequest.Freight, orderRequest.CouponId, orderRequest.CouponName, orderRequest.CouponValue, orderRequest.OrderTime);

            foreach (var orderItemRequest in orderRequest.OrderItems)
            {
                order.AddOrderItem(orderItemRequest.ProductId, orderItemRequest.Quantity, orderItemRequest.UnitPrice, orderItemRequest.JoinedMultiProductsPromotionId, orderItemRequest.ProductName);
            }

            DomainRegistry.OrderRepository().Save(order); DomainEventBus.Instance().Publish(new OrderCreated(order.ID, order.UserId, order.Receiver));
            return Result.Success();
        }

  注意其中標紅的部分,暫時沒有考慮出現異常的情況。另外這里的OrderCreated事件只是象征性的寫一下,實際的事件需要哪些屬性,只要貫徹好二八原則,設計一個滿足80%場景下的直接可用,剩下的20%可以增加一些查詢來滿足實際業務需要。 

 

六、結語

  如果說領域對象、應用層、倉儲層等這些概念還和傳統的三層架構傻傻分不清楚的話。那么領域事件應該是整個DDD中最容易理解的一部分概念,因為這一部分是獨立於傳統的三層架構之外的完全不同的部分,也是整個DDD設計中低耦合的關鍵。本篇先進行了一個對領域事件最簡單的實現,主要闡述了領域事件在整個項目設計過程中的作用和運用的方式。這是一個基礎,在這個基礎之上已經有很多成熟的解決方案可以讓我們的系統做的更好。下篇會主要講關於異常的處理(上文中標紅的那部分),數據一致性的保證等更好的提高系統可用性的部分。謝謝各位看官。

 

 

本文完整的源碼地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo12

 

作者:Zachary
出處:https://zacharyfan.com/archives/192.html

 

 

▶關於作者:張帆(Zachary,個人微信號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。歡迎掃描右側的二維碼~。

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

 

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

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


免責聲明!

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



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