原文链接:https://logcorner.com/building-microservices-through-event-driven-architecture-part1-application-specific-business-rules/
如今,洋葱或六边形等架构为代码的可测试性和维护、与外部框架的独立性提供了重要帮助。
在本教程中,我将展示如何使用Clean架构,以及诸如领域驱动设计(DDD)、测试(行为)驱动开发(TDD)、CQRS、事件溯源、容器化、Oauth2和Oidc等方法和工具来构建微服务架构。
关于Clean架构的更多信息,我建议您阅读Robert C. Martin (Uncle Bob)的这篇文章:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
依赖规则
同心圆代表软件的不同区域。一般来说,越深入代表你的软件层次越高。外圆是战术实现机制,内圆是战略核心策略。
使这个架构工作的最重要的规则是依赖规则。这个规则说明了源代码依赖只能指向内部。内圈的任何东西都不可能知道外圈的任何东西。特别是,在外圈中声明的部分不能被内圈中的代码提及。包括,函数,类。变量或任何其他命名的软件实体。
同样,外圈中使用的数据格式不应该被内圈使用,特别是如果这些格式是由外圈中的框架生成的。我们不希望外圈的任何东西影响内圈。
命令查询责任分离 (CQRS)
CQRS将命令与查询分开。命令是更改应用程序状态并且不返回数据的操作。查询是返回数据但不更改应用程序状态的操作。因此,在微服务领域,通过使用两个数据库创建应用程序,CQRS将是一个非常有用的概念:
1.一个关系型数据库,它针对在命令端写入进行了优化。
2.查询端使用NoSQL数据库,以便尽可能快地读取数据。
由于大多数应用程序读取数据的频率远高于写入数据的频率,因此在我们的容器化方法中,我们可以在2个pod上部署命令端,在10个pod上部署查询端。
领域驱动设计
CQRS适合领域驱动设计。DDD专注于构建丰富的领域模型来处理复杂的业务逻辑。
更改数据会导致更多错误。因此,清楚地了解应用程序的哪个部分更改了数据以及应用程序的哪个部分不更改数据将有助于可维护性和调试。
事件溯源
事件溯源将对象的所有更改存储为事件存储中的一系列事件。https://eventstore.org/
我将使用这个概念来构建以下内容:
领域服务实现领域相关的概念(实体、值对象、聚合、领域事件),在关系数据库中记录一个命令,在事件存储中记录一个事件。全部作为一个单元,以进行数据更改
生产者从事件存储中获取事件并将其发送到服务总线(事件存储是一个只附加表)
消费者,服务总线的订阅者,从服务总线获取事件并将其作为预先计算的数据写入NoSQL数据库
ReadModel服务查询NOSQL数据库
发生的一切都保存在事件存储中
我将建立一个系统,帮助演讲者和与会者注册和跟踪事件(会议、谈话、聚会等…)
我的项目结构如下:
EduSync.Speech.Domain
容纳核心领域的最内层。它包含我们的领域对象和业务规则。定义了我们的外部接口。
数据库、网络连接、文件系统、用户界面或特殊框架,都是不允许的。
核心领域不知道自己之外的任何东西。
这些依赖项及其实现是使用接口注入我们的核心域的。
EduSync.Speech.Application
指向核心领域并包含特定于应用程序的业务规则。
编排数据流并使用领域模型。
不依赖于数据库、UI 或特殊框架。
EduSync.Speech.Presentation
该层包含Web、UI和展示逻辑。在我们的API上下文中,这意味着它通过网络接受http请求形式的输入(POST/PUT/PATCH/DELETE),并以JSON格式的内容返回其输出。
EduSync.Speech.Infrastructure
该层包含数据库和网关。在这里,我们定义数据访问层、存储库等。
它包含在我们的Domain中定义的接口的物理实现。
测试驱动开发
实现“语音注册”用例
为了使我的测试变为绿色,我首先需要实现的是RegisterSpeechUseCase
使这个架构工作的最重要的规则是依赖规则。这个规则说源代码依赖只能指向内部。内圈中的任何东西都不可能知道外圈中的任何东西。
因此,让我们定义IRegisterSpeechUseCase接口及其实现RegisterSpeechUseCase。这些类型属于EduSync.Speech.Application。
它将输入对象作为命令。
然后是接口
然后RegisterSpeechUseCase如下所示:
让我们定义IUnitOfWork和ISpeechRepository等依赖项,这些接口属于核心域,将在基础设施上实现。
ISpeechRepository需要一个语音实体,所以让我们在核心域上创建它
一切都编译成功,但我的测试失败了。
如您所见,测试失败是因为我验证需要调用CreateAsync和Commit方法,所以让我们在RegisterSpeechUseCase类上调用SpeechRepository.CreateAsync和IUnitOfWork.Commit
然后在我的单元测试的arrange部分创建SpeechRepository.CreateAsync和IUnitOfWork.Commit的mock
所有测试都是绿色的了,但我的代码覆盖率还不够:
例如,如果我注释掉这个块,我的测试将成功,但如果command为空,我的应用程序将在运行时崩溃
让我们添加一个新的测试来修复它
最后,LogCorner.EduSync.Speech.Application的代码覆盖率为100%
但是如果我替换了赋值会发生什么?
var title = command.Type;
var urlValue = command.Title;
var description = command.Url;
var type = command.Description;
所有测试都会成功,但我的应用程序将处于无效状态,因为它将插入标题而不是 url,...。
我可以在测试断言中使用moqSpeechRepository.Verify修复它,但我会保留它并在通过引入值对象实现我的领域时修复它
下一步,我将实现领域模型。
源代码可在此处获得:RegisterSpeechUseCase(https://github.com/logcorner/LogCorner.EduSync/tree/Feature/Task/RegisterSpeechUseCase)
欢迎关注我的个人公众号”My IO“