【译】构建事件驱动的微服务(二)——领域对象与业务规则

524 阅读4分钟

原文链接

在这篇教程中,我将会实现领域模型

  • 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

源代码