[译]具有上下文映射的战略领域驱动设计

[译]具有上下文映射的战略领域驱动设计

https://www.infoq.com/articles/ddd-contextmapping/

介绍

当应用程序规模和复杂性增长时,许多面向对象建模方法往往无法很好地扩展。上下文映射是一种通用技术,是领域驱动设计(DDD) 工具包的一部分,可帮助架构师和开发人员管理他们在软件开发项目中面临的多种类型的复杂性。与其他众所周知的 DDD 模式不同,上下文映射适用于任何类型的软件开发场景,并提供可能帮助开发人员做出战略决策的高级视图,例如是否在其特定项目中进行全面的 DDD 实施环境。

在本文中,我们将探讨有界上下文的多个方面,以及如何使用它们构建上下文映射来支持软件开发项目中的关键决策。

许多模型在发挥作用

领域驱动设计非常重视维护应用程序模型的概念完整性。这是通过几个因素的结合来实现的:

强调来自用户和领域专家的频繁反馈的敏捷流程,
真正的领域专家的可用性以及与他们的创造性合作,
根据通用语言精确定义的模型的单一共享版本(在应用程序和测试代码中),以及
促进学习和探索的开放、透明的环境。
这对于创建一个安全港至关重要,让高质量设计能够蓬勃发展并发挥其效益。在这样的地方,典型的 DDD 元素(例如实体、值对象和聚合)为复杂的领域模型提供了新兴的顺序。即便如此,传统的 DDD 方法也不能在不损害模型概念完整性的情况下盲目应用于无限大的领域模型。

如图 1 所示,通用语言在 DDD 中的关键作用是充当我们模型的完整性检查。使用相同的术语,具有非常精确和明确定义的含义,从与领域专家的讨论到代码级别,确保团队中的每个人都对领域和软件拥有相同的愿景。

云的真正承诺是轻松,而不是成本。DoiT 提供技术和云专业知识,以降低云成本并提高工程师的工作效率。全部来自 AWS、Microsoft 和 Google Cloud 合作伙伴。了解更多。

图 1. 通用语言应该是用于表达模型的唯一语言。团队中的每个人都应该能够就每个具体术语达成一致,没有歧义,并且不需要翻译

代码是模型的主要表达形式。尽管在捕获需求或设计部分的过程中可能需要其他工件,但唯一与应用程序行为保持同步的就是代码本身。这种建模必杀技是一个有点脆弱的生态系统:在前面描述的条件下,它可以实现,但不能无限期地延长。模型在不损害其概念完整性的情况下可以扩展的最大范围称为上下文。

输入有界上下文

在领域驱动设计中,上下文被定义为:
“单词或语句出现的环境,决定其含义”
,乍一看可能听起来相当模糊。它没有过多说明上下文的预期大小、形状或其他特征。我们最终会发现这个定义相当精确,准确地描述了上下文是什么,但为了了解这一点,我们可能需要一些具体的例子。

示例 1:相同术语,不同含义
让我们从一个简单的例子开始,其中术语层面可能会出现歧义。有些单词根据其使用的上下文而具有不同的含义。

假设我们正在开发一个基于 Web 的个人财务管理应用程序 (PFM)。我们可能会使用此应用程序来管理银行账户、股票和储蓄,跟踪预算和支出等等。

在我们的应用程序中,域术语“帐户”可能指不同的概念。谈到银行业务,账户是某种逻辑上的“金钱容器”;然后我们期望相应的类具有余额、帐号等属性。但是,在 Web 应用程序的上下文中,术语“帐户”具有非常不同的含义,与身份验证和用户凭据相关。相应的模型(如图 2 所示)将是完全不同的。

图 2. 一个有些微不足道的歧义案例:术语“帐户”可能根据其使用的上下文而具有非常不同的含义

