废话

之前七七八八看了些DDD相关概念,充血模型、领域事件、领域服务、应用服务等,大致能理解但从未实践。最近在用ABP做个电商模块,尝试用DDD方式来实现购物车功能,感觉还行,下面做个记录。

下面这些内容只是个人理解,未必正确。

领域实体充血模型-定义满足业务规则的实体类

购物车模块涉及到两个实体ShoppingCartEntity(购物车)、ShoppingCartItemEntity(购物车明细),按我之前的做法会直接定义成POCO,伪代码如下:

public class ShoppingCartEntity
{
public long Id { get; set; }
//关联的顾客id
public long CustomerId{ get; set; }
public CustomerEntity Customer { get; set; }
//购物车明细集合
public List<ShoppingCartItemEntity> Items { get; set; } //,...略
}

购物车明细定义就省了,意思就是属性全部get; set; ,它只是用来给EF做映射,做数据库操作,做数据传递用。

但这样的领域实体类无法真是表达业务规则,添加商品到购物车、从购物车移除商品等购物车相关操作,我们可能会放到应用层(或者老式的业务逻辑层BLL),这样无法体现购物车实体的功能,代码复用性也很低。

我们思考下:

  • 如果我们将购物车关联的顾客id设置为只读的,然后通过构造函数来初始化它,因为不希望别人调用我们的购物车对象时将关联的顾客设置为空
  • 如果我们将购物车明细设置为只读的,然后在购物车实体上提供:添加商品到购物车、从购物车移除商品、清空购物车等操作如何?因为购物车明细的变化会影响到购物车金额和积分的统计
  • 如果我们在购物车操作的不同点触发一些事件如何?比如:当将商品添加进购物车时触发一个事件,因为希望将来别人使用我们的购物车模块时能加入它们的业务逻辑。

这样一来,购物车相关的操作都封装进购物车实体,将来应用层的代码就会变得很少,代码复用性、可扩展性也高。本属于购物车的功能就定义在购物车实体上也更直观。

很多属性应该是私有的

先说一点,我们定义一个方法、一个属性、一个类、一个软件时,一定要考虑这些功能可能在任何时候、任何地方、被任何一个SB(包括我自己)调用,他们很可能不按你的预期来。

购物车必须是属于某个顾客的,也就是必须有个关联的CustomerId,这是我们的业务规则,也是约束,但按我们上面的定义为get; set; 别人可能给他赋值个0或负数,这就让购物车实体处于不正确的状态,所以应该把CustomerId设置为{ get; private set; },同理在定义购物车明细时关联的ProductId(商品Id)也应该是只读的,因为购物车明细必须与某个商品关联才是正常的。

我们可以在构造函数中定义参数来初始化这些只读属性。

如此这般,当创建一个购物车实体后,这个对象无论被谁访问,CRUD工程师们无法像以前一样破坏它的状态。

至于到底哪些属性该是只读的,哪些是public的应该根据场景,每个属性认真思考再决定。

如果非要在某个阶段形成一个不符合业务要求的实体,可以考虑使用Builder模式

有时候你发现仅仅是通过构造函数才能初始化一个对象,感觉很不方便,因为对象可能需要先new出来,然后在各个步骤对它进行赋值,最后才能形成一个我们满意的对象(有严格约束,且符合业务规则),个人觉得这个时候应该为它创建一个对应的Builder对象,把那些临时的状态属性设置到Builder上,最后Builder.Build();生成一个符合业务规则的对象。这种情况不仅仅适用用域领域实体,整个软件设计中都适用。

在目前的购物车功能好像体现不了这个。

EF查询时可以访问到私有构造函数、设置只读属性

这个很重要,之前一直晓得领域实体属性有些应该是只读的,但考虑用ef无法给只读属性赋值,所以后来放弃了,也不晓得从啥时候开始,我们定义的领域实体的私有构造函数和属性EF是可以直接访问的,这就给我们定义符合业务规则的实体创造了机会。

AutoMapper可以通过构造函数做映射

上面的领域实体如果关键属性为只读的了,咱们做dto到实体的映射呢?印象里AutoMapper是可以通过构造函数做映射的,刚好我们上面说了我们的实体是有对应的构造函数的。这个规则有待证实。

领域实体应该有业务方法

想象下,将商品加入购物车这个功能,按我原来的做法会在应用层查询出购物车,比如这个对象叫shoppingCart,那么我会直接

在应用层中:
shoppingCart.Items.Add(item);
//计算明细对应的金额(明细数量*关联商品的单价)
//其它处理

