原文链接:https://logcorner.com/building-microservices-through-event-driven-architecture-part2-domain-objects-and-business-rules/
在本文中,我将实现领域模型:
EduSync.Speech.Domain
这是包含核心域的最内层。它包含我们的领域对象和业务规则。并定义我们的外部接口。
不允许使用数据库、网络连接、文件系统、UI或特殊框架。
核心领域对自身以外的任何事物一无所知。
这些依赖项及其实现使用接口注入到我们的核心域中。
在上一步结束时,我们最终得到了一个贫血领域模型。所以让我们从丰富它开始。
充血领域模型
贫血领域模型是DDD世界中的一种反模式,因此在本节中,我将使用值对象将领域模型与数据契约分离。
贫血领域模型是一种领域模型,其中数据和对该数据的操作彼此分离。换句话说,只有属性的类和处理这些属性的方法位于另一个类中。
因此,这些其他类既可以读取数据,也可以修改数据。所以领域类必须有public setter。这是缺乏封装反模式。
让我们从验证Title开始。
我的第一个测试是:Title长度必须大于10个字符且小于60个字符:
测试将失败,因此让我们实现Title验证:
Title值对象
实体和值对象的主要区别在于如何识别它们。
实体由引用相等和标识相等标识。
值对象由引用相等和结构相等来标识。
引用相等:如果两个对象引用内存中的同一个对象,则它们相等
标识相等:如果两个对象具有相同的标识,则它们相等
结构相等:如果两个对象的所有成员都相等,则两个对象相等
实体具有Id字段并且是可变的,而值对象没有Id字段并且是不可变的。
值对象没有实体就没有意义,它必须属于一个实体。
考虑以下情况:
2辆相同型号、相同颜色、相同年龄等的车辆……总是2辆不同的车辆,因为每辆车都有自己的标识:车辆是一个实体。
2个所有字段都相等的地址(相同的街道号码、相同的城市、相同的国家,等等)是完全相同的地址:地址是一个值对象。
Title的第一个实现如下所示:
请记住,值对象由引用相等和结构相等来标识.
所以右键单击Title类并选择生成 Equals和GetHashCode。
Title只有一个值,因此选择它并单击确定
Title现在是一个值对象,它的最终实现看起来像这样
这是Title值对象的单元测试。如果它们具有相同的值,我应该验证2个标题是否相等,如果不是,则不同
URL值对象
验证Url的所有逻辑都在名称为UrlValue的值对象中实现
Type值对象
验证SpeechType的所有逻辑都在名称为SpeechType的值对象中实现
Speech领域对象如下所示:
实体和聚合
请记住,实体由引用相等和标识相等标识并具有Id字段。因此,让我们创建一个基本实体类:Entity,并在Id字段上生成Equals和GetHashCode。如果2个实体E1和E2具有相同的id,则 E1==E2应该返回true
DDD聚合是可以作为单个单元处理的领域对象的集群。例如订单及其订单项,它们将是单独的对象,但将订单(及其订单项)视为单个聚合非常有用。
聚合应该始终处于有效状态,并且每个聚合都有一个根是一个实体,不属于该聚合的类只能引用聚合根。
因此,让我们创建一个继承自Entity的基类AggregateRoot,我将其设为泛型,因为T是Id字段的类型,并且它可以根据这些实体而变化
领域事件
领域事件通过避免直接调用来实现有界上下文之间的通信。所以一个有界上下文B1引发一个事件,一个或多个有界上下文B2…Bn对此事件的子订阅方应该处理该事件以使用它。
因此,让我们创建一个基类DomainEvent
但是在这里,由于我实施事件溯源的策略,我的有界上下文产生的所有事件都将保存在我的事件存储中。对这些事件感兴趣的其他有界上下文、服务或其他程序将必须订阅服务总线。
比如我每次创建一个新的Speech,然后我都会创建一个SpeechCreatedEvent事件
SpeechCreatedEvent类必须从DomainEvent基类继承
聚合根的最终实现将如下所示:
因为Speech实体是聚合根,所以让我们继续从AggregateRoot继承它,Speech实体的Id字段是一个 Guid
让我们添加一些测试来覆盖 domainEvents
LogCorner.EduSync.Speech.Application和LogCorner.EduSync.Speech.Domain是100%的代码覆盖率
欢迎关注我的个人公众号”My IO“