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

1、前言

  上篇中咱们讲述了“把商品卖给用户”中的商品和用户的初步设计。如今把剩余的“卖”这个动做给作了。这里提醒一下,正常状况下,咱们的每一步业务设计都须要和领域专家进行沟通,尽量的符合通用语言的表述。这里的领域专家包括但不限于当前开发团队中对这块业务最了解的开发人员、系统实际的使用人等。

 

2、怎么卖

  若是在没有结合当前上下文的状况下,用通用语言来表述,咱们很容易把代码写成下面的这个样子(其中DomainRegistry只是一个简单的工厂,解耦应用层与其余具体实现的依赖,内部也可使用IOC容器来实现):

 

            var user = DomainRegistry.UserService().GetUser(userId);
            if (user == null)
            {
                return Result.Fail("未找到用户信息");
            }

            var product = DomainRegistry.ProductService().GetProduct(productId);
            if (product == null)
            {
                return Result.Fail("未找到产品信息");
            }

            user.Buy(product, quantity);
            return null;    

  

  初步来看,好像很合理。这里表达出的是“用户购买了商品”这个语义。而后继续往下写,咱们会发现购买了以后应该怎么办呢,要把东西放到购物车啊。这里又出现了购物车,我认为购物车是咱们销售子域中的一个核心概念,它也是整个用户购买过程当中变化最频繁的一个对象。咱们来梳理一下,一个最简单的购物车至少包含哪些东西:

  A.一个购物车必须是属于一个用户的。

  B.一个购物车内必然包含购买的商品的相关信息。

  首先咱们思考一下如何在咱们的购物车中表达出用户的概念,购物车须要知道用户的全部信息吗?答案在大部分场景下应该是否认的,由于在用户挑选商品并加到购物车的这个过程当中,整个购物车是不稳定的,那么其实在用户想要进行结算之前,咱们只须要知道这个购物车是谁的,仅此而已。那么这里咱们已经排除了一种方式是购物车直接持有User的引用。因此说对于购物车来讲,在咱们排除为性能而进行数据冗余的状况下,咱们只须要保持一个用户惟一标识的引用便可。

  购物车明细和商品之间的关系也是同样,每次须要从远程上下中获取到最新的商品信息(如价格等),故也仅需保持一个惟一标识的引用。

  结合上一篇讲的,咱们目前已经出现了如下几个对象,见【图1,点击图片查看原图】。

 

                       【图1】

 下面贴上购物车和购物车明细的简单实现。

 

    public class Cart : Infrastructure.DomainCore.Aggregate
    {
        private readonly List<CartItem> _cartItems;

        public Guid CartId { get; private set; }

        public Guid UserId { get; private set; }

        public DateTime LastChangeTime { get; private set; }

        public Cart(Guid cartId, Guid userId, DateTime lastChangeTime)
        {
            if (cartId == default(Guid))
                throw new ArgumentException("cartId 不能为default(Guid)", "cartId");

            if (userId == default(Guid))
                throw new ArgumentException("userId 不能为default(Guid)", "userId");

            if (lastChangeTime == default(DateTime))
                throw new ArgumentException("lastChangeTime 不能为default(DateTime)", "lastChangeTime");

            this.CartId = cartId;
            this.UserId = userId;
            this.LastChangeTime = lastChangeTime;
            this._cartItems = new List<CartItem>();
        }

        public void AddCartItem(CartItem cartItem)
        {
            var existedCartItem = this._cartItems.FirstOrDefault(ent => ent.ProductId == cartItem.ProductId);
            if (existedCartItem == null)
            {
                this._cartItems.Add(cartItem);
            }
            else
            {
                existedCartItem.ModifyQuantity(existedCartItem.Quantity + cartItem.Quantity);
            }
        }
    }

 

   public class CartItem : Infrastructure.DomainCore.Entity
    {
        public Guid ProductId { get; private set; }

        public int Quantity { get; private set; }

        public decimal Price { get; private set; }

        public CartItem(Guid productId, int quantity, decimal price)
        {
            if (productId == default(Guid))
                throw new ArgumentException("productId 不能为default(Guid)", "productId");

            if (quantity <= 0)
                throw new ArgumentException("quantity不能小于等于0", "quantity");

            if (quantity < 0)
                throw new ArgumentException("price不能小于0", "price");

            this.ProductId = productId;
            this.Quantity = quantity;
            this.Price = price;
        }

        public void ModifyQuantity(int quantity)
        {
            this.Quantity = quantity;
        }
    }

 

  回到咱们最上面的代码中的“user.Buy(product, quantity);” 的问题。在DDD中主张的是清晰的业务边界,在这里,咱们目前的定义致使的结果是User与Cart产生了强依赖,让User内部须要知道过多的Cart的细节,而这些是User不该该知道的。这里还有一个问题是在领域对象内部去访问仓储(或者调用远程上下文的接口)来获取数据并非一种提倡的方式,他会致使事务管理的混乱。固然有人会说,把Cart做为一个参数传进来,这看上去是个好主意,解决了在领域对象内部访问仓储的问题,然而看一下接口的定义,用户购买商品和购物车?仍是用户购买商品而且放入到购物车?这样来看这个方法作的事情彷佛过多了,违背了单一职责原则。

  其实在大部分语义中使用“用户”做为一个主体对象,看上去也都还挺合理的,然而细细的去思考当前上下文(系统)的核心价值,会发现“用户”有时并非核心,固然好比是一个CRM系统的话核心便是“用户”。

  总结一下这种方式的缺点:

  A.领域对象之间的耦合太高,项目中的对象容易造成蜘蛛网结构的引用关系。

  B.须要在领域对象内部调用仓储,不利于最小化事务管理。

  C.没法清晰的表达出通用语言的概念。

  从新思考这个方法。“购买”这个概念更合理的描述是在销售过程当中所发生的一个操做过程。在咱们电商行业下,能够表述为“用户购买了商品”和“商品被加入购物车”。这时候须要领域服务出场了,由它来表达出“用户购买商品”这个概念最为合适不过了。其实就是把应用层的代码搬过来了,如下是对应的代码: 

 

    public class UserBuyProductDomainService
    {
        public CartItem UserBuyProduct(Guid userId, Guid productId, int quantity)
        {
            var user = DomainRegistry.UserService().GetUser(userId);
            if (user == null)
            {
                throw new ApplicationException("未能获取用户信息!");
            }

            var product = DomainRegistry.ProductService().GetProduct(productId);
            if (product == null)
            {
                throw new ApplicationException("未能获取产品信息!");
            }

            return new CartItem(productId, quantity, product.SalePrice);
        }
    }

