DDD中如何破解上下文划分难点?


领域驱动设计(DDD)通过将精心设计的领域模型整合到软件系统中,为解决复杂业务问题提供了有价值的框架。其中,有界上下文(BC:限界上下文、有边界的上下文)的概念至关重要,它们是针对特定用户或业务挑战而定制的模型,使用共享的通用语言。然而,如何定义一个“好”的设计却是一个具有挑战性的问题。

这篇文章介绍了Barry O'Reilly的残差Residuality理论,它提供了一种实用和科学的方法来设计有界上下文。可以在面对未知的未来变化时提供适应性和弹性的解决方案。

为什么要设计限界上下文BC?
您是否遇到过这样的情况:一位领域专家向您解释他们的需求,希望在当前的软件系统中解决一个复杂的业务问题。

  • 当您深入研究代码时,却发现很难将他们所说的大部分内容与实际代码联系起来。
  • 你可以找出一些名词,它们与其他名词相关,似乎是现实的抽象。但是这些对象执行的功能往往与领域专家讨论的业务问题脱节。
  • 与业务问题相关的逻辑已经是系统的一部分,要理解这一切是如何组合在一起的,却仍然是个难题。
  • 改变代码的一部分,可能会与其他团队或不同的相关业务问题产生交集,从而降低代码的自主性。

"如果设计或设计的某些核心部分没有映射到领域模型,那么该模型就没有什么价值,软件的正确性也会受到怀疑"。

因此,埃里克-埃文斯提出了领域驱动设计(DDD)的核心概念--有边界的上下文模式BC。

就其核心而言,有边界的上下文是一种为解决特定用户/业务问题而设计的模型,它以通用语言为边界:

  • 旨在解决特定问题的模型 ,这意味着我们要设计一个模型,以处理独特的业务或用户挑战所固有的复杂性。
  • 通过囊括某一领域的各种元素,模型可以简化设计过程并增强对系统的理解。

无处不在的统一专业术语语言UL:"指的是一种专为模型设计的一致而清晰的语言。这种语言形成了一个领域模型的语言、术语和规则保持一致的范畴。重要的是,这种语言是与领域专家合作设计的。概念和行话通常来自领域专家使用的术语,但将通过合作进行清理和改进,以获得更清晰、更狭义的定义。

有界上下文的主要目的是隔离模型的复杂性,避免不同模型交互时可能出现的混乱或不一致。需要注意的关键一点是,如果您的领域专家没有意识到,或者您没有合作设计有界上下文、其模型和语言,那么它就不是真正的有界上下文,也不符合领域驱动设计(DDD)。

有边界的上下文BC是一种语言边界和协作模式。

订餐案例
在这篇文章中,我将以在线食品配送行业为例,让我们考虑一下我们业务的核心价值流,在这个价值流中:

  • 订餐者下订单,餐厅厨师创建订单并将其发送给送餐司机,由送餐司机送餐。
  • 当订餐者下订单时,我们可能会在一个有边界的上下文中进行处理,标题可能是 "订餐者外卖订单Orderer Take-away orders"。
  • 在这种情况下(在这种上下文中),我们要满足订餐者的需求,为他们找到并制作最合适的订单。

现在,当我们考虑为餐厅厨师创建订单的旅程时,会发生新的上下文:

  • 我们可能会在另外一个单独的有界上下文中进行处理,标题可能是 ""菜肴交付订单Dish Delivery orders"。
  • 在这个上下文中,我们需要满足厨师的需求,让他知道我们是否能准备好该订单。

注意到围绕”订餐者“和”厨师“有两种不同上下文:

  1. 针对订餐者:订餐者外卖订单Orderer Take-away orders
  2. 针对厨师:菜肴交付订单Dish Delivery orders

这两种上下文都可能使用术语'订单'。但是,该术语在不同语境上下文中的含义有所不同。

  • 在 ""订餐者外卖订单 "中,"订单 "的职责是提供有关他们所下订单的信息,以及他们是否可以根据业务规则或限制取消订单。
  • 在 "菜肴交付订单 "中,"订单 "的职责与餐厅厨师正在准备的订单有关,包括他们需要制作哪些产品,以及顾客对该订单可能有哪些饮食限制。

下面的上下文图进一步说明了这种设计:

我们的上下文映射中现在有两个有界上下文。这两个有界上下文是相互连接的。在这种情况下,"订餐者外卖订单Orderer Take-away orders“需要”"菜肴交付订单Dish Delivery orders“来确保订单确实正在准备中。上下文映射非常适合评估或让我们概述这些有界上下文如何相互作用。

