HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

基于上下文图的策略性领域驱动开发

  • 2010-04-06
  • 本文字数:7754 字

    阅读完需:约 25 分钟

简介

当应用程序逐渐变得庞大和复杂后,很多面向对象建模的方法都达不到非常好的可伸缩性。上下文图是一种通用目的的技术,作为领域驱动开发大家族的一名成员,它帮助架构师和开发人员管理他们在软件开发项目中不得不面对的形形色色的复杂性。与其他广为人知的 DDD 模式相比,上下文图可以应用在任何软件开发的场景中,在开发者进行策略性决策时,为他们提供一个高层视图,比如是采用全套的 DDD 实现,还是根据项目的特定条件进行取舍等。

在这篇文章中,我们将探讨界限上下文,以及如何在构建上下文图时应用它们,来支持软件开发项目中的关键决策。

多个模型共存

领域驱动开发花了很大力气强调一件事,即维护应用程序模型的概念完整性。要做到这一点,需要很多因素:

  • 一种敏捷的流程,确保能从用户和领域专家那里频繁地获得反馈,
  • 确保有若干领域专家在场,并且与他们开展创造性的合作,
  • (在应用和测试代码中)维护单一的、可共享的模型,并用通用语言精确地进行定义它,
  • 营造一种开放透明的环境,鼓励学习与探索。

这些对于营造一个可以让高质量的设计繁荣发展的环境至关重要。即使是这样的环境,那些常见的 DDD 元素,比如实体、值对象、聚合,也会逐渐地形成一个复杂领域模型。所以,如果不对模型的概念完整性进行妥协的话,传统的 DDD 方法也不能盲目地应用在一个无限大的领域模型中。

如图 1 所示,在 DDD 中,通用语言的关键责任是对模型进行完整性检查。无论是与领域专家的讨论,还是最终的实现代码,都可以通过使用相同的术语,并将领域知识清晰准确地进行定义,以此来保证团队中的每位成员可以分享都相同的领域知识和软件。

图 1. 通用语言应该是用于表达模型的唯一语言。团队中的每位成员应该对每个特定术语达成一致。这些术语不能有歧义,也不允许在不同角色间进行翻译。

代码是表达模型的主要形式。虽然其他一些工件或许也能捕获需求或者局部设计,但是只有代码自身才会与应用程序的行为永远保持一致。不过这种看上去美妙的建模方式其实非常脆弱:如果满足了前面提到的四条要求,它能做到,但是不能被无限地扩展。真正让模型得以最大程度地扩展,并且不必牺牲其概念完整性的方法叫做“上下文”。

了解“界限上下文”

在领域驱动设计的世界里,“上下文”是这样定义的:
“一个单词或一个句子所出现的环境,这个环境会反过来影响它们的含义。”
这段定义初看起来相当含糊。它并没有直接告诉我们“上下文”的大小、形状或者其他什么特性。但是最后我们又发现这个定义其实非常准确地描述了“上下文”是什么,如果要想窥得其全貌的话,大概还需要一些具体的例子。

示例 1:术语相同,含义不同

第一个例子很简单,它示范了在术语层面出现歧义的情况。有些词汇根据不同的使用场景,会有不同的含义。

假设我们正在开发一个基于 Web 的个人金融管理程序(PFM)。该程序可能用于管理银行帐户(Account)、股票、储蓄,未来可能用于追踪预算和开销记录等等。

在这个程序中,领域术语“帐户”可能是指不同的概念。谈论银行的时候,一个“帐户”在逻辑上其实是“金钱的容器”;于是我们自然而然地为相应的类加上“余额”、“帐户号”等属性。但是,在“Web 应用程序”的上下文中,术语“帐户”会有非常不同的含义,它和用户的认证、可信度有关。如图 2 所示,相应的模型将是完全不同的。

图 2. 一个出现歧义的简单场景:当术语“帐户”应用在不同的上下文时,它的含义可以是完全不同的。

