如何一步一步用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_Fan 出处:http://www.cnblogs.com/Zachary-Fan/p/DDD_7.html

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏人工智能头条

笔记 | 笨方法学Python

1895
来自专栏企鹅号快讯

用python爬取自己的朋友圈,得到的信息超过你的想象!

微信作为一款拥有将近9亿用户的超级APP,已经成为很多人生活中不可或缺的一部分,聊天、分享动态、阅读资讯、购物支付……微信就像一张移动互联网的身份证,拥有它就能...

53110
来自专栏前端儿

当角色转换为面试官之后

昨日,HR小姐姐突然通知,今天要面试15个候选人,19届的校招儿,每位15分钟左右,而且只有一个是现场面,其他都电面。

912
来自专栏谦谦君子修罗刀

程序员面试闪充--简历书写

对于职场来说,简历就如同门面。若是没想好,出了差错,耽误些时日倒不打紧,便是这简历入不了HR的眼,费力伤神还不能觅得好去处,这数年来勤学苦练的大好光阴,岂不辜负...

3505
来自专栏腾讯IVWEB团队的专栏

腾讯 Web 工程师的前端书单

2014年一月以来,自己接触web前端开发已经两年多了,记录一下自己前端学习路上看过的,以及道听途说的一些书,基本上按照由浅入深来介绍。

19K11
来自专栏程序员互动联盟

编程菜鸟如何写出高质量的代码?

疑惑一 公司老手对编程实习生没什么耐心? 其实不然,不是没有耐心。谁不是从无知到认知再到熟悉的一个过程。老手他是天生会吗?遇到老手不耐烦的情况我们要分析分析,一...

33010
来自专栏斑斓

《AngularJS深度剖析与最佳实践》推荐序

这是一本具有强烈ThoughtWorks项目风格的书。书中打造的实战项目,完全遵循了ThoughtWorks工程实践,一步一步从最初的Skeleton通过快速迭...

3046
来自专栏我是攻城师

如何学好一门编程语言?

4995
来自专栏大数据钻研

为什么编程那么难?是我脑子不行吗?

 “现在,导航到那个你希望程序在终端中打开的小文件夹。好的,下面需要在PATH文件夹中创建符号链接,但是,在我们创建之前,先使用nano ~/.bash_pro...

2923
来自专栏程序员宝库

九年程序人生

一次做规划局的项目,规划局的职员很是钦佩地说:“你们真了不起,在电脑上敲敲键盘就能做出软件来。”,

1062

扫码关注云+社区

领取腾讯云代金券