为什么设计限界上下文很难?
到目前为止,我已经提出了一个相当集中的上下文映射,仅包含两个有界上下文。

然而,在实际的在线食品配送业务等组织中,存在许多业务和用户挑战,这些挑战超出了构建 "菜肴外卖订单 "有界上下文的团队可能意识到的范围。这些额外的挑战可能会影响有界上下文的设计。

请记住,有界上下文本质上是一个模型,是对现实的简化和抽象呈现,其特定目的是:解决我们的问题。对于许多业务问题,尤其是越复杂的问题,有多种方法可以对解决方案进行建模,每种方法都有自己的权衡取舍。

领域建模人员通常会采用一系列协作建模练习,如业务模型画布、沃德利映射、领域故事讲述、事件风暴、示例映射、领域信息流建模和上下文映射。通过这些方法,我们可以从不同角度看待问题并对其进行建模,在现有理解的基础上逐步发现、提炼和设计有边界的上下文。

问题越复杂,我们与利益相关者的合作就越多,以了解他们所面临的挑战,从而形成我们的设计。有关这方面的更多信息,请参阅我与吉恩和伊夫林合著的《协作式软件设计Collaborative Software Design》一书。

真正的挑战出现在处理复杂问题时,这些问题存在于一个系统中,其因果关系只能在事后而非事前确定。

这类问题需要通过探索和实验来发现规律和解决方案,而不是依靠预先确定的最佳实践或分析。

这意味着我们在设计时要面对未知因素。

因此,我们广泛使用协作建模练习来探索我们的有界上下文设计。这也强调了为什么 DDD 在解决复杂问题时更有价值。
为了更好地理解复杂性,我建议阅读 Cynefin 框架。Liz Keigh的博客 "Cynefin for Everyone "是IT行业人士的一个很好的起点。

领域建模人员如何处理复杂问题?
在为复杂问题设计上下文BC时,领域建模者会利用他们收集的隐式或显式设计启发式方法。
顾名思义,启发法是指导问题解决过程的似是而非的辅助工具或方向,但它们最终仍然是没有道理的,不可能完全验证,而且容易出错。
当我们处理复杂问题时,这些启发式方法就变得至关重要,因为如前所述,因果关系只能在事后才能发现

启发法(类比、比喻形象思维)不是因果思维
我们都会使用启发式方法(即使我们没有向他人阐明)来发现、理解、探索、创建、修改或扩展复杂的软件系统。

我们都使用启发法(即使我们没有向其他人阐明它们)来发现、理解、探索、创建、修改或扩展复杂的软件系统。
Billy Vaughn Koen 在《方法讨论:引导工程师解决问题的方法》中将启发式定义为:任何在解决问题时提供合理帮助或方向但最终分析不合理、无法证明其合理性的事物,并且可能会犯错。

值得注意的是,我们都在使用这些启发法,甚至是下意识的。熟练的领域建模者不仅能够识别他们个人使用的启发式方法,而且还可以从他们正在设计的团队中提取启发式方法。

分离和完善这些启发法以做出更明智的决策非常重要。

领域驱动设计 (DDD) 的常见做法是设计至少三个不同的有界上下文模型。使用启发式方法,我们可以进行权衡分析,做出明智的决策,并将这些决策记录在架构决策记录中。我们即将出版的书提供了有关此过程的更详细的指南。

然而,这里存在一个固有的问题:我们经常过度依赖领域建模者的经验、直觉和团队可用的启发法。如果我们真的对自己诚实,这种方法并不是特别科学,不是吗?

