原文链接
在这篇教程中,我将会实现领域模型
这是项目的最内层,包含核心领域对象和业务规则,并定义了外部接口。
数据库,网络连接,文件系统,UI,特殊框架等等都不应该存在于该层。
核心领域对于自身之外的一切一无所知。
依赖以及它们的实现都是通过接口注入到核心领域模型中的。
在上一篇文章的最后,我们实现了一个“贫血”的领域模型,现在让我们来丰富一下。
贫血的领域模型是领域驱动设计的反模式,在这一小节,我会使用值对象将领域模型与数据契约解耦。
贫血的模型将数据与操作分离开了。换句话说,就是一个类仅有属性,而操作这些属性的方法位于另一个类中。
结果,其他的类不仅要读数据,还要修改数据,这样,领域模型就必须有公共的setter方法。这违反了封装原则。
让我们从验证Title开始。
我的第一个测试是:Title必须超过十个字符,不超过60个字符。
测试会失败,我们需要实现这个验证:
实体与值对象的区别在于如何判断相等。
实体相等是当引用相等或Id相等。
值对象相等是引用相等或结构相等。
实体有一个Id字段并是易变的,值对象没有Id字段且不易变。
离开了实体,值对象将毫无意义,值对象必须从属于实体。
考虑下面的情形:
我的Title的第一个实现像这样:
让我们修复一下测试。记住一个值对象的判等条件是引用相等或结构相等。在Title类上右键,选择生成Equals与 GetHashCode方法,Title只有一个Value字段,选择它并点击OK。
现在Title就是一个值对象,它的最终实现看起来像下面这样:
public class Title : IEquatable<Title> { private const int MinLenght = 10; private const int MaxLenght = 60; public string Value { get; } public Title(string value) { if (value?.Length < MinLenght) throw new InvalidLenghtAggregateException("Value is too short"); if (value?.Length > MaxLenght) throw new InvalidLenghtAggregateException("Value is too long"); Value = value; } public override bool Equals(object obj) { return Equals(obj as Title); } public bool Equals(Title other) { return other != null && Value == other.Value; } public override int GetHashCode() { return HashCode.Combine(Value); } public static bool operator ==(Title title1, Title title2) { return EqualityComparer<Title>.Default.Equals(title1, title2); } public static bool operator !=(Title title1, Title title2) { return !(title1 == title2); } } 复制代码
下面是Title值对象的单元测试,我要看看两个拥有相同的Value的Title是否相等
Url的验证逻辑包含在UrlValue值对象中
SpeechType的验证逻辑包含在SpeechType值对象中
最后,Speech领域对象看起来像下面这样:
记住实体判等条件是引用相等或Id相等。让我们创建一个实体的基类: Entity,并基于Id生成Equals 与 GetHashCode方法。如果E1,E2有相同的Id,E1==E2就返回true。
聚合应该始终处于可用状态,每个聚合都有一个实体根,不属于同一个聚合的类只能引用聚合根。
建一个AggregateRoot继承自Entity,泛型T代表Id字段的类型,不同的实体可能会不同
领域事件能够允许有界上下文之间通信,而避免了直接的调用。一个有界上下文B1引发一个事件,有界上下文B2订阅该事件并处理。
建一个DomainEvent基类:
这里,由于实现了事件源策略,所有有界上下文引发的事件都会保存在我的事件仓库里。
其它对事件感兴趣的有界上下文,服务或应用都必须到消息总线注册。
例如,每次我新建一个Speech,都会建一个SpeechCreatedEvent事件
SpeechCreatedEvent类继承自DomainEvent基类
聚合根的最终实现如下:
由于Speech实体是一个聚合根,它需要继承AggregateRoot,Speech实体的Id字段是一个Guid
让我们添加一些测试,测试一下领域事件
LogCorner.EduSync.Speech.Application 与 LogCorner.EduSync.Speech.Domain 都是100%代码覆盖率。
下一步我会实现表现层:LogCorner.EduSync.Speech.Application
源代码