本系列所有文章
如何一步一步用DDD設計一個電商網站(一)—— 先理解核心概念
如何一步一步用DDD設計一個電商網站(四)—— 把商品賣給用戶
如何一步一步用DDD設計一個電商網站(五)—— 停下腳步,重新出發
如何一步一步用DDD設計一個電商網站(六)—— 給購物車加點料,集成售價上下文
如何一步一步用DDD設計一個電商網站(七)—— 實現售價上下文
如何一步一步用DDD設計一個電商網站(八)—— 會員價的集成
如何一步一步用DDD設計一個電商網站(九)—— 小心陷入值對象持久化的坑
如何一步一步用DDD設計一個電商網站(十)—— 一個完整的購物車
如何一步一步用DDD設計一個電商網站(十一)—— 最后的准備
如何一步一步用DDD設計一個電商網站(十二)—— 提交並生成訂單
如何一步一步用DDD設計一個電商網站(十三)—— 領域事件擴展
閱讀目錄
一、前言
最近實在太忙,上周停更了一周。按流程一步一步走到現在,到達了整個下單流程的最后一公里——結算頁的處理。從整個流程來看,這里需要用戶填寫的信息是最多的,那么在后端的設計中如何考慮到業務邊界的划分,和相互之間的交互復雜度,又是我們需要考慮的地方。總體來說本篇講述的內容在前幾篇都有涉及,所以這次一次性處理的業務比較多,已經比較熟練的看官可以跳過本篇。
二、准備
主流的電商設計中結算頁包含以下5個概念:選擇收貨地址、選擇支付方式、選擇快遞、使用優惠券、使用余額和積分。筆者認為,根據我們在本系列的第一篇博文中的上下文映射圖,這背后涉及到了多個上下文的協作:
1.用戶上下文:包含選擇收貨地址
2.支付上下文:包含選擇支付方式、使用余額和積分
3.售價上下文:使用優惠券。
其中第“1”點我的理解是在整個大系統中,收貨地址並不是僅在購買的時候會用到,而是用戶可以直接管理的(一般主流電商都可以在《用戶中心》菜單內操作個人的收貨地址信息),在購物車中進行管理其實並不是一個必須經過的流程,大部分場景下只是在現有地址中做一個選擇,所以收貨地址更接近於用戶域而不是購買域,在購物車的管理可以理解為一個快捷方式而已。
第“2”點,我的理解是,把支付操作相關的概念放到一起,可以做的很靈活,可以和運營打法搭配起來。如:支付方式和使用積分的聯動、像天貓那樣的紅包等促進用戶購買欲望的招式。
第“3”點,我的理解是,優惠券也是會影響到整個商品的售價的,所以它應該屬於售價上下文,配合其它的促銷方式做出更多的打法。
剩下的快遞我認為是本地購買上下文內的概念,因為它只服務於購買的流程之中。
三、實現
根據服務能力來編寫ApplicationService,那么這里總共是提供了3種服務能力,所以定義了3個ApplicationService來提供這些功能:
1.IDeliveryService:其中包含選擇收貨地址和選擇快遞
2.IPaymentService:其中包含選擇支付方式、使用余額和積分
3.ICouponService:包含選擇禮券。
好了接下來就是其中涉及到的領域模型的設計,這里需要糾正一個之前的錯誤,在之前的設計中把余額直接放到了User這個值對象中,並且是從用戶上下文獲取的,現在看看當初的設計不是很妥當。因為余額並不是用戶與生俱來的東西,就好比我要認識一個人,並不一定要知道他有多少錢,但是必然需要知道姓名、年齡等。所以余額與用戶之間並不是一個強依賴關系。而且分屬於2個不同的領域聚合、甚至是上下文。這里涉及的所有領域模型的UML圖如下圖1所示:
【圖1】
其中的值對象都是從遠程上下文獲取的,所以這里在購買上下文里只是使用了其的一個副本。在購買上下文的3個ApplicationService如下:
public interface IDeliveryService { List<ShippingAddressDTO> GetAllShippingAddresses(string userId); Result AddNewShippingAddress(string userId, DeliveryAddNewShippingAddressRequest request); Result EditShippingAddress(string userId, DeliveryEditShippingAddressRequest request); Result DeleteShippingAddress(string id); List<ExpressDTO> GetAllCanUseExpresses(); }
public interface IPaymentService { List<PaymentMethodDTO> GetAllCanUsePaymentMethods(); WalletDTO GetUserWallet(string userId); }
public interface ICouponService { List<CouponDTO> GetAllCoupons(string userId); }
這里接口定義思路是把界面上的操作記錄全部由UI程序做本地緩存/Cookie等,減少服務端的處理壓力,所以接口看上去比較簡單,沒有那些使用禮券,修改使用的收貨地址這類的接口。
另外提一下,在當前的解決方案中的售價上下文中的處理中,增加了2個聚合來處理優惠券相關的業務。
public class Coupon : AggregateRoot { public string Name { get; private set; } public decimal Value { get; private set; } public DateTime ExpiryDate { get; private set; } public List<string> ContainsProductIds { get; private set; } public Coupon(string name, decimal value, DateTime expiryDate, IEnumerable<string> containsProductIds) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException("name"); if (value <= 0) throw new ArgumentException("value不能小於等於0", "value"); if (expiryDate == default(DateTime)) throw new ArgumentException("請傳入正確的expiryDate", "expiryDate"); if (containsProductIds == null) throw new ArgumentNullException("containsProductIds"); this.Name = name; this.Value = value; this.ExpiryDate = expiryDate; this.ContainsProductIds = containsProductIds.ToList(); } }
public class CouponNo : AggregateRoot { public string CouponId { get; private set; } public DateTime UsedTime { get; private set; } public bool IsUsed { get { return UsedTime != default(DateTime) && UsedTime < DateTime.Now; } } public string UserId { get; private set; } public CouponNo(string couponId, DateTime usedTime, string userId) { if (string.IsNullOrWhiteSpace(couponId)) throw new ArgumentNullException("couponId"); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException("userId"); this.CouponId = couponId; this.UsedTime = usedTime; this.UserId = userId; } public void BeUsed() { this.UsedTime = DateTime.Now; } }
其中CouponNo中的CouponId是保持了一個對Coupon聚合ID的引用,在需要的時候從Repository中取出Coupon的信息。部分代碼如下:
var couponNos = DomainRegistry.CouponNoRepository().GetNotUsedByUserId(cart.UserId); var buyProductIds = cart.CartItems.Select(ent => ent.ProductId); List<CouponDTO> couponDtos = new List<CouponDTO>(); foreach (var couponNo in couponNos) { if (couponNo.IsUsed) continue; var coupon = DomainRegistry.CouponRepository().GetByIdentity(couponNo.CouponId); if (coupon.ContainsProductIds.Count == 0 || coupon.ContainsProductIds.Any(ent => buyProductIds.Any(e => e == ent))) { couponDtos.Add(new CouponDTO { CanUse = couponNo.IsUsed, ExpiryDate = coupon.ExpiryDate, ID = couponNo.ID, Name = coupon.Name, Value = coupon.Value }); } }
四、結語
本篇比較簡單不多述了,下面源碼奉上,有興趣的同學自行下載查看全部源碼。
本文的源碼地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo11。
作者:Zachary
出處:https://zacharyfan.com/archives/876.html
▶關於作者:張帆(Zachary,個人微信號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。歡迎掃描右側的二維碼~。
定期發表原創內容:架構設計丨分布式系統丨產品丨運營丨一些思考。
如果你是初級程序員,想提升但不知道如何下手。又或者做程序員多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注我的公眾號「跨界架構師」,回復「技術」,送你一份我長期收集和整理的思維導圖。
如果你是運營,面對不斷變化的市場束手無策。又或者想了解主流的運營策略,以豐富自己的“倉庫”。歡迎關注我的公眾號「跨界架構師」,回復「運營」,送你一份我長期收集和整理的思維導圖。