閱讀目錄 前言 明確業務細節 建模 實現 結語 一、前言 上一篇我們已經確立的購買上下文和銷售上下文的交互方式,傳送門在此:http://www.cnblogs.com/Zachary-Fan/p/DDD_6.html,本篇我們來實現售價上下文的具體細節。 二、明確業務細節 電商市場越來越成熟,競爭 ...
閱讀目錄
一、前言
上一篇我們已經確立的購買上下文和銷售上下文的交互方式,傳送門在此: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_Fan
出處:http://www.cnblogs.com/Zachary-Fan/p/DDD_7.html