转载

【译】构建事件驱动的微服务(一)——应用特定的业务规则

原文链接

如今,洋葱架构和六边形架构为我们提供了诸多好处,例如可测试性,代码的可维护性以及相对于外部框架的独立性等等。在这这教程中,我会教你使用整洁架构(clean architecture)的方法与工具,例如领域驱动设计(DDD),测试驱动开发,CQRS,事件源,容器化,Oauth2,Oidc等,并以此来构建一个微服务。

关于整洁架构的更多内容,推荐阅读 Robert C. Martin的这篇文章 。

【译】构建事件驱动的微服务(一)——应用特定的业务规则

依赖规则

上图中的同心圆代表软件的不同领域。一般来说,越向外,软件的层级就越高。外圈是机制,内圈是策略。

使得整个架构工作的最重要的规则是依赖规则,该规则指出,源码的依赖只能指向内部。内圈对于外圈一无所知。特别是,在外圈中声明的任何名称都不能由内圈中提及,包括,方法,类,参数或其他实体名称。

出于同样的原因,外圈中使用的数据格式不应由内圈使用,特别是在外圈中使用的格式是由某个框架生成的。我们不希望外圈中的任何内容影响内圈。

命令查询职责分离(CQRS)

CQRS将命令与查询分离。

命令是改变应用程序状态的操作,它没有返回数据。查询返回数据但并不修改应用状态。 因此,CQRS是一个非常有用的原理。在微服务世界中,我们使用两个数据库创建了我们的应用程序:

  1. 一个关系型数据库,针对命令端的写。
  2. 一个NoSQLshujuk ,针对查询,能够尽快读取数据。

由于大部分的应用读数据都比写数据更加频繁,在我们容器化的时候,打个比方,我们可以为我们的命令端规划2 pods,查询端规划10 pods。

领域驱动设计

CQRS适用于领域驱动设计。领域驱动设计致力于建立丰富的领域模型来处理复杂的业务逻辑。

改变数据会造成更多的漏洞。因此,弄清楚应用的哪部分修改数据,哪部分不修改数据有助于维护与调试。

事件源

事件源将所有的改变存到一个对象中,构成了 事件仓库 中的一系列事件。

我会使用这个原理来:

  • 领域服务实现领域相关的概念(entity,值对象(VO),聚合,领域事件),在关系型数据库中记录命令,在事件仓库中记录事件。
  • 生产者从事件仓库中取出事件,并将事件发送到服务总线(事件仓库只是一个附加表,并使用通用语言)
  • 消费者在服务总线中订阅,从服务总线中取出事件并将它作为预计算的数据存到nosql数据库
  • 读服务查询NOSQL数据库
  • 发生的所有事都被保存在事件仓库中
【译】构建事件驱动的微服务(一)——应用特定的业务规则

我将要建立的系统,是帮助演讲者与参加者订阅相关事件的,会议,演讲,交流会等。

我的项目结构如下:

  • EduSync.Speech.Domain

架构的最内层,包含领域对象与业务规则,并定义了对外的接口。

数据库,网络连接,文件系统,UI以及特殊的框架,都是不允许出现的。

核心域对自己以外的事一无所知。

依赖使用接口注入到我们的核心域中。

  • EduSync.Speech.Application

该层指向核心域并包含应用特定的业务规则。

编排数据流并使用领域模型。

不存在对于数据库,UI以及特殊框架的依赖。

  • EduSync.Speech.Presentation

该层是表现层,包含Web,UI等。在我们API的上下文中,这意味着它接收http请求(POST/PUT/PATCH/DELETE),并返回JSON格式的内容。

  • EduSync.Speech.Infrastructure

该层是数据库与网关相关,在这里,我们定义数据访问层,仓库等等。

它包含了我们在领域层定义的接口的实际实现。

【译】构建事件驱动的微服务(一)——应用特定的业务规则

测试驱动开发

实现“演讲登记”使用场景。

【译】构建事件驱动的微服务(一)——应用特定的业务规则

要让测试可用,首先要实现的是RegisterSpeechUseCase。

  • 首先定义IRegisterSpeechUseCase接口以及它的实现RegisterSpeechUseCase,这些类属于EduSync.Speech.Application。
public interface IRegisterSpeechUseCase : ICommandHandler<RegisterSpeechCommandMessage>
{
}
复制代码
public interface ICommandHandler<in TCommand> where TCommand : ICommand
{
    Task Handle(TCommand message);
}
复制代码
public interface ICommand { }
复制代码

IRegisterSpeechUseCase接口处理登记演讲的命令,该命令是一个RegisterSpeechCommandMessage对象。

【译】构建事件驱动的微服务(一)——应用特定的业务规则

RegisterSpeechUseCase实现了IRegisterSpeechUseCase接口:

【译】构建事件驱动的微服务(一)——应用特定的业务规则
  • 接下来定义其他依赖,例如IUnitOfWork,以及ISpeechRepository,这些接口属于EduSync.Speech.Domain,它们的实现则位于EduSync.Speech.Infrastructure。

ISpeechRepository需要一个Speech实体。

【译】构建事件驱动的微服务(一)——应用特定的业务规则

到这一步,项目可以顺利编译,但是测试会失败

【译】构建事件驱动的微服务(一)——应用特定的业务规则

测试失败的原因是我要验证CreateAsync与commit被调用了,那么我们就在RegisterSpeechUseCase类中调用SpeechRepository.CreateAsync以及IUnitOfWork.Commit这两个方法。

【译】构建事件驱动的微服务(一)——应用特定的业务规则

然后在单元测试的安排部分,创建SpeechRepository.CreateAsync与IUnitOfWork.Commit的mock。

【译】构建事件驱动的微服务(一)——应用特定的业务规则

到这一步,所有的测试都通过了,但是我的代码覆盖率还不够:例如,如果我注释掉这个块,我的测试仍然能够成功,但是如果command 为null,我的应用会在运行时崩溃

【译】构建事件驱动的微服务(一)——应用特定的业务规则
【译】构建事件驱动的微服务(一)——应用特定的业务规则

让我们添加一个新的测试来修复这个问题

【译】构建事件驱动的微服务(一)——应用特定的业务规则

现在我的LogCorner.EduSync.Speech.Application代码覆盖率就是100%了

【译】构建事件驱动的微服务(一)——应用特定的业务规则

但是,如果我交换赋值的话会怎么样呢?

var title = command.Type;
var urlValue = command.Title;
var description = command.Url;
var type = command.Description;
复制代码

所有的测试仍然可以成功,但是应用会保存错误的状态,名称与url颠倒了。

可以在测试的断言阶段使用moqSpeechRepository.Verify修复这个问题,但是我现在将它留在这里,等到我实现我的领域模型时,我会介绍一下值对象。

【译】构建事件驱动的微服务(一)——应用特定的业务规则

下一步我将介绍实现领域模型。

源代码

总结

上面的教程中,实现了一个登记演讲的业务,该层属于应用层,在这个逻辑中,IRegisterSpeechUseCase接口用处理一个命令,并修改应用的状态(插入数据库)。

这个修改状态的过程,让我想到了Redux,在Redux中,如果想要修改应用的状态,只能dispatch一个action,而在这里则是发出一个command。

教程中从一个单元测试开始,逐步实现单元测试中需要的类,是测试驱动开发的方法。

原文  https://juejin.im/post/5df2e387e51d45583e4dbcc0
正文到此结束
Loading...