这应该是我们在对应用程序建模的时候,所遇到的最简单的歧义场景了:一个术语,有两个与上下文相关的含义。这个问题的解决办法通常是在类的全名(类名或者包名)前面加一些前缀,以此来划分名字空间。但是在概念层面上,必须清楚我们在和两个上下文打交道,有时不同上下文之间的区别很大,足以防止开发人员犯错误,但有时这样的区别却非常微妙。

不过,在类名层面上解决问题的方式并不能用在所有的情况下:在银行的领域里,术语“银行帐户”或许已经存在了,但却有不同的含义;或者领域专家坚持使用“帐户”作为术语。此时切记不要发明一个特殊的两头这种的术语,或者在专家术语和代码之间引入一个“翻译层”。否则你将不得不面对两个独立的上下文。

绘制第一份上下文图

当歧义真的到来的时候,我们需要一种工具来让开发团队明白,应用程序中正存在着两个不同的上下文。“歧义”是通用语言的头号大敌,我们必须铲除它。消除歧义的最好办法就是在上下文图中,将领域结构分拆在多个界限上下文中。

图 3. 包含两个领域上下文的上下文图

按照领域驱动设计一书的描述,上下文图是用于将上下文边界变得更清晰的重要工具。其基本的想法是在白板上画出上下文的边界,然后选择性地将相关类的领域术语填写在上面。它不是一幅绘制精美的UML 图:它是一个可用的工具,允许我们描绘那种模糊不清的状况,因此它自身看上去模糊不清也就不足为其了。

示例2:概念相同,用法不同

还有一种更加令人困惑的情况,就是底层的概念相同,但是使用的方式不同,最终导致了不同的模型。银行帐户的模型是一个BankingAccount 类,如图4 所示。

图4. 精简版本的BankingAccount 类

通常,有些PFM 应用也允许我们管理支付行为,并且持有一个收款人(Payee)注册器。在这个场景中,收款人可能与一个或者多个银行帐户关联,但是对于收款人来说,我们既不能获取其银行帐户的内部情况,也不能在该帐户上触发任何操作。那么将“收款人帐户”与刚刚定义的BookingAccount 类关联在一起是否正确呢?

图5. Payee 类与BankingAccount 类

恩…这听上去有些道理:毕竟它们都是相同的概念,在现实世界中,我们的帐户和收款人的帐户甚至会处在同一个物理上的银行里。另一方面,这样做似乎又不完全正确:因为我们不允许调用收款人银行帐户的任何操作,也不能追踪他们的任何信息。更糟的是,这样做了后,可能会在我们的程序中埋下一个概念的错误。

我们应该如何做?我们应该再一次回到应用程序的两个不同的上下文里去:这一次我们可以采取两种不同的方式对同一个领域概念进行建模,因为对领域概念的两种使用场景明显不同,每一种都需要一个不同的模型。BankingAccount 类仍然允许我们执行(或者跟踪)特定的操作(比如存款与取款),同时另一个独立的PayeeAccount 类可能有一些和BankingAccount 相同的通用数据(比如accountNumber),但是有一个简化的模型和完全不同的行为(比如我们不能访问收款人的余额信息)。图6 所示的正是这种场景:尽管“银行帐户”有着清晰的含义,其底层概念也是惟一的,但是在应用程序中却以不同的方式被使用着。

图6. BankingAccount 和PayeeAccount 类

这看着似乎挺明显的,其实不然。当你设计类图,或者使用UML 建模工具时,你可能很自然地让收款人具有一个bankingAccount 属性,而且会庆幸“我刚好有一个这样的类”。 Pavlovian 试图去除代码中的重复,有时,它的作用会弊大于利。

如图 7 所示的上下文图,可以用于表述上面讨论的示例。注意,只要我们关于环境的知识在增加,就将它反映在图中。在这个例子中,我们将 PFM 的应用上下文分成了“银行”和“开销跟踪”。

图 7. 非常简单的上下文图:画上了领域模型区域间的轮廓,可以看出在这些区域内保证了概念的完整性

