一、前言

之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能。本篇准备把剩下的购物车的基本概念一次处理完。

二、回顾

在动手之前我对之前的购买上下文内对象做了一次回顾。先梳理一下已经在上下文内出现的领域对象,如图 1 所示:

10 一个完整的购物车 - 图1

图 1

在梳理的过程中,我把原来 Cart.AddCartItem(string productId, int quantity, decimal price) 重构为了 Cart.AddCartItem(Product product, int quantity),这样的好处的是 2 个:

  1. 更清晰的表述出了在购物车中添加商品的意思。

  2. 约束了外部只能通过 Product 对象来进行商品的添加,这样在 Product 构造函数中的约束在这里无需再次验证(如 salename 不能空等)。

三、梳理

目前的购物车中在操作上的方法只有一个。参照目前主流电商平台的设计,我们需要增加:

  1. 修改数量

  2. 删除

  3. 选择参与的促销(如果存在多个非单品级促销)

  4. 收藏商品

前面 3 个比较简单,都是购物车自身的概念,只有其中第四点超出了购物车自身的范畴,并且笔者认为收藏本就不是购物车特有的概念,而是在任何看得到商品的地方都可以做添加收藏的操作。

那么自然引出了一个新的概念——收藏夹。看下最新的 UML 图,如图 2 所示:

10 一个完整的购物车 - 图2

图 2

我想会有一部分同学在设计收藏夹(Favorites)的时候会以另外的方式来做,比如像下图 3 这样:

10 一个完整的购物车 - 图3

图 3

这里我认为这样考虑的原因可能是由于 DBFirst 的思想导致的,因为图 2 中的 “收藏夹” 仅仅是维护了一个 “用户” 与“收藏项”之间的关系,那么只要在 “收藏项” 上增加一个 UserId 就直接可以省去了这一层关系,并且数据结构更加简单。

这时候我们就需要注意了,千万不能有 DBFirst 思想去影响领域的建模,这样的方式会把 “添加购物项” 这类的业务含义泄露到了 Repository 层或者 Application 层去实现,导致无法用通用语言进行完整的业务描述了。

并且在这个场景下,我个人观点认为,收藏商品其实只是为商品的展示途径中增加了一种途径而已,所以它应该被设计为独立存在的,由它自身来管理这些 “被收藏的商品”,它的存在与否都不影响其它领域对象。

四、实现

要实现这 4 个操作,那么需要在 ICartService 中增加下面 4 个接口:

  1. Result ChangeQuantity(string userId, string id, int quantity);
  2. Result DeleteCartItem(string userId, string id);
  3. Result AddToFavorites(string userId, string productId);
  4. Result ChangeMultiProductsPromotion(string userId, string productId, string selectedMultiProductsPromotionId);

其中的部分实现如下:

  1. public Result AddToFavorites(string userId, string productId)
  2. {
  3. var cart = _confirmUserCartExistedDomainService.GetUserCart(userId);
  4. if (cart.IsEmpty())
  5. {
  6. return Result.Fail("当前购物车中并没有商品");
  7. }
  8. var cartItem = cart.GetCartItem(productId);
  9. if (cartItem == null)
  10. {
  11. return Result.Fail("该购物项已不存在");
  12. }
  13. var favorites = DomainRegistry.FavoritesRepository().GetByUserId(userId) ?? new Favorites(userId, null);
  14. favorites.AddFavoritesItem(cartItem);
  15. DomainRegistry.FavoritesRepository().Save(favorites);
  16. return Result.Success();
  17. }

其中关于 Favorites 的构造函数我是这么做的:

  1. public Favorites(string userId, IEnumerable<FavoritesItem> favoritesItems)
  2. {
  3. if (string.IsNullOrWhiteSpace(userId))
  4. throw new ArgumentNullException("userId");
  5. this.UserId = userId;
  6. this._favoritesItems = new List<FavoritesItem>();
  7. if (favoritesItems != null && favoritesItems.Any())
  8. {
  9. foreach (var favoritesItem in favoritesItems)
  10. {
  11. AddFavoritesItem(favoritesItem);
  12. }
  13. }
  14. }

这样可以重用 AddFavoritesItem 中的一些守卫操作,保证在业务产生变动之后历史数据从 DB 取出来的时候经过一次最新的业务验证,确保数据在流转过程中的合法性。这个方式可以择机运用在任何聚合的构造函数中。

五、结语

本篇主要的观点还是在建模上的思维惯性,抛开 DB,抛开 DB,抛开 DB,重要的事情说 3 遍。

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


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

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

10 一个完整的购物车 - 图4

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

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

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