这可能是我们在对应用程序进行建模时可能遇到的最简单的歧义情况:同一个术语,具有两种不同的上下文相关含义。这个问题通常可以通过在类名中添加一些前缀(无论是名称本身还是包的一部分)来划分名称空间来解决。但在概念层面上,我们必须意识到我们有两个不同的上下文在起作用,有时它们的不同足以防止开发人员犯错误,但有时差异可能很微妙。

不幸的是,在类名级别上工作可能并不总是一个可行的解决方案:在银行领域,术语“银行账户”可能已经存在,但具有不同的含义,或者领域专家会坚持认为“账户”是正确的术语。抵制发明特定权衡术语的诱惑,或引入从领域专家术语到代码的翻译偏移。您在这里面临两个不同的上下文。

绘制我们的第一个上下文图

当出现歧义时,我们需要一个工具来让开发团队意识到应用程序中存在两个不同的上下文。歧义是我们无处不在的语言的超级恶棍,我们需要摆脱它。做到这一点的最佳方法是根据上下文映射中的有界上下文公开域结构。图 3 显示了一个简单的上下文图。

图 3. 包含两个正在运行的域上下文的简单上下文映射

在领域驱动设计一书中,上下文映射被指定为用于使上下文边界明确的主要工具。基本思想是在白板上绘制上下文边界,可以选择用类的相关领域术语填充它们。这不是一个精确定义良好的 UML 图:它是一个工作工具,允许我们绘制模糊的情况,因此有点模糊的外观是必要的。

示例 2:相同的概念,不同的用途
当基本概念相同但以不同方式使用时,可能会出现更令人费解的区别,最终导致不同的模型。我们的银行账户模型可能是一个 BankingAccount 类,如下图 4 所示。

图 4. BankingAccount 类的真正简化版本

一些 PFM 应用程序还允许我们管理付款,通常保留收款人注册表。在这种情况下,收款人可能与一个或多个银行账户相关联,但在这种情况下,我们不会了解收款人银行账户的任何内部结构,也无法对这些账户进行任何操作。使用我们刚刚定义的 BankingAccount 类对收款人帐户进行建模是否有意义?

图 5. Payee 和 BankingAccount 类

嗯……听起来确实很合理:毕竟这是同一个概念,在现实世界中,我们的账户和收款人的账户甚至可能位于同一个实体银行。尽管如此,感觉并不完全正确:我们不应该对收款人银行账户进行任何操作,或跟踪任何相关内容。更糟糕的是:这样做可能会是我们应用程序中的概念错误。

那么我们应该做什么呢?我们刚刚在同一个应用程序中(再次)遇到了两个不同的上下文:这一次我们最终可以用两种不同的方式对同一领域概念进行建模,因为我们有两种清晰且不同的用途,每一种都需要一个不同的模型。BankingAccount 可能仍然是一个允许我们执行(或跟踪)特定操作(例如存款或取款)的类,而单独的类 PayeeAccount 可能具有与 BankingAccount 相同的一些数据(例如 accountNumber),但模型更简单以及绝对不同的行为(例如,我们不应该能够访问收款人的余额)。图 6 显示,尽管该术语具有明确的含义并且只有一个基本概念,但我们在应用程序中以不同的方式使用它。

图 6. 银行和收款人帐户类

这对某些人来说可能听起来很明显,但事实并非如此。在处理类图或 UML 建模工具时,您可能真的很容易开始对具有bankingAccount 属性的收款人进行建模,并认为“我已经为此创建了一个类”。有时,巴甫洛夫式的消除代码重复的尝试可能弊大于利。

应用于之前使用的示例的简单上下文映射可能如下图 7 所示。请注意,只要我们对环境的了解增加,就会反映在地图上。在本例中,我们将 PFM 应用程序上下文分为银行业务和费用跟踪。

图 7. 一个非常简单的上下文图:在领域模型的各个部分周围绘制轮廓显示了保留概念完整性的区域