在这个例子中,两个上下文拥有一些逻辑上重叠的区域,即“银行帐户”的概念,它在应用程序的不用区域中,使用方式也不同,这意味着我们要使用不同的模型。但是两个模型又可能有非常紧密的交互。上下文图除了能帮助我们保证模型的概念在不同上下文边界内的完整性,它还能帮助我们关注在不同上下文之间出现的情况。在这个例子中,假设同一个团队正在两个上下文上同时工作,我们就需要让团队中的每位成员的明确两个上下文的区别,并且就两个上下文中出现的术语和概念,分享同一个转换的映射关系。

示例 3:外部系统

再来考虑一下 PFM。很多这种应用程序都需要与某些金融在线服务进行数据交换。在这个例子中,银行会为家庭银行服务提供实时的访问。其他的例子还包括允许用户下载通用标准格式(比如 Money 或者 Quicken 格式)的银行对帐单。但是从上下文图的视角来看,无论是交互活动还是通讯的方向(单向或是双向),并不重要。有一件事是要关注的,我们有了不同的模型。图 8 展示了 PFM 与在线银行应用程序的交互行为。

图 8. 在上下文图中,与外部应用的交互行为很自然地需要独立的界限上下文

即使设计两个模型之初是让它们展现相同的数据(至少在一定程度上),但随着时间的推移,它们还是会受到不同因素的影响,而且它们也会用于不同的目的。因此,分离上下文边界是必须的。如果假设用户档案(User profiling)模块是由第三方库实现的,那么示例 1 也能够归入到这一类中。

管理多个上下文

当应用程序跨越了多个上下文后,我们必须管理上下文之间的关联。不同的界限上下文之间的关系,通常是我们深入观察项目的线索。

有一件事情非常关键,即两个上下文之间的联系是有方向的。DDD 用两个专门的术语表述它们:“上游(upstream)”和“下游(downstream)”,一个上游上下文会影响到相应的下游上下文,但是反过来就不一定了。这不仅体现在代码上(一个库依赖于另一个),还体现在技术含义较少的因素上,比如进度、对外部请求的响应,比如,当在线银行服务更改了 API 或者其他什么原因,我们的 PFM 银行应用程序都必须要快速地更新。所以我们的 PFM 上下文应该是下游的,而在线银行服务很明显就是上游的了。图 9 演示了这两种领域上下文的关系。

图 9. 分离的上下文之间的 Upstream-downstream 关系

如果外部系统发生变化,我们可以接受这种变化,来更新与外部系统通讯的方式。不过我们仍然需要一些保护措施隔离来自上游上下文的变化,保证我们自己的“银行”的上下文的概念完整性。DDD 包含了几种组织模式,帮助我们描述和管理不同的上下文交互方式。最适合我们在这里使用的是模式叫做反腐化层(Anti-Corruption Layer,ACL),它会在代码层面上实现显式的转换,转换可以在两个上下文之间,或者在“银行”上下文的外部边界上完成。这不仅局限于技术上的转换,比如 Java 转化为 XML,同时也是一个很合适的机会,能够管理各个模型之间的所有微妙的不同。如下面的图 10 所示,我们在上下文图上添加了 ACL。

图 10. PFM 程序边界上的反腐化层,防止在线银行服务的变化影响到我们的边界上下文

很明显,一个外部系统需要一个独立的上下文。然而对于一个已有的遗留组件,通常也伴有一个非常难以修改的模型。尽管遗留组件是在我们组织内部来维护的,甚至这个模型也会受到不同因素的影响,它会被其他的上下文所使用。如果必须和遗留系统进行交付,不同模型之间的转换应该放在一个不同的界限上下文里。

上下文图中还有其他的关系吗?我们能够根据相关的 DDD 模式对它们进行分类吗?如果假设开发活动是在单一的团队内进行的,那这里的模式就不会引起太多的关注。但是,如果“银行”和“开销”是由不同的团队来维护的话,团队之间应该是一种合作关系:他们的开发会朝向一个共同的目标(这里谈论上游和下游没有意义,因为他们处于同一级别)。如果 Web 用户档案模块来自于外部,我们将会作为下游的上下文。

