正如你在前一章中所看到的,为了确保项目的成功,你必须开发通用语言,以便从软件工程师到领域专家等所有利益相关者都能使用这种语言进行交流。这种语言应该反映领域专家对业务领域内部运作和基本原则的心智模型。
由于我们的目标是使用通用语言来驱动软件设计决策,因此语言必须清晰且一致。它应该是没有歧义的,没有隐含的假设,也没有无关的细节。然而,在一个组织范围内,领域专家的心智模型本身可能就是不一致的。不同的领域专家对同一业务领域可以有不同的模型。让我们来看一个例子。
让我们回到第 2 章电话营销公司的例子。该公司的营销(marketing)部门通过在线广告产生营销线索(lead)。其销售(sales)部门负责招揽潜在客户购买其产品或服务,这个链条如图3-1所示。
图 3-1. 业务领域示例:电话营销公司对领域专家语言的研究揭示了一个独特的发现。lead 一词在营销和销售部门有不同的含义:
营销部门
对于营销人员来说,lead(营销线索)代表有人对某种产品感兴趣的通知。收到潜在客户联系方式的事件被视为 lead(营销线索)。
销售部门
在销售部门的上下文中,lead(销售线索)是一个更复杂的实体。它代表了销售过程的整个生命周期。这不仅仅是一个事件,而是一个长期运行的过程。
在这个电话营销公司的案例中,我们如何制定通用语言?
一方面,我们知道通用语言必须保持一致——每个术语都应该仅有一个含义。另一方面,我们知道通用语言必须反映领域专家的心智模型。在这种情况下,“lead” 的心智模型在销售和营销部门的领域专家之间是不一致的。
这种模棱两可在人与人之间的交流中并没有太大的挑战。事实上,来自不同部门的人之间的沟通可能更具挑战性,但人们可以很容易从交互的上下文中推断出确切的含义。
然而,要在软件中表现这样一个有分歧的业务领域模型是比较困难的。源代码不能很好地处理歧义。如果我们把销售部门的复杂 lead 模型引入到营销部门中,就会在不需要的地方引入复杂性——比营销部门人员在优化广告活动时需要的细节和行为多得多。然而,如果我们试图按照营销部门的世界观来简化 lead 模型,它就不符合销售子域的需要,因为它对于管理和优化销售过程来说太简单了。
我们如何解决这个进退两难的状况?
解决这个问题的传统方法是设计一个可以用于各种问题的单一模型。这样的模型会产生跨越整个办公室墙壁的巨大实体关系图 (ERD)。图 3-2 是一个有效的模型吗?
图 3-2. 企业级实体关系图俗话说,“样样都会,样样不精通”。这样的模型试图让自己能适合所有场景,但最终却对什么都没有效果。不管你做什么,你总是面临着复杂性:过滤掉无关细节的复杂性,找到你真正需要的东西的复杂性,最重要的是,让数据保持一致状态的复杂性。
另一个解决方案是在有问题的术语前加上上下文的定义。比如 "营销线索" 和 "销售线索"。这将允许在代码中实现两个模型。然而,这种方法有两个主要的缺点。首先,它引发了认知负荷。每个模型应该在什么时候使用?冲突模型的实现越接近,就越容易出错。其次,模型的实现无法与通用语言保持一致。没有人会在对话中使用前缀。人们不需要这些额外的信息;他们可以依赖对话的上下文。
让我们转向可解决此类场景的领域驱动设计模式:限界上下文模式。
领域驱动设计中的解决方案并不复杂:把通用语言拆分成多个更小的语言,然后把每个语言分配给它可以应用的明确的上下文:它的 限界上下文。
在前面的例子中,我们可以识别出两个限界上下文:营销和销售。如图 3-3 所示,术语 lead 一词存在于两种限界上下文中。每个细粒度的通用语言都具有一致性,并遵循领域专家的心理模型,只要它在每个限界上下文中只具有单一的含义。
图 3-3. 通过将通用语言分割成限界上下文来解决其不一致的问题从某种意义上说,术语冲突和隐含的上下文是任何有规模的业务所固有的一部分。有了限界上下文模式,上下文就被建模为业务领域中明确的和不可或缺的部分。
正如我们在前一章所讨论的,模型不是现实世界的副本,而是帮助我们理解复杂系统的一种构造。模型所要解决的问题是模型的一个固有部分 -- 也是模型的目的。模型不能没有边界而存在;否则它将蔓延成为现实世界的副本。这使得定义模型的边界 -- 也就是它的限界上下文 -- 成为了建模过程固有的部分。
让我们回到地图作为模型的例子。我们看到的每张地图都有其特定的上下文 -- 航空、航海、地形、地铁等等。地图只有在其特定目的范围内才是有用的和一致的。
正如地铁地图对航海导航毫无用处一样,在一个限界上下文中的通用语言可能与另一个限界上下文中的在范围上完全无关。限界上下文定义了通用语言和它所代表的模型的适用性范围。它允许根据不同的问题域定义不同的模型。换句话说,限界上下文是通用语言的一致性边界。通用语言中的术语、原则和业务规则只在其限界上下文中才是一致的。
限界上下文使我们能够完成对通用语言的定义。通用语言的 “通用” 不是 "放之四海而皆准" 的意思,而是它应该在整个组织中 "无处不在" 地使用和应用。通用语言 不是 万能的。
相反,通用语言只在其限界上下文中是是普适的。它只专注于描述限界上下文所包含的模型。正如一个模型如果没有要解决的问题就不可能存在一样,如果没有一个明确适用的上下文,通用语言就无法定义或使用。
本章开头的例子展示了业务领域有其固有边界。不同的领域专家对同一个业务实体会持有相互冲突的心智模型。为了对业务领域进行建模,我们必须对模型进行拆分,并为每个细粒度的模型定义一个严格的适用范围 -- 它的限界上下文。
通用语言的一致性只有助于确定它的最宽边界。边界不能更宽是因为那样就会出现不一致的模型和术语。然而,我们仍然可以将模型进一步分解为更小的限界上下文,如图3-4所示。
图 3-4。更小的限界上下文定义通用语言的范围 -- 它的限界上下文 -- 是一个战略设计决策。边界可以很宽,遵循业务领域的固有上下文,也可以很窄,进一步将业务领域划分为更小的问题域。
限界上下文的大小本身并不是一个决定性因素。模型不应该一定是大的或小的。但必须是有用的。通用语言的边界越宽,就越难保持一致。将大型的通用语言划分为较小的、更容易管理的问题域可能是有益的,但是努力追求小的限界上下文也可能会适得其反。它们越小,设计引起的集成开销就越大。
因此,决定你的限界上下文有多大,应该取决于具体的问题域。有时,使用一个宽的边界会更清晰,而在其他时候,进一步分解会更有意义。
从一个更大的限界上下文中提取更细粒度的上下文,原因包括组成新的软件工程团队或解决系统的一些非功能需求。例如,当你需要分离最初驻留在单个限界上下文中的某些组件的开发生命周期时。抽取某个功能的另一个常见原因是为了使其能够独立于限界上下文中其他的功能而进行扩展。
因此,保持你的模型的有效性,并使限界上下文的大小与你的业务需求和组织约束相一致。需要注意的是,将一个连贯的功能切分成多个限界上下文,这样的分割会阻碍每个上下文独立发展的能力。而且,业务的需求和变化将同时影响到这些限界上下文,并需要在这些上下文中同时部署变更。为了避免这种无效的分解,使用我们在第 1 章中讨论过的寻找子域的经验法则:识别对相同数据进行操作的连贯用例集,并避免将其分解为多个限界上下文。
我们将在第 8 章和第 10 章中进一步讨论不断优化限界上下文的主题。
在第 2 章中,我们了解到一个业务领域由多个子域组成。在本章中,到目前为止,我们探讨了将业务领域分解为一组细粒度问题域或限界上下文的概念。起初,这两种分解业务领域的方法可能看起来是冗余的。然而,事实并非如此。让我们来看看为什么我们需要这两种边界。
要理解公司的业务战略,我们必须分析其业务领域。根据领域驱动设计方法,分析阶段涉及识别不同的子域(核心、支撑和通用)。这也是组织的运作方式和规划竞争战略的方式。
正如你在第 1 章中所了解的,子域类似于一组相互关联的用例。用例由业务领域和系统需求定义。作为软件工程师,我们不定义需求;那是企业的责任。相反,我们分析业务领域以识别子域。
另一方面,限界上下文是被设计出来的。选择模型的边界是一项战略性设计决策。我们决定如何将业务领域划分为更小的、可管理的问题领域。
从理论上讲,虽然不切实际,但一个单一的模型可以跨越整个业务领域。这种策略仅适用于小型系统,如图 3-5 所示。
图 3-5. 单体限界上下文当模型出现冲突时,我们可以按照领域专家的心智模型,将系统分解为限界上下文,如图 3-6 所示。
图 3-6. 由通用语言的一致性驱动的限界上下文如果模型仍然很大且难以维护,我们可以将它们分解为更小的限界上下文;例如,为每个子域设置限界上下文,如图 3-7 所示。
图 3-7. 限界上下文与子域的边界对齐 (勘误:左下角的 Ad campaigns 应为 Contracts)无论哪种方式,这都是一个设计决定。我们将这些边界设计为解决方案的一部分。
在某些情况下,限界上下文和子域之间的一对一关系是完全合理的。然而,在其他情况下,不同的分解策略可能更合适。
关键是要记住,子域是被发现的,而限界上下文是被设计的。1 子域是由商业战略定义的。然而,我们可以针对具体项目的背景和约束条件,设计软件解决方案及其限界上下文。
最后,正如你在第 1 章中所了解的,模型旨在解决特定问题。在某些情况下,使用同一概念的多个模型来解决不同的问题是有益的。由于不同类型的地图提供了关于我们这个星球的不同类型的信息,因此使用同一子域的不同模型来解决不同的问题是合理的。将设计限制在子域与限界上下文之间的一对一关系会抑制这种灵活性,并迫使我们在限界上下文中使用子域的单一模型。
正如 Ruth Malan 所说,架构设计本质上是关于边界的:
架构设计就是系统设计。系统设计就是上下文设计——它本质上是关于边界(什么在里面,什么在外面,什么横跨其间,什么在其中流动)和权衡的。它重塑了外部,一如它塑造了内部。2
限界上下文模式是领域驱动设计的工具,用于定义物理和所有权边界。
限界上下文不仅是模型的边界,也是实现它们的系统的物理边界。每个限界上下文应该作为一个单独的服务/项目来实现,这意味着它的实现、演化和版本控制都独立于其他限界上下文。
限界上下文之间清晰的物理边界使我们能够用最适合其需求的技术栈来实现它们。
正如我们之前所讨论的,限界上下文可以包含多个子域。在这种情况下,限界上下文是一个物理边界,而它的每个子域都是一个逻辑边界。逻辑边界在不同的编程语言中有不同的名称:命名空间、模块或者包。
研究表明,好的篱笆确实能造就好的邻居。在软件项目中,我们可以利用模型边界(限界上下文)来实现团队的和平共处。团队之间的工作分工是另一个可以使用限界上下文模式做出的战略决策。
一个限界上下文应该只由一个团队来实现、演化和维护。两个团队不能在同一个限界上下文上工作。这种隔离消除了团队可能对彼此的模型做出的隐性假设。相反,他们必须明确地定义通信协议以整合他们的模型和系统。
值得注意的是,团队和限界上下文之间的关系是单向的:一个限界上下文应该只由一个团队拥有。然而,如图3-8所示,一个团队可以拥有多个限界上下文。
图 3-8. 团队 1 处理营销和优化限界上下文,而团队 2 处理销售限界上下文在我的一次领域驱动设计课程中,一位学员曾经指出。"你说 DDD 是用来对齐软件设计与业务领域的。但是,现实生活中的限界上下文在哪里?在现实生活的业务领域中并没有限界上下文"。
事实上,现实生活中的限界上下文并不像业务领域和子域那样明显,但它们就在那里,就像领域专家的心智模型一样。你只需要有意识地去理解领域专家对不同业务实体和流程的思考方式。
在本章的最后,我想通过讨论一些例子来证明,不仅当我们在软件中对业务领域进行建模时存在限界上下文,而且在不同背景下使用不同模型这个概念在日常生活中也很普遍。
可以说,领域驱动设计的限界上下文是基于 语义域 的词汇学概念。语义域 被定义为一个意义的区域和用来谈论它的词语。例如,monitor、port 和 processor 这些词在软件语义域和硬件工程语义域中有着不同的含义。
不同语义域的另一个相当有特点的例子是 番茄 这个词的含义。
根据植物学的定义,果实是植物传播其种子的方式。植物的花里应该长出一个果实,并至少结出一颗种子。另一方面,蔬菜是一个泛指,包括植物的所有其他可食用部分:根、茎和叶。根据这个定义,番茄是一种果实(水果)。
然而,这一定义在烹饪上下文中没什么用处。在这种情况下,水果和蔬菜是根据它们的风味特征来定义的。水果的质地柔软,或甜或酸,可以生吃,而蔬菜的质地较硬,味道较淡,通常需要烹饪。根据这个定义,番茄是一种蔬菜。
因此,在 植物学限界上下文 中,番茄是一种水果,而在 烹饪限界上下文 中,它是一种蔬菜。但这还不是全部。
1883年,美国规定对进口蔬菜征收10%的税,但不包括水果。番茄作为水果的植物学定义允许将其进口到美国而无需缴纳进口税。为了弥补这一漏洞,1893年,美国最高法院作出决定,将番茄列为蔬菜。因此,在 税收限界上下文 中,番茄是一种蔬菜。
此外,正如我的朋友 Romeu Moura 所说,在戏剧表演限界上下文中,番茄是一种反馈机制。
正如历史学家 Yuval Noah Harari 所说,“科学家们普遍认为,没有任何理论是 100% 正确的。因此,对知识的真正检验不是真理,而是效用”。换句话说,没有任何科学理论在所有情况下是都正确的。不同的理论在不同的情况下有用。
这个概念可以通过艾萨克·牛顿爵士和阿尔伯特·爱因斯坦所发现的不同引力模型来证明。根据牛顿运动定律,空间和时间是绝对的。它们是物体运动发生的舞台。在爱因斯坦的相对论中,空间和时间不再是绝对的,而是对于不同的观察者来说是不同的。
尽管这两个模型可以被看作是矛盾的,但两者在其合适的(限界)上下文中都很有用。
最后,让我们来看一个更接地气的现实生活中的限界上下文的例子。你在图3-9中看到了什么?
图 3-9. 一块硬纸板这只是一块硬纸板吗?不,它是一个模型。这是西门子 KG86NAI31L 冰箱的模型。如果你上网查过,你可能会说这块硬纸板看起来一点也不像那个冰箱。它没有门,甚至它的颜色也不一样。
虽然这是事实,但也无关紧要。正如我们所讨论的,一个模型不应该复制真实世界的实体。相反,它应该有一个目的(一个它应该解决的问题)。因此,对纸板提出的正确问题是,这个模型要解决什么问题?
在我们的公寓里,并没有进入厨房的标准入口。纸板被精确切割成冰箱宽度和深度的大小。它解决的问题是检查冰箱是否可以通过厨房门(见图3-10)。
图 3-10. 厨房门口的纸板模型尽管硬纸板看起来完全不像冰箱,但当我们不得不决定是购买这种型号还是选择较小的型号时,它被证明非常有用。再次说明,所有模型都是错误的,但有些模型是有用的。虽然构建冰箱的 3D 模型绝对会是一个有意思的项目。但它会比硬纸板更有效地解决此问题吗?并不会。如果纸板适合,3D 模型也会适合,反之亦然。在软件工程的术语中,构建冰箱的 3D 模型将是严重的过度工程。
但是冰箱的高度呢?如果底座适合,但它太高而无法进入厨房入口怎么办?这可以证明粘合出冰箱的 3D 模型是合理的吗?并不能,通过使用简单的卷尺来检查门口的高度,可以更快、更轻松地解决这个问题。在这种情况下,卷尺是什么?其实是另一个简单的模型。
因此,我们最终得到了同一台冰箱的两个模型。使用这两个模型,每个模型都为其特定的任务进行了优化,反映了 DDD 对业务领域建模的方法。每个模型都有其严格的限界上下文:验证冰箱底座是否可以通过厨房入口的纸板,以及验证冰箱不会太高的卷尺。模型应当省略与当前任务无关的信息。此外,如果多个更简单的模型可以有效地单独解决每个问题,则无需设计复杂的万事通模型。
我 在 Twitter 上发布这个故事 的几天后,我收到了一个回复,说与其摆弄硬纸板,不如直接用带有LiDAR 扫描仪的手机和增强现实(AR)应用程序。让我们从领域驱动设计的角度分析一下这个建议。
评论的作者说,这是一个别人已经解决了的问题,而且解决方案是现成的。毋庸置疑,扫描技术和 AR 应用都很复杂。在 DDD 术语中,这使得检查冰箱是否适合通过门口的问题成为了一个通用子域。
每当我们偶然发现领域专家的心智模型中存在固有冲突时,我们必须将通用语言分解为多个限界上下文。通用语言应该在其限界上下文的范围内保持一致。然而,在不同的限界上下文中,同样的术语可以有不同的含义。
在发现子域的同时,对限界上下文进行设计。将领域划分为限界上下文是一项战略性设计决策。
一个限界上下文及其通用语言应由一个团队实现和维护。两个团队无法在同一个限界上下文上分享工作。然而,一个团队可以负责多个限界上下文。
限界上下文将系统分解为物理组件 -- 服务、子系统等等。每个限界上下文的生命周期都与其余的解耦。每个限界上下文都可以独立于系统的其余部分而发展。然而,限界上下文必须一起工作以形成一个系统。有些变化会无意中影响到另一个限界上下文。在下一章中,我们将讨论集成限界上下文的不同模式,这些模式可用于保护它们免受级联变化的影响。
-
子域和限界上下文有什么区别?
A. 子域是被设计出来的,而限界上下文是被发现的。
B. 限界上下文是被设计出来的,而子域是被发现的。
C. 限界上下文和子域本质上是相同的。
D. 以上都不正确。 -
限界上下文是以下内容的边界:
A. 模型
B. 生命周期
C. 所有者
D. 以上所有 -
关于限界上下文的大小,下列哪项是正确的?
A. 限界上下文越小,系统越灵活。
B. 限界上下文应始终与子域的边界对齐。
C. 限界上下文边界越宽越好。
D. 视情况而定。 -
关于限界上下文的团队所有权,以下哪项是正确的?
A. 多个团队可以在同一个限界上下文上工作。
B. 一个团队可以拥有多个限界上下文。
C. 限界上下文只能由一个团队拥有。
D. B 和 C 是正确的。 -
回顾前言中 WolfDesk 公司的例子,并尝试识别系统中支持工单这个功能,此功能需要不同模型。
-
除了本章中描述的例子之外,试着找到现实生活中的限界上下文的例子。
Footnotes
-
这里有个例外值得一提。根据你所在的组织,你可能身兼两职,同时负责软件工程和业务发展。因此,你有能力影响软件设计(限界上下文)和业务战略(子域)。因此,在我们这里讨论的(限界)上下文中,我们只关注软件工程。 ↩
-
Bredemeyer Consulting,“什么是软件架构”。 2021 年 9 月 22 日,https://www.bredemeyer.com/who.htm ↩