作者回复: 我先理解一下你说的业务场景哈。不知道理解的对不对?不对的地方请你指出。由于不好展示事件风暴的过程,我就口述吧。
你描述的业务场景主要包括这两个部分吧。
流程一:创建任务
1、根据任务规则获取任务基础数据,生成任务。(产生任务已创建事件,领域对象包括:任务生成的基础数据、数据过滤规则、任务、任务类型)
2、自动将任务分配给销售。(销售的数据应该来源于其他系统,这个过程实际上是一个给任务赋值的过程,领域对象包括:销售)
3、销售领取任务。(给任务分配销售)
这个过程领域对象包括:基础数据、数据过滤规则、任务、任务类型、销售。
命令有:创建任务,给任务分配销售。
领域事件有:任务已创建。
流程二、任务执行
1、销售查询并获取任务,执行任务。(不清楚你说的字段在这个过程是什么含义,是查询时勾选类型吗)
2、任务执行完成后,记录执行结果,产生任务执行日志。(产生任务执行日志已创建事件,领域对象有:任务、任务日志)
3、统计日志。这一块不清楚你的业务逻辑和流程。
因为有统计日志,是不是就会去查询任务的执行日志,如果是这样的话,任务日志就需要设计为实体,跟任务关联,这里任务是聚合根。
这个阶段的领域对象有:任务和任务执行日志。
命令有:查询任务,生成任务执行日志。
领域事件有:执行任务日志已创建。
结合这两个流程,我们整体来分析一下。
领域对象包括:基础数据、数据过滤规则、任务、销售、任务类型、任务执行日志。
由于销售人员数据来源于第三方,以值的形式存储在任务中,因此我们可以将它设计为任务的值对象。
任务执行日志依附于任务,但是由于它后续要做查询和统计分析,因此将它设计为被任务引用的实体。
任务类型是任务的值对象
其它的基础数据、数据过滤规则这两个领域对象很独立,你可以理解他们是独立的实体,或者说一个实体就是一个聚合。
但是这样设计在代码目录设计时会显得比较单薄,一个聚合会有一个仓储和聚合自己的代码目录结构。因此我们可以将这两个实体可以直接放在任务的聚合里,但是他们的生命周期不受任务这个聚合根管理。
这样的话,我们就可以只建立一个任务聚合。这个聚合的聚合根是任务。它引用的实体包括任务执行日志,任务的值对象有:销售、任务类型。还有两个独立实体:基础数据和数据过滤规则。
说明一下:
在不少的数据统计和计算场景中,有很多实体之间相互独立,只参与计算和统计分析,但是这类场景中业务内聚性又很高,你找不出管理这些实体的聚合根。我称这种业务模型是非典型领域模型。虽然有些方面(比如聚合根)不符合DDD的一些原则,但是我们也可以按照DDD方法来完成设计。
作者回复: 电商里面比较典型的几个聚合根,比如:库存、商品、订单等等。
以订单为例,订单在聚合里是聚合根,与订单关联的有订单明细和收货地址。订单明细包括商品ID,商品名称,价格以及数量等信息,由于订单明细是多个,它是一个集合,它被设计为实体,被订单引用。而订单只有一个收货地址,这个收货地址的值来源于你个人中心维护的收货地址,收货地址只能被整体替换,所以它被设计为值对象。
作者回复: 被保人这个值对象以属性嵌入的方式嵌入保单聚合根中,查询客户保单时你不需要到客户聚合去查询客户信息了,直接根据客户信息在投保聚合查保单就可以了,当然这个客户信息不只是ID。
说明一下:这个跨聚合引用是在生成保单的时候,通过客户聚合根查询获取的客户信息,从客户聚合获取客户信息后,客户的信息就作为值对象的值嵌入到了保单实体中。
作者回复: 后面会有建模方法的介绍。
你可以试着从用户旅程入手,根据流程看看会发生哪些领域事件,再找找是哪些命令触发这些领域事件的。梳理的差不多的时候,你就找找这些命令都是哪些实体的行为,这样就可以找出好多实体。根据实体可以找出聚合根,再根据聚合根找出关联的实体和值对象。这样聚合就找到了,然后对聚合划分限界上下文。这样就可以设计微服务了。
作者回复: 非常感谢!
首先你说的外界,要明确一下,这个外界是微服务内,不同的层之间?还是指微服务之间,也就是微服务外部调用。
如果是在微服务内,不同层之间的话,由于运行在同一个微服务内,实体是可以被其它高层服务获取的,这时实体以DO对象的形式,存在于应用层和领域层,你可以使用DO对象的方法。但是不建议将实体的方法编排放在应用层,而是在领域层封装成领域服务后暴露给应用层。
如果这个外界是微服务之间的话,你需要将实体DO数据转换为DTO对象后,才能被外界使用,这时,外界是不能调用实体的方法的,因此可以隐藏实体核心业务逻辑的实现方式。
作者回复: 基础篇主要讲解DDD的基础概念和设计理念,所以相对抽象一些。后面会有中台业务建模和微服务设计案例,比较好理解。敬请期待。
作者回复: 类比的不错。
作者回复: 说到充血模型,就离不开贫血模型。
先说一下贫血模型吧,贫血模型是指使用的领域对象中只有setter和getter方法,所有的业务逻辑都不包含在领域对象中而是放在业务逻辑层。
而充血模型将大多数业务逻辑放在领域实体中实现,实体本身包含了属性和它的业务行为,它在领域模型中就是一个具有业务行为和逻辑的基本业务单元。
作者回复: 如果数据一致性没什么问题,那就选择自己最合适的方式去做吧。但是一定要记住,边界要清晰,聚合之间的服务和数据不要耦合,耦合度太高,以后领域模型和微服务演进就很难。
作者回复: 它是通过仓储实现的。
由于领域模型中的DO实体与数据模型中的PO不一定一一对应,所以聚合里的实体在持久化时,会有一个DO到PO的转换过程。仓储持久化的组织者是聚合根。具体的实现你可以用MyBatis等组件,目前来讲现在的几个主流持久化组件,在仓储实现持久化方面使用起来还不是特别的顺手。
在某些不强调数据一致性的聚合中,个人感觉你可以突破聚合根,按照你习惯的方式去完成数据持久化。
作者回复: 两个出发点不一样,聚合根主要是从实体关联的角度,关注数据一致性。领域服务主要组合的是实体业务行为。
作者回复: 因为微服务的架构演进,会有功能和代码的拆分和重构的过程。而一般来讲聚合的内聚性很高,聚合内的功能相对稳定,我们可以聚合功能和代码为单位在不同的微服务之间进行功能和代码的重构。
如果聚合之间代码和业务边界不清晰,聚合之间数据和服务可以随便访问,就会因为耦合度过高,代码很难剥离,最后微服务架构演进时又要在走一遍从单体拆分微服务的过程。后面的章节我会专门讲微服务架构的演进。
作者回复: 是的,在一个聚合内是由聚合根来管理和协调所有实体和值对象的生命周期的,所以在一个聚合内可以保证数据的一致性。
由于多个聚合运行在同一个微服务内,在应用层我们是可以拿到其它聚合的聚合根的DO对象的,拿到聚合根后你就可以通过聚合根引用聚合的内部实体和值对象。
作者回复: 你好,事件风暴会有专门的一节介绍。
作者回复: 聚合应该比模块还要小一些,它是一个比较小的功能集合,聚合内的对象可以独立完成聚合的逻辑,它也是可以独立部署为微服务的最小单位。领域模型里面可以包括多个聚合,一个聚合会有一个聚合根。所以一个领域模型至少有一个聚合根。
作者回复: 在设计的时候要实体采用充血模型,让实体要有业务行为,也就是实体通过自身的实体类方法来实现实体的业务逻辑,采用面向对象的设计方法。多个实体组合的业务逻辑用领域服务来实现。
作者回复: 账户在账户聚合内是聚合根。