图 11. 加入了关系模式后的上下文图

示例 4:向组织扩展

到目前为止,我们只考虑了包含一个开发团队的简单场景。在这种场景下,我们可以忽略沟通的开销,假设团队中的每位开发者都很明确“模型将会如何发展”(也许有些乐观)。更复杂的场景中还可能包含下面的影响因素:

  • 领域复杂度(需要很多不同的领域专家)
  • 组织复杂度
  • 项目跨时很长
  • 项目需要大量的人天
  • 涉及到很多外部的、独立的或者遗留的系统
  • 大型团队,多个开发组
  • 分布的、离岸的团队
  • 个人因素

每个因素都会影响开发团队和组织的通讯方式,并最终影响到要交付的软件。

每个独立的团队,尤其是一个处在敏捷环境的团队,团队内的成员间有很多共享信息的方式:面对面的交谈,多人参与的设计讨论、结对编程、会议、信息散播装置(information radiator)等等。但问题在于,当团队规模、人数增加后,这些技术很难再继续使用了,跨团队地共享模型的概念完整性也非常困难。

毕竟,能够对模型保持统一看法,是沟通中相当成熟的方式,这涉及到对问题具有一致的理解,以及对可行解决方案大致相似的看法。在那些沟通不顺畅的场景下,“埋头干”很容易取代了“识别和确认”。这种沟通瓶颈带来的典型后果就是在同一个代码库中的不同地方散布着不同的类,它们做着基本上同样的事情。

总有一天 PFM 应用会变得更大,这样就要有另一个团队(团队 B)和我们一起工作(显然我们是团队 A),他们开发一个名为“交易”的新模块。团队 B 可能在一个不同的房间、建筑物、城市、公司里,他们全心投入到新模块的开发工作上。如下图所示,团队 A 与团队 B 共享了一些代码,虽然他们很可能会使用彼此独立的代码。最后,团队 B 会写一些类(比如图 12 所示的 A’)来实现自己所需的功能,不过这些功能已经存在于类 A 了。

图 12. 当不同的团队访问相同的代码库时,他们会去关心模型上的不同部分。物理上的团队分割会令信息共享的效果大打折扣

这是重复代码,万恶之源啊!在一个独立的、良好定义的、有界的上下文内,这是毋庸置疑的。但是由于某些原因,这种现象几乎会出现在所有复杂的项目中。这通常是个信号,告诉我们在项目的同一个区域内,或许存在没有恰当隔离的上下文。不过在有些时候,使用两个独立的上下文是组织领域模型更加有效的手段,而不会强迫两个不同的团队不断地去整合他们的模型。

那么,我们如何在图上画出这些呢?上下文图反映了当前我们对整个系统的理解水平。一旦我们学到了更多东西,或者环境发生了改变,还会马上更新它。现在,我们还不能准确地预知接下来会发生什么,所以这就是“我们当前的理解水平”。

图 13. 尚未很好划分的“交易”上下文,它还需要进一步探索或更切合实际的设计决策

图中的危险警告符告诉我们那里有些问题:两个上下文有局部的重叠,它们的关系还不是非常清晰。这可能是需要解决的第一类问题,可以尝试着在上下文内设置一个被广泛认可的、合理的关系,比如消费者 - 供应者、持续集成或者共享内核(Shared Kernel)。不过,这是明天的工作。上下文图是为今天准备的工具,而问题在今天还存在着,所以我们还把警告符号留在图中。

不要被图中的颜色和阴影搞迷惑了。我不过是想让上下文图的打印效果更好一些。一个真实的上下文应该是很乱的,起码和你的项目一样乱。不过这个警告符提醒我们这里有一个危险区域,此处的上下文尚未被清晰地分离,事态很容易朝着“一团大泥球”发展(最有弹性的DDD 组织模式),除非我们采取行动。

一种非传统观点的视角

上下文图迫使我们将非软件的因素也包含在整体考虑中,这样我们更能识别出一些污点热区,而这些热区在传统架构分析的观点中是“不在范围内”的。

