如何实现多限界上下文的集成?


领域驱动设计(DDD)自2003年由Eric Evans提出以来一直存在。由于它可以为我们的软件开发实践和成果带来潜在的好处,我经常与开发人员一起介绍它的概念,并将其作为推荐的方法进行宣传,特别是对于我们更复杂的解决方案。

在回答他们的问题或常见的误解时,我发现了两个主要的反复出现的话题:多个有边界的界限上下文和聚合设计的集成。

关于语言
任何学习过DDD的人都已经了解了与它一起使用的许多概念:界限上下文、实体、聚合、值对象等。其中一些,由于其直接的代码关联,更容易吸收和应用,而其他人则由于其抽象的性质而被降级。无处不在的语言就是这样一个例子,尽管它简单而强大,但很容易被忽视或遗忘。

我们的目标是能够建立一种明确的方式或语言,用于专家和开发人员之间的交流。这意味着所有在代码中表示或没有表示的概念,例如类和进程,都不应该被任意命名或以开发人员为中心的观点,而是作为关于您的软件解决方案如何试图解决给定问题的聚合讨论的结果。

也许一个经典的例子是我们倾向于将使用系统的个人称为用户,而领域专家将其称为客户。或者倾向于将我们操作的概念称为SomethingData或SomethingInformation。

这种看似无害的差异是一个可能导致混乱或“破碎的电话游戏”的例子,在这种情况下,当意图从业务预期发生的事情转换为实现的事情时,意图就会丢失。

收敛到正确的语言是强大的,因为它可以帮助您将隐式定义显式化,这总是一件好事。想象一下,您发现它实际上是一个目的地地址,而不是调用“Shipping Information”。

现在让我们谈谈范围。在编程语言中,作用域很容易理解为程序中某个项目(如变量)被识别的区域。在该范围之外,不保证存在相同的概念,甚至具有相同的值/含义。当我们把这个讨论带到我们的非编程语言时,我们承认我们精心制作的语言有局限性。这意味着,不追求可以在所有应用程序中使用的概念的通用企业范围定义是可以的,甚至是鼓励的。

为了定义这样的限制并探索为什么需要这样做,有必要讨论有边界感的界限上下文。

让我们有一些边界感
在讨论有界的界限上下文(Bounded Contexts,BC)的概念时,Eric Evans指出我们应该“显式定义模型应用的上下文。在团队组织、应用程序特定部分的使用以及代码库和数据库模式等物理表现形式方面显式地设置边界。保持模型在这些界限内严格一致,但不要被外部问题分散注意力或混淆。

这句话暴露了一个事实,即我们的模型,以及最终定义它们的语言,都有一个限度。它还强化了有一些 "在 "极限之外的东西,我们不应该被其 "分心"。虽然讨论如何定义这些界限不属于本文的范围,但我想把重点放在限制的存在上,如图1所示。


在此图中,我们注意到以下特征:

  1. 限界上下文之间可能有也可能没有关系
  2. 从一个限界上下文到另一个模型可以具有相同的名称但不同的定义
  3. 从一个限界上下文到另一个模型可以有不同的名称,但具有一些或所有共同特征

对于它们之间没有任何关系的限界上下文,没有什么好说的,只是重申如果你在两者中找到相同的模型名称就可以了。假设这是仔细决定/迭代的结果。

另一方面,对于共享关系的限界上下文,让我们看一下上下文映射,它提供有关它们的关系如何影响它们的模型和语言的附加信息。

上游/下游

在这里,上游是决定关系的人,使下游采用该语言作为自己的一部分。在我们的示例中,不仅在代码中使用了 Customer Profile,而且还被识别为 Checkout 限界上下文的概念部分。


反腐败层

在此示例中,Warehouse 不需要从产品信息管理 (PIM) 导入产品定义,而是定义一个本地概念,该概念具有所需信息的子集。

让我们从实现的角度仔细看看这些示例。

连接点
我们的第一个实现将说明如何表示上游/下游关系。代码和讨论将集中在集成方面,而不是其他与 DDD 相关的方面。

我们虚构的 Customer Profile 定义了一个名为Ordering Preferences的模型,供 Checkout 使用。想象一下,作为其运营的一部分,它需要了解客户的预定义决定,以加快交付所有购买的商品。