在这种情况下,这两个上下文有一些逻辑重叠区域:银行帐户的概念在应用程序的不同部分以不同的方式使用,这意味着我们将使用不同的模型。然而,这两种模式可能会密切相互作用。除了在上下文边界内保留模型的概念完整性之外,上下文映射还帮助我们关注不同上下文之间发生的情况。在这种情况下,假设同一个团队正在处理两种上下文,我们需要团队中的每个人都了解两种不同的上下文,最终共享两个模型中出现的术语和概念的翻译图。

示例 3:外部系统
让我们再次考虑 PFM 应用程序。其中许多应用程序允许与金融机构在线服务进行某种类型的数据交换。在某些情况下,银行提供对家庭银行服务的实时访问,在另一些情况下,它们只是允许用户以通用标准格式(例如 Money 或 Quicken 格式)下载银行对账单。然而,从上下文映射的角度来看,交互性和通信的方向(单向或双向)是不相关的。重要的一件事是,我们将再次使用不同的模型。图 8 显示了 PFM 银行应用程序与在线银行服务应用程序的交互。

图 8. 与外部应用程序的交互自然需要上下文映射中的单独有界上下文

即使这两个模型被设计用来表示相同的数据(至少在一定程度上),它们也是不同的,随着时间的推移,它们将受到不同的进化力量的影响,并且它们服务于不同的目的。因此,需要单独的有界上下文。如果使用可用的第三方库对用户分析进行建模,示例 1 也可能属于此类。

管理多个上下文
当我们的应用程序跨越多个上下文时,我们还需要管理其间发生的情况。不同限界上下文之间的关系通常可以为我们的项目提供非常重要的见解。

要了解的最重要的事情之一是两个上下文之间关系的方向。DDD 使用术语“上游”或“下游”:上游上下文将影响下游对应项,而反之则可能不成立。这可能适用于代码(相互依赖的库),但也适用于技术性较低的因素,例如时间表或对外部请求的响应能力。在我们的示例中,我们有一个外部系统,显然不会根据我们的请求进行更改,而我们的 PFM 银行应用程序必须快速更新,以防在线银行服务因任何原因更改其 API。因此,我们的 PFM 环境将位于下游,而在线银行服务显然位于上游。图 9 说明了两个域上下文之间的这种关系。

图9. 单独上下文之间的上下游关系

我们可以接受根据外部需求更新与外部系统通信的方式,但我们可能需要一些保护措施,防止来自上游上下文的变更,并保持我们银行上下文的概念完整性。领域驱动设计(DDD)描述了几种组织模式,帮助我们描述和/或管理不同上下文之间的交互方式。这里最合适的模式称为反腐层(ACL),并要求在两个上下文之间在代码级别进行显式转换,或者更好的是:在银行上下文的外部边界进行。这可能不仅是技术转换,比如Java到XML的转换,还可能是一个地方,用于管理两端模型之间的所有微妙差异。在上下文地图上绘制我们的ACL将产生类似于下图10所示的图表。

图10. 在PFM应用程序边界上的反腐层,防止在线银行服务泄漏到我们的有界上下文中

不仅外部系统自然需要一个单独的上下文。一个现有的遗留组件通常具有不容易演变的模型。尽管在我们的组织内维护,甚至该模型也受到不同力量的影响,并且是与我们当前用途不同的结果。如果我们必须与一个遗留系统互动,很有可能这将在一个不同的有界上下文中。

那么上下文地图上的其他关系呢?我们能否根据关系式DDD模式对它们进行分类?由于我们假设开发是在一个单一团队内进行的,所以这里的模式可能稍显无趣。但是,如果银行和费用跟踪由不同的团队维护,它们可能会处于合作关系:它们都朝着共同的目标发展(上下游关系在它们处于相同级别时并不太有意义)。如果使用外部模块实现Web用户配置文件,则我们可能会将其“按原样”使用,这意味着我们是Web用户配置文件的下游和顺应者。

图11. 在我们勾画关系模式之后的上下文地图

