如何一步一步用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



原创文章,转载请注明本文链接: https://zacharyfan.com/archives/147.html

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

微信公众号

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

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

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

Leave a Reply

发表评论

电子邮件地址不会被公开。 必填项已用*标注

ZacharyFan.com © 2019 | WordPress Theme: BlogGem by TwoPoints.