我们的应用程序服务——命令处理程序——需要为试图启动结账流程的客户获取订购偏好。有了这些信息,就可以创建一个Checkout来封装预期要遵循的业务规则。

所以OrderingPreferences是一个本地概念,从 Customer Profile 导入并通过 OrderingPreferencesService获得。一种常见的方法是将其表示为一个接口。

在我们的基础设施层中,我们有这样一个接口的具体实现,它实际上与客户资料交互并产生预期值对象。

在这里我们看到我正在使用某种 HTTP 客户端发出此请求以检索数据,此时它只是一个数据集合。然后我使用该数据创建 OrderingPreferences ,这是公认的本地概念。

现在让我们看看我们的第二个实现,它旨在说明您何时不导入概念,而是必须根据外部定义构建一个本地概念。

我们有一个仓库操作,为了处理和运送客户订单,要求我们提供一些用于海关目的的监管信息。与下订单时收到的上下文信息相反,这些额外的信息位于产品信息管理 (PIM) 的另一个限界上下文中。在 PIM 中,此信息是产品模型的一部分。


上图 由部分产品定义组成的自由贸易协定。

经过讨论,决定在当地没有必要有相同的产品概念,所需的信息称为自由贸易协定。

像以前一样,我们有一个应用程序服务,它注入了依赖项。

首先要提到的是,您没有将 PIM 的产品定义公开给您的应用程序。它期望一个自由贸易协定值对象作为输出。和以前一样,一种方法是定义一个具有具体基础设施实现的接口。

TranslatingFreeTradeAgreementService的前缀旨在明确此服务的作用,即采用外国概念并生成遵循本地语言的内容。

至于信息的实际检索及其操作,一种推荐的方法是在适配器和翻译器之间拆分职责,前者提出实际请求,后者负责验证并将必要的信息传递给您值对象。

这很好,但我需要所有这些吗?
使用的结构肯定有很多移动部分:接口、具体的翻译实现、适配器和翻译器。您的第一反应可能是认为它在现实生活中没有用处太多或太复杂。最后,决定将取决于您和您采用的开发生态系统、编程语言和工具。

这里简要总结了每个组件的用途和一些实际注意事项。

1、服务接口
如果您知道并相信 SOLID 原则可以促进良好实践,那么该界面可以帮助您专注于意图,而不是被迫预先对太多实施细节做出决定。
一些开发语言允许您动态替换现有的实现以方便测试,这可以说可以让您跳过接口定义。

2. 翻译服务
这里的目标是确保您的翻译服务知道它需要联系哪些服务才能返回您的应用程序期望的本地概念。
它依靠一个或多个适配器来执行其指令,更像是一个编排器。

3.适配器
它是实际使用基础设施、http、gRPC 等从外部限界上下文中获取数据并将其传递给翻译器的一种。
如果您只有一个 BC 要联系,一个潜在的简化方法是直接从翻译服务进行远程呼叫和翻译。

4. 译者
获取从外部限界上下文接收的数据,验证它是否包含我们需要的结构,并使用它来创建所需的值对象。
翻译中不应存在任何实际的业务规则。在这里,您要验证预期存在的数据是否确实存在,丢弃我们不需要的数据,必要时在数据中执行最小转换,并将其传递给值对象以进行实际的域验证。

由于您将不得不编写代码来执行此操作,如果您想避免创建单独的类,您可以将此功能分组为适配器的内部部分。

从视觉上看,这是所有部分之间的关​​系:


我的建议是遵循垂直开发方法并不断重构您的代码。随着您的进步和获得更多关于所需集成的知识,这种功能分解将变得更加容易。

结论
集成多个限界上下文更像是一种规范而不是例外,因此花时间了解可以维护这些关系的多种形式是必不可少的。正如我们所看到的,这不是“魔法”,但确实需要一些时间来了解如何进行这种集成,而不仅仅是技术方面。

如果您将依赖同步或异步通信,REST 或 gRPC 是重要且必要的方面,通常应推迟到您实际需要做出决定时再考虑。而且,这是在了解关系类型和将采用的(领域)语言之后发生的。