例子4:组织规模扩大
到目前为止,我们考虑了一个只有一个开发团队的简单场景。这使我们得以忽略沟通成本,假设(或许是乐观地)团队中的每个开发人员都了解“模型的进展情况”。一个更复杂的场景可能包括以下一些影响因素:

  • 领域复杂性(需要许多不同的领域专家)
  • 组织复杂性
  • 较长的项目(时间)
  • 非常大的项目(人天)
  • 多个外部,独立或遗留系统涉及
  • 大团队规模或多个开发团队
  • 分布式或离岸团队
  • 人为因素
    这些因素中的每一个都会影响开发团队和组织整体内部沟通的方式,最终塑造出的软件。

在敏捷的共同环境中,一个单一团队有许多有效的方法可以在团队成员之间共享信息:面对面的对话,联合设计会议,成对编程,会议,信息辐射器等等。不幸的是,当团队规模或数量增加时,这些技术并不那么容易扩展,这使得在单一开发团队的边界内共享模型的概念完整性变得困难。

毕竟,达成对模型的共识是一种相当复杂的沟通形式,涉及对问题的共同理解以及对可能解决方案的相似看法。在沟通不那么容易的情景中,做事比达成共识要便宜得多。这种沟通瓶颈的经典结果是代码库中不同位置的不同类别,基本上在做相同的事情。

现在,让我们假设我们的PFM应用程序变得更大,另一个团队(Team B)被分配与我们一起(我们显然是Team A)在同一应用程序的新交易模块上工作。团队B可能位于不同的房间,架构物,城市,公司或国家,并且完全致力于交易领域。在下面的例子中,即使Team A倾向于在代码库的不同部分上工作,Team A与Team B共享了一些代码。最终,Team B编写了一些在图12中所示的类(A),该类实现了Team B所需的功能,这些功能在类A中已经可用。

图12. 在不同团队访问同一代码库的场景中,它们可能对模型的某些部分有不同的看法。团队的物理分布将影响团队之间共享的信息的质量

这就是代码复制,一切邪恶之源!在一个单一,明确定义的有界上下文中,这绝对是正确的。但是由于某种原因,在几乎每个非平凡的项目中都会发生这种情况。通常,这表明可能

在项目的同一区域存在不太明确的上下文。有时,这使得两个分开的上下文比强迫两个不同的团队不断集成他们的视野更有效地构造我们的领域模型。

那么,我们如何在我们的地图上绘制这个呢?上下文地图反映了我们对整个系统的当前理解水平,并且将随着我们学到更多信息或环境变化而更新。当前,我们不知道确切发生了什么,这就是“我们当前的理解水平”。

图13. 我们不太明确的交易上下文,需要进一步探讨或进行明智的设计决策

图中的警告标志意味着那里有些不对劲:两个上下文部分重叠,它们的关系不明确。这可能是要解决的第一个区域之一,试图在上下文内建立一个达成一致且可持续的关系,如客户-供应商,持续集成或共享内核。但那是明天的工作。上下文地图是今天的工具,今天的问题仍然未解决,因此我们在图中留下了警告标志。

不要被颜色和阴影愚弄:我尽量使上下文地图在打印时看起来不错。一个真实的上下文地图可能看起来很混乱,至少和你的项目一样混乱。但是这里的警告标志告诉我们,有一个关键区域的上下文尚未明确分开,整个事物可能很容易变成一个“大球”(DDD组织模式中最有弹性的模式),除非我们采取措施。

非常规的观点
上下文映射迫使我们将非软件方面纳入整个图景,最终发现传统的架构分析可能认为“超出范围”的热点区域。

例如,在组织内部的沟通流程主要影响最终的软件。总的来说,在小规模上使用是定义上下文边界的主要因素,而在大规模上,沟通速度和项目组织成为关键因素。诸如维基,电子邮件或即时消息之类的工具给了我们一个团队持续同步每个人的知识的虚假观念。但我们都知道这只是一个梦想:在典型的大型项目中,我们不是博格式的集体智慧的一部分,一些人几乎不知道他们团队之外发生了什么。