仔细考虑下,将商品加入购物车这个方法不是应该定义在购物车实体上吗?如果这样,商品进入购物车,后续要从新计算金额、积分之类的逻辑也都会写在购物车实体内部,而不是放在应用层。这样,应用层将来只需要shoppingCart.AddItem(item);是不是更符合业务场景?

领域实体的方法只修改自己的状态属性

以订单支付这个方法为例,订单支付 要修改支付状态为已支付、改变支付金额、将物流状态改为待发货等等,支付状态、物流状态、支付金额 这些属性都是订单实体类的,购物车实体中的方法也只是修改自己实体的状态属性。

别想在领域实体里去做依赖注入、访问数据库或其它服务

领域实体里只是根据业务定义相关方法,这些操作都是跟这个领域实体相关的,状态属性的改变。依赖注入、访问数据库或其它服务可以在应用层或领域服务去做。

通过领域事件实现可扩展性

我们可以在购物车中定义这样的事件:当商品明细加入购物车后触发、当移除购物车明细时触发、当购物车明细数量变更时触发.....等等。这样我们的购物车模块可以做得很干净,将来别人使用这个模块时可以订阅这些事件来扩展购物车模块。

这个事件的功能是abp自带的事件总线,可以去参考官方文档。

并且这个事件还是事务性的,意思说如果将来别人扩展我们的模块,在它们的事件处理代码中若操作数据库,和我们处理购物车逻辑是在一个数据库事务中,他们可以抛出异常来阻止我们的正常提交。

领域服务

DDD的说法是当一个功能无法只归结到一个领域实体上时可以考虑领域服务,协调多个实体或其它领域服务时也行。

目前在购物车模块中没有使用领域服务,还是以订单支付为例

上面说了,订单实体本身定义了个“支付”的方法,它内部改变订单自己的状态(修改订单状态、修改支付状态、修改物流状态),然而订单支付还涉及到其它处理,比如:要先判断顾客会员等级、余额情况、是不是黑名单 等等,这里就涉及到多个实体和服务了,所以在订单领域服务中有个支付方法,它会做各种业务判断处理后再调用订单实体.支付();

领域服务中也可以触发领域事件

领域服务也属于领域层,也可以触发相关事件,以这种方式来预留扩展点。abp也提供了这个功能。

何时使用领域服务?合适使用领域事件?

我比较倾向用事件,上面说支付订单前要做各种业务判断,比如会员等级决定折扣、余额检查等,用领域服务很直观,但是不够灵活,比如将来又变了,要在支付前做更多判断呢?此时如果在支付前触发一个事件,那么将来有新的需求就可以加新的事件处理器,不符合业务规则的情况,在事件处理逻辑中抛异常就可以了。

领域服务中是否访问当前用户(session)?

不建议,当前登陆用户严格来说是应用程序状态,而领域服务是细小的领域逻辑,它与应用程序状态无关。

应用服务

领域层整好了,这个代码会变得很少,

它访问数据库得到领域实体,也可以依赖注入领域服务。按业务流程逐个调用领域实体和领域服务的相关方法,通常感觉对应用户的一个操作,比如点个按钮提交

它访问当前用户

它做权限判断等。

它做基本数据校验

它做dto到实体的映射

开始事务、调用领域服务、实体后提交事务

将商品加入购物车的流程

顾客点击“加入购物车”,前端上传商品(或skuId)

应用层做权限判断、基本数据验证、然后查询当前用户关联的购物车

调用购物车.AddItem(item);

购物车领域实体检测这个商品是否已存在购物车了,若在则累加数量,并触发购物车明细数量改变的事件;若不存在则添加商品到购物车并触发 购物车明细增加成功的事件

事件处理程序预留给模块使用方进行扩展的

如果业务流程复杂,在应用层可能还有好几个步骤要做,但如何完成通常是交给领域服务和实体

应用层最后保存数据到数据库(事务)

