在这篇教程中,我将会实现领域模型
- EduSync.Speech.Domain
这是项目的最内层,包含核心领域对象和业务规则,并定义了外部接口。
数据库,网络连接,文件系统,UI,特殊框架等等都不应该存在于该层。
核心领域对于自身之外的一切一无所知。
依赖以及它们的实现都是通过接口注入到核心领域模型中的。
在上一篇文章的最后,我们实现了一个“贫血”的领域模型,现在让我们来丰富一下。
充实的领域模型
贫血的领域模型是领域驱动设计的反模式,在这一小节,我会使用值对象将领域模型与数据契约解耦。
贫血的模型将数据与操作分离开了。换句话说,就是一个类仅有属性,而操作这些属性的方法位于另一个类中。
结果,其他的类不仅要读数据,还要修改数据,这样,领域模型就必须有公共的setter方法。这违反了封装原则。
让我们从验证Title开始。
我的第一个测试是:Title必须超过十个字符,不超过60个字符。
测试会失败,我们需要实现这个验证:
值对象
实体与值对象的区别在于如何判断相等。
实体相等是当引用相等或Id相等。
值对象相等是引用相等或结构相等。
- 引用相等:当引用了内存中的同一个对象,则两个对象相等。
- Id相等: 两个对象有相同的唯一标识符,则两个对象相等。
- 结构相等:当两个对象的成员全都相等,则两个对象相等。
实体有一个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的值对象
Url的验证逻辑包含在UrlValue值对象中
Type的值对象
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