在大型组织中定义上下文边界是一项具有挑战性但有益的工作。许多时候,团队并不知道不同的上下文正在发挥作用;模型概念完整性的违反发生是因为很少有人或没有人看到整体画面。绘制上下文地图是一种调查活动,许多信息片段可能在第一次尝试时不正确,边界最初模糊,需要几个步骤才能获得对整体画面的清晰快照。

图14. 我们地图的最新版本。不要期望它是“最终版”,总有更多要学习的东西

可能存在更多的上下文在发挥作用,例如交易可能与一些在线股票定价服务相连接,但那是一个交易问题!上下文地图与我们的周围环境有关,我们(Team A)正在处理应用程序的银行和费用跟踪领域:我们只对我们直接连接并可能影响我们软件的上下文感兴趣。

只要我们收集更多信息,地图就会变得更清晰。如前所述,仅仅承认在我们的应用程序中有不同的模型,并且模型的完整性只能在明确定义的有界上下文内得以保持,对我们的领域建模视角提供了很大的价值。许多模型在增长过程中失去完整性,上下文映射在这方面非常有帮助。

关于战略DDD模式

在这里,我们对模式的使用有一个微妙的区别:尽管定义是相同的 - 对重复问题的已证明的解决方案 - 但这些很少代表我们可以选择的解决方案。往往,组织结构将强加模式,我们唯一的希望是在陷入无法取胜的情况之前认识到它们。有时我们将有机会选择最佳选项,或更改现有情况,但我们必须意识到组织层面的变化可能需要比项目范围允许的时间更多,或者简单地超出我们的可能性。

如果你不确定从哪里开始,请从开发团队开始。团队可能是最大的组织单元,可以有效地共享对模型的愿景。一旦认识到,同一个团队可能会管理多个上下文,从而归结为主要是架构选择。

每个模式都有不同的成本分配:即使它们解决了类似的问题(连接上下文),它们也不能轻松交换。例如,反腐层在代码级别留下足迹(额外的层),在组织中几乎没有足迹。而合作关系或客户-供应商可能需要更少的代码和一个单一的代码库,但在没有高效的沟通渠道和明确定义的流程的情况下可能无法运作。试图建立一个没有协作环境的合作关系显然是一种死胡同的策略。

结论

事实证明,上下文的原始定义 - “一个词或陈述出现的环境,决定其含义” - 是相当精确的,并且在从设计级别扩展到架构和组织级别时没有失去精度或有效性。尽管存在一些合理的“一致性欲望”,但模型不能无限延伸。有界上下文提供了定义良好的安全港,允许模型在复杂性增长的情况下发展,而无需牺牲概念完整性。

作为副作用,当应用于大规模项目时,上下文地图还显示了存在于组织内的隐含边界,提供了我们的项目将努力奋斗的阶段的生动,未经修饰的快照。一个好的上下文地图将给你展示面临的难题的图片。你实际上可能会知道组织是否 - 有意或无意地 - 在项目成功之前就在针对你的项目。

作为一名顾问,我发现上下文映射对于快速掌握客户项目环境的关键细节非常有帮助,并且作为一种战略决策支持工具(这就是地图的用途)。上下文图提供了 UML 或架构图完全忽略的系统整体概述,帮助我们专注于在您的场景中真正可行的选择,而不会在“大规模的一厢情愿”中浪费金钱。

关于作者
Alberto Brandolini 是一名信息技术顾问和培训师,拥有全面的软件开发方法。作为Avanscoperta (一家位于意大利的咨询和软件开发公司)的创始人和所有者,他也是意大利领域驱动设计和Grails社区的推动者。您可以在他的英文博客Ziobrando’s Lair或Twitter上关注 Alberto 的想法。