残差理论如何影响领域建模?
事实上,巴里·M·奥莱利(Barry M. O'Reilly)也面临着同样的问题。我参加了为期三天的高级软件架构课程,他在课程中介绍了残差的概念。巴里的任务是培训初级软件架构师,让他们像他一样学习软件架构。

但是,他发现自己在问:“我究竟该如何处理软件架构?” 他与其他高级软件架构师讨论了这个问题,结果发现他们很大程度上依赖于直觉。

尽管他们正在设计能够承受组织复杂性的弹性软件架构,但没有人能够清楚地说明他们是如何实现这一目标的,更不用说提供科学证据了。这促使巴里进行了博士研究并发展了残差理论。

残差理论到底是什么?
这是一个广泛而复杂的理论;我三天的培训只是对其应用的介绍。我不会在这篇博文中深入探讨它,但我可以通过这个虚拟领域驱动设计记录推荐一个全面的介绍。

这篇文章的要点是,残差理论通过理解对压力的敏感性和残差行为的概念,为设计具有弹性和反脆弱行为的软件系统提供了基础。

该理论的一个基本思想是超极限 ,即无序系统中的有序系统。

  • 有序系统是可预测、可映射和可检验的。
  • 无序系统是动态的、不断增长的和不可预测的。
  •  

架构师不得不在这两个世界之间不断转换,有序的软件和无序的组织环境需要完全不同的工具和认识论来理解。

我们的软件系统是有序系统,本质上是可预测、可映射和可测试的。它在我们的组织中运行,而我们的组织是一个无序的系统。我们无法预测该组织及其所处市场会发生什么。

当我们设计一个有边界的上下文环境时,我们不得不为一个未知的未来而努力。如下图所示,在这个未知的未来中出现的任何事件都被称为压力源:

首先想到的是,我们经常与领域专家组织协作建模会议,以发现任何未知因素。

示例映射、领域故事讲述和事件风暴等技术都是发现这些压力因素的绝佳方法。

然而,这些方法通常都侧重于手头的问题,这可能会因功能固定性和框架偏差而限制我们的思维。

  • 功能固定性是一种限制我们只思考熟悉概念的偏见。
  • 而框架偏见则是指信息的呈现方式会如何影响我们的观点。

巴里认为,在这些环节中,概率会极大地影响我们的决定。
这意味着,如果我们认为某个问题不太可能发生,我们可能会决定不将其纳入讨论。

我们的设计可能会因为认知偏差和对某些事件发生概率的判断而失去弹性。重要的是要记住,我们无法预测复杂系统中的一切。通过考虑和讨论我们通常可能会忽略的各种意外问题,我们可以增加可能性的范围,从而提高设计的弹性。

这些压力因素都会产生一种叫做吸引子的东西,即网络状态空间中系统会反复返回的有限数量的状态。关于吸引子还有更多内容,考夫曼网络已于 1964 年证明了这一点。

可以在巴里的文章《残差理论、随机模拟和吸引子网络》中阅读到相关内容。

即使是一个看似无关紧要的压力源,我们可能会因为其可能性较低而不予考虑,但它最终可能会以与我们没有预见到的压力源相同的方式对系统产生影响,但这是很有可能发生的。

因此,考虑这种 "不可能 "的压力因素至关重要。它有助于我们设计出更有弹性的软件,以适应突发事件。系统在不可预见的情况下重塑自身的能力就是我们所说的 "临界度"。临界度可以通过观察软件中组件的数量以及这些组件之间的连接数量来量化。我们将在今后的博文中进一步深入探讨这一主题。目前,了解超临界压力的潜在影响是我们的主要收获。

残差的协作建模压力源
我们已经谈到了领域驱动设计在软件架构中的重要性,以及残差性在构建弹性软件系统中的重要作用。但是,我们如何将残差理论嵌入领域驱动设计方法中呢?

在超极限中,有几个前提条件。始终存在当前状态。然后,我们知道未来会因压力源而发生变化,从而将当前状态转变为新状态。这种新状态被称为 "残差"。

通过压力源分析,我们可以模拟各种随机压力源,观察它们产生的模式。这有助于我们预测这些压力源可能产生的 "残差 "或新状态。

在我们的方法中,首先要确定的是系统的当前状态。我们可能已经以 C4 模型或上下文图的形式掌握了部分信息。如果没有,我们可以从协作建模会议开始,使用咬合式架构,或从 "大图片事件风暴 "中提炼上下文图。

不过,这些模型和地图通常主要关注软件系统。我们还需要了解超临界中更大的无序系统,即我们能够识别的与我们的软件相互作用的所有元素。

沃德利制图公司(Wardley Mapping)所做的价值链制图就是一个有用的工具。

这种方法需要了解我们的业务。如果缺乏对业务的了解,您可能需要与利益相关者一起创建一个业务模型画布,或者如果您能将很多利益相关者聚集在一起,则可以开展一次大图片事件风暴会议。

在了解了更广泛的背景上下文之后,下一步就是从用户的角度出发,将他们的需求与你的软件架构联系起来。团队拓扑 Team Topology 的人员在他们的 "用户需求映射 "方法中对此进行了解释。不过,重要的是要记住,这些例子都是简单化的描述。实际上,在进行这些会话时,你可能会面临更多的混乱和复杂情况。


在这种情况下,有界上下文被建模为部署在软件系统中的组件。

然而,这些软件系统也可以表示为价值链中的组件。

软件架构的可视化方式在很大程度上取决于您的偏好和您的建模目标,尤其是在上下文地图已经到位的情况下。

了解现状为根据新需求设计软件系统奠定了基础:
值得注意的是,使用残差理论并不一定要等到有具体的变革需求时,它也可以应用于现有系统。当有明确的业务需求时,软件设计往往会变得更加简单明了,这不仅能激发设计,还能确保实际实施。不过,应用残差性理论可以揭示当前设计中的弱点,从而促使对业务战略做出必要的改变。(过度设计?)

利用压力源分析设计有界上下文
现在,我们可以对原先”考虑不周“的软件设计进行协作压力源分析。

残差理论的这一工具旨在识别压力源,了解压力源对架构的影响,确定检测压力源的方法,并提出缓解压力源的方法。

这有两种方法:

  1. 一种是集思广益,想出尽可能多的潜在压力源,然后开始定义影响、如何观察其发生以及每个压力源的缓解措施。
  2. 或者,我们可以从单一的压力源入手,确定影响、如何观察压力源的发生以及缓解措施,然后让更多的压力源有机地出现。

就我个人而言,我更喜欢前一种方法,即首先确定所有潜在的压力源,然后逐一进行评估。重要的是要记住,当你深入分析每个压力源时,新的压力源可能会浮出水面。另一个重要方面是,如果你不能想到很多压力源,那么你所面对的可能就不是一个复杂的系统。

让我们举一个与食品配送相关的例子:假设有人用所有库存产品下了一个订单,然后对订单进行了下单配送。这种情况会影响 Dish 外卖订单、Dish 产品库存、Dish 申请配送和配送服务。我们可以通过 Dish Dash 订单页面检测到这一趋势。

潜在的缓解策略可能是对创建的订单进行限制

寻找缓解措施:

  • 绘制压力地图,找出缓解压力源的模式。是否有反复出现的缓解方法?这些模式很可能对未来不可预见的压力源有效。实施这些模式可以使你的架构具有更强的弹性和反脆弱行为。
  • 接下来,我们可以根据这些压力源及其缓解措施来设计或完善有界上下文。

正如本博客之前提到的,有界上下文是一种模型--一种对现实的简化、抽象表示,旨在解决特定问题。压力源及其缓解措施为此奠定了基础。

在绘制压力地图中,如果缓解措施反复出现:一个可行的设计方案可能是专门为更智能的交付创建一个有边界的上下文。

目前,如用户需求图所示,Dish Dash 订单页面通过 Dish Request Delivery 系统发起送货请求。Dish Request Delivery 已经成为一个有边界的上下文,主要用于促进与配送服务的沟通。虽然在我们的地图上并不显眼,但此类系统有多个实例。

鉴于目的和用户需求的相似性--增强智能配送和简化与配送服务的沟通--我们有机会设计两个独立但相关的绑定上下文:

  • 我们可以将缓解措施直接集成到现有的 "Dish Request Delivery 菜肴请求递送 "有界上下文中,
  • 或者设计一个新的有边界的上下文,专门用于更智能的递送。

在现阶段,一种方案优于另一种方案的情况仍未确定;不过,很明显,这两种方案都是残差方案。


此外,我们的分析凸显了一个反复出现的模式:两种不同的缓解措施(例如”缓解措施二“和”缓解措施三“)经常同时出现。这一观察结果可能表明,创建一个同时解决”缓解措施二“和”缓解措施三“的单一约束上下文是可行的。

这种反复出现的缓解措施组合也为重新评估和可能改变当前子域提供了绝佳的机会。

虽然我打算在以后的文章中深入探讨有界上下文和子域之间的区别,但在这一点上,理解子域本质上代表一个知识领域是非常有用的。这种理解有助于围绕特定的专业领域有效地组织团队

压力分析也可能揭示出拆分单体的必要性,尤其是当我们注意到某些压力影响到单个应用程序中的两个有限制的上下文时。透彻的分析可能会发现许多这样的模式和潜在的有界上下文,并对其进行提炼和设计。但是,这并不意味着我们要立即开始实施所有这些缓解措施。我们应首先使用指标矩阵分析这些残差,以确定我们的架构是否已达到临界状态。

当一个系统能够在不同的吸引子中生存时,它就具有了临界性--一种能够重组以在其他吸引子中生存的内部结构。达到临界点是残差理论的目标,与 "正确性 "的目标不同,"正确性 "是软件工程的数学和计算机科学根源。

原文点击标题