3、领域服务的使用

  领域中的服务表示一个无状态的操做,它用于实现特定于某个领域的任务。当某个操做不适合放在聚合和值对象上时,最好的方式即是使用领域服务了。

1.列举几个领域服务适用场景

    A.执行一个显著的业务操做过程。

    B.对领域对象进行转换。

    C.以多个领域对象做为输入进行计算,结果产生一个值对象。

  D.隐藏技术细节,如持久化与缓存之间的依存关系。

2.不要把领域服务做为“银弹”。过多的非必要的领域服务会使项目从面向对象变成面向过程,致使贫血模型的产生。

3.能够不给领域服务建立接口,若是须要建立则须要放到相关聚合、实体、值对象的同一个包(文件夹)中。服务的实现能够不只限于存在单个项目中。

 

4、回到现实

  按照这样设计以后咱们的应用层代码变为:

 

1             var cartItem = _userBuyProductDomainService.UserBuyProduct(userId, productId, quantity);
2             var cart = DomainRegistry.CartRepository().GetOfUserId(userId);
3             if (cart == null)
4             {
5                 cart = new Cart(DomainRegistry.CartRepository().NextIdentity(), userId, DateTime.Now);
6             }
7             cart.AddCartItem(cartItem);
8             DomainRegistry.CartRepository().Save(cart);    

 

  这里的第5行用到了一个仓储(资源库)CartRepository,仓储算是DDD中比较好理解的概念。在DDD中仓储的基本思想是用面向集合的方式来体现,也就是至关于你在和一个List作操做,因此切记不能把任何的业务信息泄露到仓储层去,它仅用于数据的存储。仓储的广泛使用方式以下:

  A.包含保存、删除、指定条件的查询(固然在大型项目中能够考虑采用CQSR来作,把查询和数据操做分离)。

  B.只为聚合建立资源库

  C.一般资源库与聚合式 1对1的关系,然而有时,当2个或者多个聚合位于同一个对象层级中时,它们能够共享同一个资源库。 

  D.资源库的接口定义和聚合放在相同的模块中,实现类放在另外的包中(为了隐藏对象存储的细节)。

  回到代码中来,标红的那部分也能够用一个领域服务来实现,隐藏“若是一个用户没有购物车的状况下新建一个购物车”的业务细节。

 

    public class GetUserCartDomainService
    {
        public Cart GetUserCart(Guid userId)
        {
            var cart = DomainRegistry.CartRepository().GetOfUserId(userId);
            if (cart == null)
            {
                cart = new Cart(DomainRegistry.CartRepository().NextIdentity(), userId, DateTime.Now);
                DomainRegistry.CartRepository().Save(cart);
            }

            return cart;
        }
    }

  这样应用层就真正变成了一个讲故事的人,清晰的表达出了“用户购买商品的整个过程”,把商品购物车的商品转换成购物车明细 --> 获取用户的购物车 --> 添加购物车明细到购物车中 --> 保存购物车。 

        public Result Buy(Guid userId, Guid productId, int quantity)
        {
            var cartItem = _userBuyProductDomainService.UserBuyProduct(userId, productId, quantity);
            var cart = _getUserCartDomainService.GetUserCart(userId);
            cart.AddCartItem(cartItem);
            DomainRegistry.CartRepository().Save(cart);
            return Result.Success();
        }

 

5、结语

  这是最简单的购买流程,后续咱们会慢慢充实整个购买的业务,包括会员价、促销等等。我仍是保持每一篇内容的简短,这样能够最大限度地保证不被其余平常杂事影响每周的更新计划。但愿你们谅解:)

 

 

 

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

 


 

做者:Zachary
出处:https://zacharyfan.com/archives/134.html

 

 

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

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

 

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

若是你是运营,面对不断变化的市场一筹莫展。又或者想了解主流的运营策略,以丰富本身的“仓库”。欢迎关注个人公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思惟导图。

相关文章
相关标签/搜索