比如,组织内部的信息流通方式会在很大程度上影响最终的软件。通常,在小型组织中,组件自身的用途是定义上下文边界的主要因素,而在大型组织中,这个关键因素变成了沟通效率和项目组织方式。像Wiki、email 或即时消息软件会给我们一种假象——团队中每位成员的知识都不断地保持着同步。但是我们都知道这只是个梦想罢了:在一个典型的大型项目中,我们不是Borg 人(译注:源自《星际旅行》中的外星生物,所有Borg 人的思想是互联的,可以完全共享知识)那样的智能联合体,很多人对于自己团队以外的情况知之甚少。

在大型组织中定义上下文边界是一项颇具挑战的任务,但回报却也相当丰厚。很多时候,各个团队并不清楚多个模型存在的事实;之所以概念的完整性会频遭破坏,是因为只有很少人或者没有人看到完整的图景。绘制上下文图是一个不断探索的过程,很多部分的内容在首次尝试时都是不正确的,边界在初期也是很模糊的,还需要很长的路要走,才能获得一个更清晰的完整图景。

图14. 上下文图的最新版本。不要指望它是“最终”的,我们总是会学到一些新的东西。

涉及到的上下文还可能更多,比如“交易”模块可能需要链接到一些在线股票价格服务,但这是交易模块的事!这个上下文图是关于我们(团队A)的,我们的工作内容是“银行”和“开销跟踪”模块:我们只对直接关联的、会影响到自身软件的那些上下文感兴趣。

一旦我们收集到更多的信息,上下文图就会变得更加清晰。正如前面提到的,只要认识到应用程序中存在着各种不同的模型,而且这些模型的完整性可以在一个良好定义的有界上下文中得以保存,这会为我们的领域建模的视角提供诸多益处。很多模型都在成长的过程中逐渐失去完整性,上下文图会在这个方面给予我们很多帮助。

谈谈策略性DDD 模式

此处我们使用模式的方式略有不同:尽管定义是一样的——为一类反复出现的问题提供解决方案——但这些模式很少能展现出可供我们选择的解决方案。更可能的场景是,组织架构会决定模式,我们惟一的希望就是在事态走到死胡同以前识别出它们。有些时候我们有机会选择最好的选项,或者改变现有的状况,但是我们必须清楚的是,在组织级别的改变所需的时间可能已经远远超过了项目持续的时间,或者这个改变根本就是不可能的。

如果你还在犹豫应该从那里开始,那么就从开发团队开始吧。对于有效地共享模型信息来说,一个团队应该是最大的组织单元。当识别出多个上下文后,可以由一个团队管理它们,这样很大程度上将问题归结为架构的抉择。

每一种模式都有不同的开销:即使它们解决的是类似的问题(相近的上下文),也不能简单地交换。比如,反腐化层会在代码层面(一个额外层)上留下痕迹,并在组织里留下很小的痕迹。尽管Partnership 或者“客户- 供应者”模式可能需要更少的代码和一个单独的代码库,但是如果没有有效的沟通渠道和良好的过程的话,也很难应用起来。企图在没有合作的环境下安排执行Partnership 模式,无异于自寻死路。

结论

让我们在回到“上下文”最初的定义上来——“一个单词或一个句子所出现的环境,这个环境会反过来影响它们的含义”——它非常准确,而且可以应用在设计层面、架构层面乃至组织层面上,却没有损失其准确性和有效性。尽管有些“对统一性的期望”是合情合理的,但是模型不能被无限地扩张。界限上下文提供了一种非常安全的机制,它允许模型在其内部不断变得复杂,同时又不牺牲概念的完整性。

当把上下文图应用到大型的项目上后,它还可以显示出当前组织内的隐式边界,并提供一个来自第一手的、没有PS 过的项目境况的快照。一个好的上下文图能让你看到所面对的不利条件的大致状况。可能你已经知道——但通常都是不知道——组织是否在扮演项目成功的绊脚石,即使项目还没有开始。

