閱讀目錄 前言 準備 實現 結語 一、前言 最近實在太忙,上周停更了一周。按流程一步一步走到現在,到達了整個下單流程的最後一公裡——結算頁的處理。從整個流程來看,這裡需要用戶填寫的信息是最多的,那麼在後端的設計中如何考慮到業務邊界的劃分,和相互之間的交互複雜度,又是我們需要考慮的地方。總體來說本篇講 ...
閱讀目錄
一、前言
最近實在太忙,上周停更了一周。按流程一步一步走到現在,到達了整個下單流程的最後一公裡——結算頁的處理。從整個流程來看,這裡需要用戶填寫的信息是最多的,那麼在後端的設計中如何考慮到業務邊界的劃分,和相互之間的交互複雜度,又是我們需要考慮的地方。總體來說本篇講述的內容在前幾篇都有涉及,所以這次一次性處理的業務比較多,已經比較熟練的看官可以跳過本篇。
二、準備
主流的電商設計中結算頁包含以下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_Fan
出處:http://www.cnblogs.com/Zachary-Fan/p/DDD_11.html