原文链接
如今,洋葱架构和六边形架构为我们提供了诸多好处,例如可测试性,代码的可维护性以及相对于外部框架的独立性等等。在这这教程中,我会教你使用整洁架构(clean architecture)的方法与工具,例如领域驱动设计(DDD),测试驱动开发,CQRS,事件源,容器化,Oauth2,Oidc等,并以此来构建一个微服务。
关于整洁架构的更多内容,推荐阅读 Robert C. Martin的这篇文章 。
上图中的同心圆代表软件的不同领域。一般来说,越向外,软件的层级就越高。外圈是机制,内圈是策略。
使得整个架构工作的最重要的规则是依赖规则,该规则指出,源码的依赖只能指向内部。内圈对于外圈一无所知。特别是,在外圈中声明的任何名称都不能由内圈中提及,包括,方法,类,参数或其他实体名称。
出于同样的原因,外圈中使用的数据格式不应由内圈使用,特别是在外圈中使用的格式是由某个框架生成的。我们不希望外圈中的任何内容影响内圈。
CQRS将命令与查询分离。
命令是改变应用程序状态的操作,它没有返回数据。查询返回数据但并不修改应用状态。 因此,CQRS是一个非常有用的原理。在微服务世界中,我们使用两个数据库创建了我们的应用程序:
由于大部分的应用读数据都比写数据更加频繁,在我们容器化的时候,打个比方,我们可以为我们的命令端规划2 pods,查询端规划10 pods。
CQRS适用于领域驱动设计。领域驱动设计致力于建立丰富的领域模型来处理复杂的业务逻辑。
改变数据会造成更多的漏洞。因此,弄清楚应用的哪部分修改数据,哪部分不修改数据有助于维护与调试。
事件源将所有的改变存到一个对象中,构成了 事件仓库 中的一系列事件。
我会使用这个原理来:
我将要建立的系统,是帮助演讲者与参加者订阅相关事件的,会议,演讲,交流会等。
我的项目结构如下:
架构的最内层,包含领域对象与业务规则,并定义了对外的接口。
数据库,网络连接,文件系统,UI以及特殊的框架,都是不允许出现的。
核心域对自己以外的事一无所知。
依赖使用接口注入到我们的核心域中。
该层指向核心域并包含应用特定的业务规则。
编排数据流并使用领域模型。
不存在对于数据库,UI以及特殊框架的依赖。
该层是表现层,包含Web,UI等。在我们API的上下文中,这意味着它接收http请求(POST/PUT/PATCH/DELETE),并返回JSON格式的内容。
该层是数据库与网关相关,在这里,我们定义数据访问层,仓库等等。
它包含了我们在领域层定义的接口的实际实现。
实现“演讲登记”使用场景。
要让测试可用,首先要实现的是RegisterSpeechUseCase。
public interface IRegisterSpeechUseCase : ICommandHandler<RegisterSpeechCommandMessage> { } 复制代码
public interface ICommandHandler<in TCommand> where TCommand : ICommand { Task Handle(TCommand message); } 复制代码
public interface ICommand { } 复制代码
IRegisterSpeechUseCase接口处理登记演讲的命令,该命令是一个RegisterSpeechCommandMessage对象。
RegisterSpeechUseCase实现了IRegisterSpeechUseCase接口:
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。
教程中从一个单元测试开始,逐步实现单元测试中需要的类,是测试驱动开发的方法。