写点什么

MASA Framework - DDD 设计(2)

  • 2022 年 2 月 23 日
  • 本文字数:4343 字

    阅读完需:约 14 分钟

Clean Architecture

国内对于 Clean Architecture 的翻译很多,干净/整洁/清晰。但无论哪一种都说明了它简洁、清晰的特性。


早期它长这样



看到这张图的同学可能会对另外一张图有印象


洋葱架构(Onion)



现在长这样



看起来好像是亲戚,它们的确也有着千丝万缕的关系

分析 Clean Architecture

这部分主要是根据 explicit architecture 文章的理解整理的,有翻译也有自己理解消化的。如有错漏欢迎指正,谢谢

三大构建块

  • 用户界面

  • 基础设施

  • 应用核心


控制流

  1. 用户界面

  2. 应用核心

  3. 基础设施

  4. 应用核心

  5. 用户界面


工具

左右两侧形成鲜明对比,动机不同


  • 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 等



假设我们有一个需要持久化数据的需求:


  1. 我们创建一个持久化接口(在左侧端口周围),有一个保存数据的方法和一个通过 ID 删除数据的方法

  2. 基础设施中提供一个实现类,通过 IoC 注入这个接口和对应的实现类

  3. 在需要持久化数据的类的构造函数中注入一个持久化接口

  4. 如果有一天我们需要从 SQL Server 换到 MongoDb,只需要替换步骤 2 中的实现类和注入新的实现类即可

IoC

与上图相比,仅仅是多了一个蓝色的箭头从外部直插入应用核心内部。在上面例子中有提到通过 IoC 的作用这里就不再重复了。


至此,我们一致在讲解应用核心的外围,而应用核心是我们架构设计的重点。


应用核心组织

洋葱架构采用 DDD 分层并将它们合并到端口和适配器架构中。这些层旨在为业务逻辑带来一些组织,即端口和适配器“六边形”的内部,就像在端口和适配器中一样,依赖方向是朝向中心的。

应用层

用例(Use Case)是我们的应用中的一个或多个用户界面触发的流程(业务逻辑)。用户界面可以是终端用户界面也可以是管理界面,或者控制台界面和 API。


用例在应用层定义,由 DDD 和洋葱架构提供,它可以包含端口,ORM 接口,搜索引擎接口,消息接口等,也可以是 CQRS 处理 Handlers 的地方,发送邮件,调用第三方 API 等。


应用服务/Command Handler 包含用例的业务逻辑,作用是:


  1. 使用 Repository 查找一个或多个实体

  2. 告诉这些实体做一些领域逻辑

  3. 使用 Repository 持久化这些实体,保存数据更改


Command Handler 可以有两种不同的使用方式:


  1. 包含执行用例的真实业务逻辑

  2. 作为架构中的中间件,接收 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,欢迎联系我们



发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2021.10.26 加入

还未添加个人简介

评论

发布
暂无评论
MASA Framework - DDD设计(2)