Clean Architecture
国内对于Clean Architecture的翻译很多,干净/整洁/清晰。但无论哪一种都说明了它简洁、清晰的特性。
早期它长这样
看到这张图的同学可能会对另外一张图有印象
洋葱架构(Onion)
现在长这样
看起来好像是亲戚,它们的确也有着千丝万缕的关系
分析Clean Architecture
这部分主要是根据explicit architecture文章的理解整理的,有翻译也有自己理解消化的。如有错漏欢迎指正,谢谢
三大构建块
用户界面
基础设施
应用核心
控制流
用户界面
应用核心
基础设施
应用核心
用户界面
工具
左右两侧形成鲜明对比,动机不同
HTTP/CLI:告诉应用要做什么
SMS/Mailing Server/Search Engine...:应用告诉它们要做什么
链接工具和交付机制到应用核心
将工具连接到应用程序核心的代码单元称为适配器(端口和适配器架构)。
告诉我们的应用程序做某事的适配器称为主适配器或驱动适配器,而我们的应用程序告诉我们做某事的适配器称为辅助适配器或驱动适配器。
端口
这些适配器为了适应应用核心的一个非常特定的入口点,即端口。端口只不过是工具如何使用应用程序核心或应用核心如何使用它的规范。
你可以看作是接口和DTO
主适配器或主动适配器
主适配器或主动适配器围绕一个端口并使用它来告诉应用核心该做什么。
我们的主动适配器是Controller或Console Commands,它们在其构造函数中注入了一些对象,该对象的类实现了Controller或Console Commands所需的接口(端口)。
端口可以是控制器需要的服务接口或存储库接口,然后将 Service、Repository 或 Query 的具体实现注入并在 Controller 中使用。
或者,端口可以是Command Bus或Query Bus的接口。在这种情况下,将Command Bus或Query Bus的具体实现注入到Controller中,然后Controller构造Command或Query并将其传递给相关Bus。
注:这里其实提到了CQRS
从适配器或被动适配器
与围绕在端口周围的主动适配器不同,从适配器实现一个端口、一个接口,然后在需要端口的任何地方注入应用核心。
可以理解为是右侧是符合应用核心的需要的接口或者对象,而左侧则是包装用例的传达机制,如HTTP/CLI等
假设我们有一个需要持久化数据的需求:
我们创建一个持久化接口(在左侧端口周围),有一个保存数据的方法和一个通过ID删除数据的方法
基础设施中提供一个实现类,通过IoC注入这个接口和对应的实现类
在需要持久化数据的类的构造函数中注入一个持久化接口
如果有一天我们需要从SQL Server换到MongoDb,只需要替换步骤2中的实现类和注入新的实现类即可
IoC
与上图相比,仅仅是多了一个蓝色的箭头从外部直插入应用核心内部。在上面例子中有提到通过IoC的作用这里就不再重复了。
至此,我们一致在讲解应用核心的外围,而应用核心是我们架构设计的重点。
应用核心组织
洋葱架构采用 DDD 分层并将它们合并到端口和适配器架构中。这些层旨在为业务逻辑带来一些组织,即端口和适配器“六边形”的内部,就像在端口和适配器中一样,依赖方向是朝向中心的。
应用层
用例(Use Case)是我们的应用中的一个或多个用户界面触发的流程(业务逻辑)。用户界面可以是终端用户界面也可以是管理界面,或者控制台界面和API。
用例在应用层定义,由DDD和洋葱架构提供,它可以包含端口,ORM接口,搜索引擎接口,消息接口等,也可以是CQRS处理Handlers的地方,发送邮件,调用第三方API等。
应用服务/Command Handler包含用例的业务逻辑,作用是:
使用Repository查找一个或多个实体
告诉这些实体做一些领域逻辑
使用Repository持久化这些实体,保存数据更改
Command Handler可以有两种不同的使用方式:
包含执行用例的真实业务逻辑
作为架构中的中间件,接收Command并触发应用服务中的逻辑
领域层
领域内的对象除了有对象本身的属性外,还可以操作该对象内部的属性,这是特定于域本身的,并且独立于触发该逻辑的业务流程,它们是独立的,完全不知道应用层。
领域服务
有时我们会遇到一些涉及不同实体的领域逻辑,无论是否相同,该领域逻辑不属于实体本身,它没有直接责任。
那我们可以使用领域服务来承载这部分逻辑,可能有人会觉得那可以放应用层,但领域逻辑在其他用例中就不能重用了。领域逻辑应该在领域内部,不要上升到应用层。
领域服务可以使用其他领域服务,或者其他领域对象
领域模型
在最中心,依赖于它之外的任何东西,是领域模型,它包含代表领域中某些事物的业务对象。至于如何定义领域模型可以参考第一篇。
组件
组件与应用核心内所有的层交叉,从外贯穿到内部。例如身份验证、授权、计费、用户、评论或账户,但它们依然与领域有关。
像授权和身份验证这样的限界上下文应该被视为隐藏在某种端口后面的外部工具。
解耦组件
具有完全解耦的组件意味着一个组件不直接了解任何另一个组件。换句话说,它可能没有接口,所以我们需要一些新的架构结构。
比如事件、最终一致性、服务发现等。当你往这条路上走的时候,你就开始脱离单体了。
这里Dapr或许是个不错的选择,它包含了这些功能,对Dapr感兴趣的可以看之前的手把手教你学Dapr系列
MASA Framework解决方案
结合DDD和Clean Architecture以及MASA Framework的特性,我们将在MASA.BuildingBlocks中以接口的形式定义规范,在MASA.Contrib中对接口进行实现。
这意味着你可以只关心BuildingBlocks中的接口定义来编写你的代码,也可以基于接口重新实现在DDD落地中你自己的业务特性来调整或扩展我们提供的默认行为。比如,你有自己的UoW、仓储层等都可以随意换掉。
应用层
应用服务:
实现应用程序的用例,衔接表示层(接口层)与领域层
除此之外,基于MASA EShop的示例中的MASA.EShop.Services.Catalog的CQRS架构演示,应用层也可以承载CQRS的Command Hanlder。除了可以继续使用领域层来解决Command业务外,你也可以选择在此中止,在Command Handler里简化架构直接对Command进行处理。
示例代码:https://github.com/masalabs/MASA.EShop/tree/develop/src/Services/MASA.EShop.Services.Catalog/Application/CatalogBrands
工作单元:
默认事件是在应用服务中首次开启,所以UoW也会在应用层被激活(实际上底层会根据仓储的操作,只有首次增删改才会自动激活,这个功能可以关闭,改为手动控制)
中间件:
对于使用Event Bus开发来说,应用层还可以作为统一的AOP出入口。
例如统一的事件参数验证:
首先需要启用统一验证中间件 https://github.com/masalabs/MASA.EShop/blob/develop/src/Services/MASA.EShop.Services.Catalog/Application/Middleware/ValidatorMiddleware.cs
然后为对应的Event/Command编写验证逻辑 https://github.com/masalabs/MASA.EShop/blob/develop/src/Services/MASA.EShop.Services.Catalog/Application/Catalogs/Commands/DeleteProductCommandValidator.cs
领域层
对于实体、聚合、值对象等概念就不再介绍了,可以参考上一章的内容。
贫血模型VS充血模型
领域中需要限定领域内的业务逻辑,加上EF Core对充血模型的支持,充血模型更适合用作领域模型的开发。
将数据与行为封装,表现出现实业务对象完整行为,每个领域具备明确的职责划分,将逻辑分散到领域对象中。这也是应用层与领域层的一个比较明显的区别。
对于实体相关对象,我们提供了对应的类,当然也包括审计和值对象可能需要用到的枚举类。
枚举类:我们提供了Enumeration,参考自:https://docs.microsoft.com/zh-cn/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types
领域服务:
领域服务中可以调用其他的领域服务(包括进程内或跨进程),所以我们提供了IDomainService,它的功能包括:
自动协调进程内和跨进程的事件传递
支持被调用方是CQRS
默认支持事件压栈,在UoW Commit后统一触发。也支持实时发送事件(如后续业务可被降级,但跨进程的事件为主业务逻辑不可被降级)
跨进程事件支持最终一致性和Saga
仓储:
领域中定义的仓储为接口,代表在当前领域内关心的业务。比如用户,在用户管理和名片两种业务中,对于IRepository<User>的定义是不同的。但在基础设施中BaseRepository<User>可以是同一个,因为BaseRepository<User>可以是最完整的实现,但领域内仓储服务只认其中一部分
基础设施层
给接口提供实现,如仓储接口的实现、Event Bus中MQ或中介者的实现(MASA Framework已实现,所以我们的示例中目前只有仓储接口的实现)等。
MASA Framework模板架构
在MASA Framework模板中提供了自由组合的方式,你可以根据你的需求随意调整如是否包含Blazor、Dapr、DDD、CQRS等。
我们的MASA.EShop推荐采用了4种架构方式,从简到繁,本篇介绍最复杂的一个
Minimal APIS + CQRS + Dapr actor
MASA.EShop中的Ordering服务就是采用这种架构分层,其实分层解释上面也有,只是之前解释的是站在MASA BuildingBlocks的角度,而接下来将是站在开发者角度。
User Interface Layer:它负责提供用户接口,完整前端逻辑。用户也可以是计算机系统,不特指是人。所以这里既可以是API,也可以是Blazor、MVC等。
Application Layer:它可以很薄也可以很厚(在当前分层下推荐薄)。负责协调User Interface和Domain,包括服务的编排和转发,AOP,发送事件等。
如果你有Domain Layer可以把Command做的很薄调用Domaiin。如果你要精简CQRS,也可以不用Domain,在这一层直接做应用服务。当然Query也一样,但Query即便使用Domain也推荐把查询放在应用服务里,这样可以把Query和Command分离来获得CQRS的优势。
Domain Layer:业务核心,包括了领域对象和领域服务以及适配器的接口。建议采用充血模型将行为留在领域内,跨领域且需要被复用的可以使用领域服务。仓储接口则限定领域内的仓储行为,与物理仓储不同的是更聚焦业务本身,而不是实体的完整仓储能力。
Infrastructure Layer:给接口提供实现,如仓储接口的实现、Event Bus中MQ或中介者的实现(MASA Framework已实现,所以我们的示例中目前只有仓储接口的实现)等。
总结
至此,我们不仅实现了对单体架构的支持,还通过Event Bus对微服务架构提供了支持。
如果你对DDD或者MASA Framework感兴趣,不妨把MASA.EShop跑起来看一下,它提供了4种架构方式参考,可以满足大部分业务场景对架构的要求。
学以致用,学无止境。
参考:
DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together:https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/
开源地址
MASA.BuildingBlocks:https://github.com/masastack/MASA.BuildingBlocks
MASA.Contrib:https://github.com/masastack/MASA.Contrib
MASA.Utils:https://github.com/masastack/MASA.Utils
MASA.EShop:https://github.com/masalabs/MASA.EShop
MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor
如果你对我们的 MASA Framework 感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们