用领域驱动DDD的方式实现购物车-基于abp一代6.2的更多相关文章

  1. 【系统架构】软件核心复杂性应对之道-领域驱动DDD(Domain-Driven Design)

    前言 领域驱动设计是一个开放的设计方法体系,目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题,本文将介绍领域驱动的相关概念. 一.软件复杂度的根源 1.业务复杂度(软件的 ...

  2. 领域驱动(DDD)设计和开发实战

    领域驱动设计(DDD)的中心内容是如何将业务领域概念映射到软件工件中.大部分关于此主题的著作和文章都以 Eric Evans 的书<领域驱动设计>为基础,主要从概念和设计的角度探讨领域建模 ...

  3. 基于ABP落地领域驱动设计-04.领域服务和应用服务的最佳实践和原则

    目录 系列文章 领域服务 应用服务 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践 ...

  4. 基于ABP落地领域驱动设计-05.实体创建和更新最佳实践

    目录 系列文章 数据传输对象 输入DTO最佳实践 不要在输入DTO中定义不使用的属性 不要重用输入DTO 输入DTO中验证逻辑 输出DTO最佳实践 对象映射 学习帮助 系列文章 基于ABP落地领域驱动 ...

  5. 基于ABP落地领域驱动设计-06.正确区分领域逻辑和应用逻辑

    目录 系列文章 领域逻辑和应用逻辑 多应用层 示例:正确区分应用逻辑和领域逻辑 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落 ...

  6. 基于ABP落地领域驱动设计-00.目录和小结

    <实现领域驱动设计> -- 基于 ABP Framework 实现领域驱动设计实用指南 翻译缘由 自 ABP vNext 1.0 开始学习和使用该框架,被其优雅的设计和实现吸引,适逢 AB ...

  7. 基于领域驱动设计(DDD)超轻量级快速开发架构(二)动态linq查询的实现方式

    -之动态查询,查询逻辑封装复用 基于领域驱动设计(DDD)超轻量级快速开发架构详细介绍请看 https://www.cnblogs.com/neozhu/p/13174234.html 需求 配合Ea ...

  8. 浅谈我对DDD领域驱动设计的理解

    从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品 ...

  9. 初探领域驱动设计(2)Repository在DDD中的应用

    概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的, ...

  10. DDD 领域驱动设计-三个问题思考实体和值对象

    消息场景:用户 A 发送一个消息给用户 B,用户 B 回复一个消息给用户 A... 现有设计:消息设计为实体并为聚合根,发件人.收件人设计为值对象. 三个问题: 实体最重要的特性是什么? Messag ...

随机推荐

  1. 温习 SPI 机制 (Java SPI 、Spring SPI、Dubbo SPI)

    SPI 全称为 Service Provider Interface,是一种服务发现机制. SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类.这样可以在运行时 ...

  2. AI年代,谁还用难用的Keil?快快换CLion!!! 破解+环境替换 [上]

    安装Clion 由于这个地方碰壁比较多,所以大家严格按照我下面的安装节奏就可以了! 记得CLion的版本等,和我所说的不匹配也会导致破解失败. 包能破解安装上的. 1.下载 链接:https://pa ...

  3. linux下时间时区详解

    首先我们要明白,"时间"和"时区"是两个东西. 时间是指从某个时间点开始到另一个时间点经过的"长度",是"纵向"距离,一 ...

  4. UE5笔记:虚幻引擎反射系统和对象

    虚幻引擎反射系统 使用宏提供引擎和编辑器各种功能,封装你的类.使用虚幻时,可以使用标准的C++类,函数和变量 虚幻中对象的基类是UObject,UCALSS宏的作用是标记UObject的子类,以便UO ...

  5. 2-2 C++变量

    目录 2.2.1 变量定义:列表初始化(list initialization) 2.2.2 变量的定义与声明 C++分离式编译 定义与声明 2.2.3 C++变量命名 2.2.4 变量名的作用域(s ...

  6. 干货分享 | PCB测试点的用途

    ​ PCB测试点长什么样子?请看下图: ​ 如果你曾经用过NOKIA手机,每次你打开后盖换电池的时候,每次看到的那两排圆形的点--就是PCB测试点,or you can call it Test Po ...

  7. 工具篇-FinalShell

    转载:https://www.toutiao.com/i6694563184428188171?wid=1625538368131 FinalShell是一款免费的国产的集SSH工具.服务器管理.远程 ...

  8. 【解决方案】Error running,Command line is too long

    一.现象 IDEA 提示 Error running,Command line is too long 二.原因 Java 命令行启动举例如下图,当命令行字符过多的时候,就会出现 Error runn ...

  9. 使用 Visual Studio 调试器附加到正在运行的进程

    使用 Visual Studio 调试器附加到正在运行的进程 使用场景 当项目在测试环境上有bug,需要运行代码调试一下,这时就需要在测试环境上安装一个调试工具,然后在本地运行代码,远程链接到测试环境 ...

  10. 自有Jar包生成Docker镜像

    前言 经常会有些自己写的一些SpringBoot小项目,为了实现一些小的功能/需求,但是部署的时候,不管是生成jar包,还是war包部署到tomcat中,都容易因为需要部署的环境(比如java版本.t ...