如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户
本系列所有文章
如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念
如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户
如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发
如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文
如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑
如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备
如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单
如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展
阅读目录
一、前言
上篇中我们讲述了“把商品卖给用户”中的商品和用户的初步设计。现在把剩余的“卖”这个动作给做了。这里提醒一下,正常情况下,我们的每一步业务设计都需要和领域专家进行沟通,尽可能的符合通用语言的表述。这里的领域专家包括但不限于当前开发团队中对这块业务最了解的开发人员、系统实际的使用人等。
二、怎么卖
如果在没有结合当前上下文的情况下,用通用语言来表述,我们很容易把代码写成下面的这个样子(其中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 <= )
throw new ArgumentException("quantity不能小于等于0", "quantity"); if (quantity < )
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);
}
}
三、领域服务的使用
领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务了。
1.列举几个领域服务适用场景
A.执行一个显著的业务操作过程。
B.对领域对象进行转换。
C.以多个领域对象作为输入进行计算,结果产生一个值对象。
D.隐藏技术细节,如持久化与缓存之间的依存关系。
2.不要把领域服务作为“银弹”。过多的非必要的领域服务会使项目从面向对象变成面向过程,导致贫血模型的产生。
3.可以不给领域服务创建接口,如果需要创建则需要放到相关聚合、实体、值对象的同一个包(文件夹)中。服务的实现可以不仅限于存在单个项目中。
四、回到现实
按照这样设计之后我们的应用层代码变为:
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 }
cart.AddCartItem(cartItem);
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();
}
五、结语
这是最简单的购买流程,后续我们会慢慢充实整个购买的业务,包括会员价、促销等等。我还是保持每一篇内容的简短,这样可以最大限度地保证不被其他日常琐事影响每周的更新计划。希望大家谅解:)
本文的源码地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo4。
作者:Zachary
出处:https://zacharyfan.com/archives/134.html
▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。
定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。
如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。
如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。
如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户的更多相关文章
- 如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑
阅读目录 前言 场景1的思考 场景2的思考 避坑方式 实践 结语 一.前言 在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码: public interfa ...
- 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...
- 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...
- 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...
- 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文
阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...
- 如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发
阅读目录 前言 单元测试 纠正错误,重新出发 结语 一.前言 实际编码已经写了2篇了,在这过程中非常感谢有听到观点不同的声音,借着这个契机,今天这篇就把大家提出的建议一个个的过一遍,重新整理,重新出发 ...
- 如何一步一步用DDD设计一个电商网站(三)—— 初涉核心域
一.前言 结合我们本次系列的第一篇博文中提到的上下文映射图(传送门:如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念),得知我们这个电商网站的核心域就是销售子域.因为电子商务是以信息网络 ...
- 如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备
阅读目录 前言 准备 实现 结语 一.前言 最近实在太忙,上周停更了一周.按流程一步一步走到现在,到达了整个下单流程的最后一公里——结算页的处理.从整个流程来看,这里需要用户填写的信息是最多的,那么 ...
- 如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单
阅读目录 前言 解决数据一致性的方案 回到DDD 设计 实现 结语 一.前言 之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍.现在来到了这最后一个环节,提交订单.单从业务上看,这个 ...
随机推荐
- Azure Service Fabric 开发环境搭建
微服务体系结构是一种将服务器应用程序构建为一组小型服务的方法,每个服务都按自己的进程运行,并通过 HTTP 和 WebSocket 等协议相互通信.每个微服务都在特定的界定上下文(每服务)中实现特定的 ...
- 文档对象模型DOM通俗讲解
转自:http://www.jb51.net/article/42671.htm 在开始之前先说一点,DOM是非常容易理解的,但是大家说的太官方,让人很是难于理解,我们就用非常简单的语言翻译一遍.加深 ...
- 三星Note 7停产,原来是吃了流程的亏
三星Note 7发售两个月即成为全球噩梦,从首炸到传言停产仅仅47天.所谓"屋漏偏逢连天雨",相比华为.小米等品牌对其全球市场的挤压.侵蚀,Galaxy Note 7爆炸事件这场连 ...
- https 安全验证问题
最近为了满足苹果的 https 要求, 经过努力终于写出了方法 验证 SSL 证书是否满足 ATS 要求 nscurl --ats-diagnostics --verbose https://你的域名 ...
- CentOS:ECDSA host key "ip地址" for has changed and you have requested strict checking(转)
原文地址:http://blog.csdn.net/ausboyue/article/details/52775281 Linux SSH命令错误:ECDSA host key "ip地址& ...
- Merge Sorted Array
Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 as one sorted array. Note:Yo ...
- SQL Server2014 SP2关键特性
SQL Server2014 SP2关键特性 转载自:https://blogs.msdn.microsoft.com/sqlreleaseservices/sql-2014-service-pack ...
- 山寨Unity3D?搜狐畅游的免费开源游戏引擎Genesis-3D
在CSDN上看到了<搜狐畅游发布3D游戏引擎Genesis-3D 基于MIT协议开源>(http://www.csdn.net/article/2013-11-21/2817585-cha ...
- mono for android Json 上传文件
void button_Click(object sender, EventArgs e) { string Url = "上传地址,服务器端负责接收"; byte[] fbyte ...
- SQL SERVER全面优化-------Expert for SQL Server 诊断系列
现在很多用户被数据库的慢的问题所困扰,又苦于花钱请一个专业的DBA成本太高.软件维护人员对数据库的了解又不是那么深入,所以导致问题迟迟不能解决,或只能暂时解决不能得到根治.开发人员解决数据问题基本又是 ...