本系列所有文章
如何一步一步用DDD設計一個電商網站(一)—— 先理解核心概念
如何一步一步用DDD設計一個電商網站(四)—— 把商品賣給用戶
如何一步一步用DDD設計一個電商網站(五)—— 停下腳步,重新出發
如何一步一步用DDD設計一個電商網站(六)—— 給購物車加點料,集成售價上下文
如何一步一步用DDD設計一個電商網站(七)—— 實現售價上下文
如何一步一步用DDD設計一個電商網站(八)—— 會員價的集成
如何一步一步用DDD設計一個電商網站(九)—— 小心陷入值對象持久化的坑
如何一步一步用DDD設計一個電商網站(十)—— 一個完整的購物車
如何一步一步用DDD設計一個電商網站(十一)—— 最后的准備
如何一步一步用DDD設計一個電商網站(十二)—— 提交並生成訂單
如何一步一步用DDD設計一個電商網站(十三)—— 領域事件擴展
閱讀目錄
一、前言
上一篇我們已經確立的購買上下文和銷售上下文的交互方式,傳送門在此:http://www.cnblogs.com/Zachary-Fan/p/DDD_6.html,本篇我們來實現售價上下文的具體細節。
二、明確業務細節
電商市場越來越成熟,競爭也越來越激烈,影響客戶流量的關鍵因素之一就是價格,運營的主要打法之一也是價格,所以是商品價格是一個在電商中很重要的一環。正因為如此也讓促銷演變的越來越復雜,那么如何在編碼上花點心思來盡可能的降低業務的復雜化帶來的影響和提高可擴展性來擁抱變化就變得很重要了。先從最簡單的開始,我瀏覽了某東的促銷,先把影響價格相關的幾個促銷找出來,暫時得出以下幾個結論(這里又要提一下,我們實際工作中應在開始編碼之前要做的就是和領域專家討論促銷的細節):
1.滿減:可以多個商品共同參與,匯總金額達到某個閾值之后減免XX金額。
2.多買優惠(方式1):可以多個商品共同參與,匯總購買數量達到一定數量得到X折的優惠。
3.多買優惠(方式2):可以多個商品共同參與,匯總購買數量達到一定數量減免最便宜的X件商品。
4.限時折扣:直接商品的購買金額被修改到指定值。
5.滿減促銷的金額滿足點以優惠后價格為准,比如該商品既有限時折扣又有滿減,則使用限時折扣的價格來計算金額滿足點。
6.優惠券是在之上的規則計算之后得出的金額基礎下計算金額滿足點。
7.每一個商品的滿減+多買優惠僅能參與一種。並且相同促銷商品在購物車中商品展示的方式是在一組中。
三、建模
根據上面的業務描述先找到其中的幾個領域對象,然后在做一些適當的抽象,得出下面的UML圖(點擊圖片可查看大圖):
【圖1】
四、實現
建模完之后下面的事情就容易了,先梳理一下我們的業務處理順序:
1.根據購買上下文傳入的購物車信息獲取產品的相關促銷。
2.先處理單品促銷。
3.最后處理多商品共同參與的促銷。
梳理的過程中發現,為了能夠實現滿減和多買優惠促銷僅能參與一個,所以需要再購買上下文和售價上下文之間傳遞購物項時增加一個參數選擇的促銷唯一標識(SelectedMultiProductsPromotionId)。
隨后根據上面業務處理順序,發現整個處理的鏈路比較長,那么這里我決定定義一個值對象來承載整個處理的過程。如下:
public class BoughtProduct { private readonly List<PromotionRule> _promotionRules = new List<PromotionRule>(); public string ProductId { get; private set; } public int Quantity { get; private set; } public decimal UnitPrice { get; private set; } public decimal ReducePrice { get; private set; } /// <summary> /// 商品在單品優惠后的單價,如果沒有優惠則為正常購買的單價 /// </summary> public decimal DiscountedUnitPrice { get { return UnitPrice - ReducePrice; } } public decimal TotalDiscountedPrice { get { return DiscountedUnitPrice * Quantity; } } public ReadOnlyCollection<ISingleProductPromotion> InSingleProductPromotionRules { get { return _promotionRules.OfType<ISingleProductPromotion>().ToList().AsReadOnly(); } } public IMultiProductsPromotion InMultiProductPromotionRule { get; private set; } public BoughtProduct(string productId, int quantity, decimal unitPrice, decimal reducePrice, IEnumerable<PromotionRule> promotionRules, string selectedMultiProdcutsPromotionId) { if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentException("productId不能為null或者空字符串", "productId"); if (quantity <= 0) throw new ArgumentException("quantity不能小於等於0", "quantity"); if (unitPrice < 0) throw new ArgumentException("unitPrice不能小於0", "unitPrice"); if (reducePrice < 0) throw new ArgumentException("reducePrice不能小於0", "reducePrice"); this.ProductId = productId; this.Quantity = quantity; this.UnitPrice = unitPrice; this.ReducePrice = reducePrice; if (promotionRules != null) { this._promotionRules.AddRange(promotionRules); var multiProductsPromotions = this._promotionRules.OfType<IMultiProductsPromotion>().ToList(); if (multiProductsPromotions.Count > 0) { var selectedMultiProductsPromotionRule = multiProductsPromotions.SingleOrDefault(ent => ((PromotionRule)ent).PromotoinId == selectedMultiProdcutsPromotionId); InMultiProductPromotionRule = selectedMultiProductsPromotionRule ?? multiProductsPromotions.First(); } } } public BoughtProduct ChangeReducePrice(decimal reducePrice) { if (reducePrice < 0) throw new ArgumentException("result.ReducePrice不能小於0"); var selectedMultiProdcutsPromotionId = this.InMultiProductPromotionRule == null ? null : ((PromotionRule) this.InMultiProductPromotionRule).PromotoinId; return new BoughtProduct(this.ProductId, this.Quantity, this.UnitPrice, reducePrice, this._promotionRules, selectedMultiProdcutsPromotionId); } }
需要注意一下,值對象的不可變性,所以這里的ChangeReducePrice方法返回的是一個新的BoughtProduct對象。另外這次我們的例子比較簡單,單品促銷只有1種。理論上單品促銷是支持疊加參與的,所以這里的單品促銷設計了一個集合來存放。
下面的代碼是處理單品促銷的代碼:
foreach (var promotionRule in singleProductPromotionRules) { var tempReducePrice = ((PromotionRuleLimitTimeDiscount)promotionRule).CalculateReducePrice(productId, unitPrice, DateTime.Now); //在創建的時候約束促銷的重復性。此處邏輯上允許重復 if (unitPrice - reducePrice <= tempReducePrice) { reducePrice = unitPrice; } else { reducePrice += tempReducePrice; } }
這里也可以考慮把它重構成一個領域服務來合並同一個商品多個單品促銷計算結果。
整個應用服務的代碼如下:
public class CalculateSalePriceService : ICalculateSalePriceService { private static readonly MergeSingleProductPromotionForOneProductDomainService _mergeSingleProductPromotionForOneProductDomainService = new MergeSingleProductPromotionForOneProductDomainService(); public CalculatedCartDTO Calculate(CartRequest cart) { List<BoughtProduct> boughtProducts = new List<BoughtProduct>(); foreach (var cartItemRequest in cart.CartItems) { var promotionRules = DomainRegistry.PromotionRepository().GetListByContainsProductId(cartItemRequest.ProductId); var boughtProduct = new BoughtProduct(cartItemRequest.ProductId, cartItemRequest.Quantity, cartItemRequest.UnitPrice, 0, promotionRules, cartItemRequest.SelectedMultiProductsPromotionId); boughtProducts.Add(boughtProduct); } #region 處理單品促銷 foreach (var boughtProduct in boughtProducts.ToList()) { var calculateResult = _mergeSingleProductPromotionForOneProductDomainService.Merge(boughtProduct.ProductId, boughtProduct.DiscountedUnitPrice, boughtProduct.InSingleProductPromotionRules); var newBoughtProduct = boughtProduct.ChangeReducePrice(calculateResult); boughtProducts.Remove(boughtProduct); boughtProducts.Add(newBoughtProduct); } #endregion #region 處理多商品促銷&構造DTO模型 List<CalculatedFullGroupDTO> fullGroupDtos = new List<CalculatedFullGroupDTO>(); foreach (var groupedPromotoinId in boughtProducts.Where(ent => ent.InMultiProductPromotionRule != null).GroupBy(ent => ((PromotionRule)ent.InMultiProductPromotionRule).PromotoinId)) { var multiProdcutsReducePricePromotion = (IMultiProdcutsReducePricePromotion)groupedPromotoinId.First().InMultiProductPromotionRule; //暫時只有減金額的多商品促銷 var products = groupedPromotoinId.ToList(); if (multiProdcutsReducePricePromotion == null) continue; var reducePrice = multiProdcutsReducePricePromotion.CalculateReducePrice(products); fullGroupDtos.Add(new CalculatedFullGroupDTO { CalculatedCartItems = products.Select(ent => ent.ToDTO()).ToArray(), ReducePrice = reducePrice, MultiProductsPromotionId = groupedPromotoinId.Key }); } #endregion return new CalculatedCartDTO { CalculatedCartItems = boughtProducts.Where(ent => fullGroupDtos.SelectMany(e => e.CalculatedCartItems).All(e => e.ProductId != ent.ProductId)) .Select(ent => ent.ToDTO()).ToArray(), CalculatedFullGroups = fullGroupDtos.ToArray(), CartId = cart.CartId }; } }
五、結語
這里的設計沒有考慮促銷規則的沖突問題,如果做的話把它放在創建促銷規則的時候進行約束即可。
本文的源碼地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo7。
作者:Zachary
出處:https://zacharyfan.com/archives/154.html
▶關於作者:張帆(Zachary,個人微信號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。歡迎掃描右側的二維碼~。
定期發表原創內容:架構設計丨分布式系統丨產品丨運營丨一些思考。
如果你是初級程序員,想提升但不知道如何下手。又或者做程序員多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注我的公眾號「跨界架構師」,回復「技術」,送你一份我長期收集和整理的思維導圖。
如果你是運營,面對不斷變化的市場束手無策。又或者想了解主流的運營策略,以豐富自己的“倉庫”。歡迎關注我的公眾號「跨界架構師」,回復「運營」,送你一份我長期收集和整理的思維導圖。