作为一名顾问,我发现上下文图能够奇迹般地让我快速获取客户项目的细节。上下文图还充当了策略决策的支持工具(毕竟,这是“图”的本意)。上下文图提供了系统的全局视图,帮助我们关注在选择那些能在你的环境中真正可行的方案,而不是把钱浪费在对系统不切实际的构想中,这是UML 或者架构图所做不到的。

关于作者

Alberto 是来自 Avanscoperta 的一名咨询顾问和培训师。他致力于帮助客户管理软件开发的复杂度。他坚信只有那种全盘考虑的软件开发方法才能开发出有用的软件。

查看引文原文 Strategic Domain Driven Design with Context Mapping


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010-04-06 09:2710508
用户头像

发布了 53 篇内容, 共 13.4 次阅读, 收获喜欢 2 次。

关注

评论

发布
暂无评论
发现更多内容

数字货币交易系统应用开发,区块链交易所app

13530558032

收藏!一篇教会你写90%的shell脚本!

Geek Tech

Shell shell脚本编写 收藏教程

云计算、人工智能、大数据技术三者之间的关系

cristal

人工智能 云计算 大数据

Redis问的太深入,面试官说:你先回去等通知吧

Java redis 编程 程序员 架构师

架构师训练营十二周作业

方堃

架构师培训十二周练习

小蚂蚁

云小课 | 一份超实用的勒索病毒自救预防指南

华为云开发者联盟

勒索病毒 弱密码 云小课 企业主机安全 病毒云查杀

架构师训练营第十二周总结

Hanson

GitHub上120K Stars国内第一的Java多线程PDF到底有什么魅力?

Java 程序员 并发编程 多线程 架构师

膜拜!京东T9大牛沉淀三年终于整理出了这份架构核心修炼之道

Java 编程 程序员 架构师 计算机

LeetCode题解:155. 最小栈,使用链表代替栈,JavaScript,详细注释

Lee Chen

大前端 LeetCode

架构师训练营第十二周作业

Hanson

交易所合约跟单系统源码开发,合约跟单平台搭建

13530558032

京东T9今年首发的一份Spring Boot实战,让开发像搭积木一样简单

Java 编程 程序员 架构师 计算机

如何实现特定列脱敏?这两种方法你都要会

华为云开发者联盟

postgresql 数据 脱敏 匿名 视图

除了方文山,用TA你也能帮周杰伦写歌词了

华为云开发者联盟

AI 数据 周杰伦 modelarts 歌词

架构师训练营第 0 期第 12 周作业

无名氏

真香警告!手绘172张图解HTTP协议+703页TCP/IP协议笔记

Java 程序员 架构师 计算机

Github下载即将破百万的PDF:双十一高并发亿级流量秒杀顶级教程

Java 编程 程序员 秒杀 计算机

2. Bean Validation声明式校验方法的参数、返回值

YourBatman

参数校验 Hibernate-Validator Bean Validation 方法校验

加密数字货币钱包APP系统开发,数字货币钱包系统定制

13530558032

全网都在跪求的阿里Java修炼开发技术笔记,终于开放下载了

Java 编程 后端 架构师

疫情对在线教育的影响

anyRTC开发者

在线教育 直播 RTC 安卓

易观郭炜:流动水系数造未来

易观大数据

区块链USDT支付系统,USDT承兑支付软件开发

13530558032

使用 Next.js , Nexus, Prisma 构建全栈项目

夏木

nextjs prisma graphql fullstack

GitHub上的今年第一本《Java异步编程实战》美团T9亲荐,太赞了

Java 程序员 架构师 异步编程

TCP/IP协议族(第四版)已出,不愧是世界计算机优秀畅销精选书籍

Java 编程 架构师 TCP/IP 协议族

架构师训练营第十二周作业

叮叮董董

架构师培训 -12 hadoop

刘敏

一文说透"静态代理"与"动态代理"

Geek Tech

源码分析 动态代理 静态代理

基于上下文图的策略性领域驱动开发_架构_Alberto Brandolini_InfoQ精选文章