如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文

 

 

本系列所有文章

如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念

如何一步一步用DDD设计一个电商网站(二)—— 项目架构

如何一步一步用DDD设计一个电商网站(三)—— 初涉核心域

如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户

如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发

如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文

如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文

如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成

如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑

如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车

如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备

如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单

如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展

 

阅读目录

 

一、前言

  前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西。比如促销、会员价等,在我们的第一篇文章(如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念)中规划的上下文映射图可以看到,这些都属于一个独立的上下文(售价上下文)。

 

二、如何在一个项目中实现多个上下文的业务

  一般情况下,为了更好的分而治之,把不同的上下文作为单独的service,然后通过rpc框架(如WCF)来对其访问是个比较常见的做法。但是在一些小型团队中,虽然划分出了不同上下文,但是我们的开发团队还是同一个。在这种情况下,我个人一般的做法是直接在同一个解决方案中建立不同的项目去做,但是这里需要在解决方案中明确的划分好不同上下文之间的边界,通过代码审核等手段管理好这个边界不被破坏。

  

                      【图1】

  增加的几个项目如图1所示。

  

三、售价上下文与购买上下文的集成

  根据我们第一篇如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念所定义的上下文映射图和9种集成模式可以看出,这2个上下文在同一个子域中,并且在我们实际业务场景中,这2者又是相辅相成,所以售价上下文和购买上下文是一种合作关系。确立这个关系之后,那么这个促销的计算逻辑到底是放到哪个上下文种做更合适呢?我们先整理一下几种可能的方式:

  1.购买上下文把购物车中的商品信息丢给售价上下文 --> 售价上下文进行计算 --> 把结果再返回给购买上下文。

  2.购买上下文从销价上下文获取相关会员价和促销信息 --> 再本地的购物车对象基础上进行运算,并直接可运用结果。

  3.再抽出一个专门的计算服务(隶属于售价上下文),去做这个计算的动作。购买上下文把购物车中的商品信息丢给计算服务 --> 计算上下文从销价上下文获取到相关会员价和促销信息 --> 计算 --> 返回结果给购买上下文

  我相信1和2是比较主流的2个方式。但是方式2是把售价上下文仅作为一种数据的提供方,这就把合作关系变成了一个上下游的关系,并且这种方式使得促销规则和购物车强耦合到了一起,不利于促销规则的变化。在这里售价上下文只起了一个简单的数据维护作用,无法完全控制“售价”的定义,没有很好的做到职责分离。方式1和3对购买上下文来说其实是没有区别的,只是方式3让整个数据交互的链路多了一层,会产生额外的开销,好处是服务的粒度更细了,需要结合实际情况权衡一下得失。这里我选择1方式来实现,因为我们在项目初期,还是尽可能的减少非业务目的的拆分导致的额外成本。

  好了,确定了集成方式之后,先把2个上下文之间用于数据交互的DTO模型定一下,如下图2(售价上下文的DTO模型),图3(购买上下文中与前者对应的值对象)。

 

                     【图2】

                  【图3】

  另外在图3中可以发现增加了一个ISellingPriceService,抽象了与售价上下文的交互。那么我们在Mall.Infrastructure.Translators项目中增加对这个上下文的防腐层处理,老3样SellingPriceAdapter(发起上下文数据请求的适配器)、SellingPriceService(实现ISellingPriceService)、SellingPriceTranslator(把远程数据对象转换成本地的值对象),代码很简单大家可以在源码中查看。需要注意的是,这里的Mall.Infrastructure.Translators项目仅增加了对Mall.Application.SellingPrice项目的引用,类似于把它当作一个远程资源来对待(按上面所说,如果实际由不同的团队负责可以物理上的分离到2个解决方案中)。

  最后创建一个CartService,里面的GetCart()方法——获取购物车信息,来作为调用发起方。这其中的实现使用了最简单的方式,本地不做任何的数据冗余,代码如下:

    public class CartService
    {
        private readonly static ConfirmUserCartExistedDomainService _confirmUserCartExistedDomainService = new ConfirmUserCartExistedDomainService();

        public CartDTO GetCart(string userId)
        {
            var cart = _confirmUserCartExistedDomainService.GetUserCart(userId);

            if (cart.IsEmpty())
            {
                return null;
            }

            var sellingPriceCart = DomainRegistry.SellingPriceService().Calculate(cart);
            return ConvertToCart(cart, sellingPriceCart);
        }

        private CartDTO ConvertToCart(Cart cart, SellingPriceCart sellingPriceCart)
        {
            return new CartDTO
            {
                CartItemGroups = sellingPriceCart.CalculatedFullGroups.Select(ent => new CartItemGroupDTO
                {
                    CartItems = ent.CalculatedCartItems.Select(e => ConvertToCartItem(e, cart.GetCartItem(e.ProductId))).ToArray(),
                    ReducePrice = ent.ReducePrice
                }).ToArray(),
                CartItems = sellingPriceCart.CalculatedCartItems.Select(ent => ConvertToCartItem(ent, cart.GetCartItem(ent.ProductId))).ToArray()
            };
        }

        private CartItemDTO ConvertToCartItem(SellingPriceCartItem sellingPriceCartItem, CartItem cartItem)
        {
            var product = DomainRegistry.ProductService().GetProduct(cartItem.ProductId);
            return new CartItemDTO
            {
                ProductId = cartItem.ProductId,
                ProductName = product == null ? "商品已失效" : product.SaleName,
                ReducePrice = sellingPriceCartItem.ReducePrice,
                SalePrice = cartItem.Price
            };
        }
    }    

 

 四、结语

  这次有个全局改动这里提一下,我在本次编码中把之前所有的Guid标识全部改为了string类型,弱化了对唯一标识的数据类型约束,提高可扩展性(如自增字段、其它自定义的唯一标识等),另外还把购物项中的Price改为了UnitPrice,让语义更加清晰。本篇内容比较粗,欢迎大家探讨。

 

 

本文的源码地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo6

 

 

作者:Zachary
出处:https://zacharyfan.com/archives/147.html

 

 

▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

 

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

posted @ 2016-11-30 06:43  Zachary_Fan  阅读(8995)  评论(3编辑  收藏  举报