设计面向 DDD 的微服务

小窍门

此内容摘自电子书《适用于容器化 .NET 应用程序的 .NET 微服务体系结构》,可以在 .NET Docs 上获取,也可以下载免费的 PDF 以供离线阅读。

适用于容器化 .NET 应用程序的 .NET 微服务体系结构电子书封面缩略图。

域驱动设计(DDD)倡导根据业务现实进行模型建立,并与业务场景相关联。 在构建应用程序上下文中,DDD 将问题作为域进行讨论。 它将独立问题区域描述为边界上下文(每个边界上下文都与微服务相关),并强调一种共同语言来讨论这些问题。 它还提出许多技术概念和模式,如具有充血模型的域实体(无贫血模型)、值对象、聚合和聚合根(或根实体)规则,用于支持内部实现。 本部分介绍这些内部模式的设计和实现。

有时,这些 DDD 技术规则和模式被视为具有陡峭学习曲线以实现 DDD 方法的障碍。 但重要的部分不是模式本身,而是组织代码,使其与业务问题保持一致,并使用相同的业务术语(无处不在的语言)。 此外,仅当实现具有重要业务规则的复杂微服务时,才应应用 DDD 方法。 更简单的责任(如 CRUD 服务)可以通过更简单的方法进行管理。

在设计和定义微服务时,在何处绘制边界是关键任务。 DDD 模式可帮助你了解域中的复杂性。 对于每个界定的上下文的域模型,需确定和定义为域建模时所需的实体、值对象和聚合。 生成并优化包含在定义上下文的边界内的域模型。 如果是微服务的形式,这会十分明晰。 这些边界中的组件最终成为微服务,但在某些情况下,BC 或业务微服务可以由多个物理服务组成。 DDD 与边界有关,微服务也是如此。

保持微服务上下文边界相对较小

确定界定的上下文之间的边界需要在两个相互冲突的目标之间进行权衡。 首先,你希望最初创建最小的微服务,尽管这不应是主要驱动程序;应围绕需要凝聚力的事情创建边界。 其次,需要避免微服务之间的聊天通信。 这些目标可以相互矛盾。 要平衡这些目标,应将系统分解为尽可能多的小型微服务,直至通信边界数量迅速增加,同时不断试图划分新的界定的上下文。 在有限的上下文中,凝聚力是关键。

它类似于实现类时的不适当亲密关系代码异味。 如果两个微服务需要相互协作,它们可能应该是相同的微服务。

另一种看待这一方面的方法是自治。 如果微服务必须依赖另一个服务直接服务请求,则它并不真正自主。

DDD 微服务中的架构层

大多数具有重大业务和技术复杂性的企业应用程序都由多层定义。 层是一个逻辑项目,与服务的部署无关。 它们的存在可帮助开发人员管理代码中的复杂性。 不同的层(如域模型层与呈现层等)可能有不同的类型,这要求在这些类型之间进行转换。

例如,可以从数据库加载实体。 然后,该信息的一部分,或者包含来自其他实体的其他数据的聚合,可以通过 REST Web API 发送到客户端 UI。 此处的要点是域实体包含在域模型层中,不应传播到它不属于的其他区域,例如表示层。

此外,还需要拥有始终有效的实体对象(请参阅由聚合根(根实体)管理的域模型层中设计验证的部分)。 因此,实体不应绑定到客户端视图,因为在 UI 级别,某些数据可能仍未得到验证。 这是 ViewModel 的用途。 ViewModel 是专用于表示层需求的数据模型。 域实体不直接属于 ViewModel。 相反,需要在 ViewModel 和域实体之间进行转换,反之亦然。

处理复杂性时,必须有由聚合根控制的域模型,确保与该组实体(聚合)相关的所有不变量和规则通过单一入口点,即聚合根,得到执行。

图 7-5 显示了如何在 eShopOnContainers 应用程序中实现分层设计。

显示域驱动设计微服务中的层图。

图 7-5. eShopOnContainers 订单微服务中的 DDD 层

DDD 微服务(例如,订购)中的三个层。 每个层都是 VS 项目:应用程序层是 Ordering.API,域层是 Ordering.Domain,基础结构层是 Ordering.Infrastructure。 你想要设计系统,以便每个层仅与某些其他层通信。 如果层作为不同的类库实现,这种方法可能更容易强制实施,因为可以清楚地确定库之间的依赖关系。 例如,域模型层不应依赖于任何其他层(域模型类应为普通旧类对象,或 POCO、类)。 如图 7-6 所示, Ordering.Domain 层库仅依赖于 .NET 库或 NuGet 包,但不依赖于任何其他自定义库,例如数据库或持久性库。

Ordering.Domain 依赖项的屏幕截图。

图 7-6. 作为库实现的层可以更好地控制层之间的依赖关系

域模型层

Eric Evans 的优秀书籍 《域驱动设计》 介绍了域模型层和应用程序层。

域模型层:负责表示业务的概念、有关业务情况的信息和业务规则。 反映业务状况的状态是通过这个层进行控制和利用的,但有关状态存储的具体技术细节则由基础结构负责实施。 此层是业务软件的核心。

域模型层是体现业务的地方。 在 .NET 中实现微服务域模型层时,该层将编码为类库,其中包含捕获数据加上行为的域实体(具有逻辑的方法)。

遵循 持久性忽略基础结构无知 原则,此层必须完全忽略数据持久性详细信息。 这些持久性任务应由基础结构层执行。 因此,此层不应直接依赖基础结构,这意味着重要的规则是域模型实体类应为 POCO。

域实体不应在任何数据访问基础结构框架(如 Entity Framework 或 NHibernate)上具有任何直接依赖项(例如从基类派生)。 理想情况下,领域实体不应派生自或实现任何基础设施框架中定义的类型。

大多数新式 ORM 框架(如 Entity Framework Core)都允许此方法,以便域模型类不与基础结构耦合。 但是,在使用某些 NoSQL 数据库和框架(例如 Azure Service Fabric 中的执行组件和 Reliable Collections)时,并不总是可能拥有 POCO 实体。

即使必须遵循域模型的持久性忽略原则,也不应忽略持久性问题。 请务必了解物理数据模型及其映射到实体对象模型的方式。 否则,可以创建不可能的设计。

此外,此方面并不意味着可以采用专为关系数据库设计的模型,并将其直接移动到 NoSQL 或面向文档的数据库。 在某些实体模型中,该模型可能适合,但通常不适合。 实体模型仍必须遵循的约束,具体取决于存储技术和 ORM 技术。

应用程序层

转到应用程序层,我们可以再次引用 Eric Evans 的书籍 域驱动设计

应用程序层: 定义软件应执行的作,并指示表达域对象解决问题。 此层负责的任务对业务有意义,或者需要与其他系统的应用程序层交互。 此层保持薄。 它不包含业务规则或知识,但仅协调任务,并将工作委托给下一层中域对象的协作。 它没有反映业务状况的状态,但它可以具有反映用户或程序任务进度的状态。

.NET 中的微服务应用程序层通常编码为 ASP.NET Core Web API 项目。 该项目实现微服务的交互、远程网络访问以及 UI 或客户端应用中使用的外部 Web API。 它包含使用 CQRS 方法时的查询、微服务接受的命令,甚至微服务间的事件驱动通信(集成事件)。 表示应用程序层的 ASP.NET 核心 Web API 不得包含业务规则或域知识(尤其是事务或更新的域规则):这些应归域模型类库所有。 应用程序层只能协调任务,不得保留或定义任何域状态(域模型)。 它将业务规则的执行委托给域模型类本身(聚合根和域实体),最终将更新这些域实体中的数据。

基本上,应用程序逻辑是实现依赖于特定前端的所有用例的地方。 例如,与 Web API 服务相关的实现。

目标是域模型层中的域逻辑、其固定、数据模型和相关业务规则必须完全独立于表示层和应用程序层。 最重要的是,域模型层不得直接依赖于任何基础结构框架。

基础结构层

基础结构层是最初保存在域实体(内存中)中的数据保存在数据库或其他持久存储中的方式。 例如,使用 Entity Framework Core 代码实现使用 DBContext 将数据保存在关系数据库中的存储库模式类。

根据前面提到的 持久性无知基础结构无知 原则,基础结构层不得“污染”域模型层。 您必须确保域模型实体类与用于持久化数据的基础设施(如 EF 或其他框架)保持无关,通过不对这些框架产生强依赖。 您的领域模型层类库应该仅包含领域代码,只有实现软件核心的 POCO 实体类,并且与基础结构技术完全分离。

因此,您的层或类库和项目最终应依赖于域模型层(库),而不是相反,如图 7-7 所示。

显示 DDD 服务层之间存在的依赖项的关系图。

图 7-7. DDD 中的层之间的依赖关系

DDD 服务中的依赖关系,应用程序层依赖于域和基础结构,基础结构依赖于域,但域不依赖于任何层。 此层设计应独立于每个微服务。 如前所述,可以采用 DDD 模式实现最复杂的微服务,同时以更简单的方式实现更简单的数据驱动微服务(单层中的简单 CRUD)。

其他资源