领域驱动设计(DDD) 自_Eric Evans_于 2003 年出版他的有关该主题的书以来就一直存在。几年前,当我加入一个遭受数据一致性问题困扰的项目时,我自己就接触了 DDD。重复出现在数据库中,一些信息根本没有保存,你可能随时随地遇到乐观锁定错误。我们设法通过使用战术领域驱动设计的构建块来解决这个问题。

从那时起,我学到了更多关于领域驱动设计的知识,并尝试在我的项目中适当地使用它。然而,在过去几年中,当我与其他开发人员交谈时,他们中的许多人都听说过域驱动设计这个术语,但他们不知道它是什么意思。在本系列文章中,我将简要介绍我所看到和理解的领域驱动设计。内容很大程度上基于Eric Evans 的《Domain-Driven Design: Tackling Complexity in the Heart of Software》和Vaughn _Vernon__的《_实现领域驱动设计》这本书。然而,我试图用自己的话来解释一切,也注入自己的想法、观点和经验。

通过阅读我的系列文章,您不会成为领域驱动设计方面的专家,但我希望它能激发您在其他地方阅读更多有关它的信息。我也强烈建议您阅读 Evans 和 Vernon 的书籍。

现在让我们从第一个主题开始,即战略领域驱动设计。

什么是域?

如果我在 MacBook 上的 Dictionary 应用程序中查找单词_domain_,我会得到以下定义:

统治者或政府拥有或控制的领土…

  • 特定的活动或知识领域……

- 苹果词典

在领域驱动设计的情况下,我们感兴趣的是定义的第二部分。在这种情况下,_活动_是组织所做的任何事情,知识_是组织如何做的。我们还将将组织开展活动的_环境添加到域概念中。

子域

领域概念非常广泛和抽象。为了使其更加具体和有形,将其拆分为称为_子域_的较小部分是有意义的。找到这些子域并不总是一件容易的事,如果你弄错了,当你的拼图中的各个部分突然不能很好地结合在一起时,你可能会遇到麻烦。

在寻找子域之前,您应该考虑子域类别。所有子域可以分为三类:

  1. 核心域
  2. 支持子域
  3. 通用子域

这些类别不仅可以帮助您找到子域,还可以帮助您确定开发工作的优先级。

_核心域_是使组织与众不同并与其他组织不同的原因。如果一个组织在其核心领域没有特别出色,就无法成功(甚至存在)。因为核心领域是如此重要,它应该得到最高优先级、最大努力和最好的开发人员。对于较小的域,您可能只识别一个核心域,较大的域可能有多个。您应该准备好从头开始实现核心域的功能。

_支持子域_是组织成功所必需的子域,但它不属于核心域类别。它也不是通用的,因为它仍然需要相关组织的某种程度的专业化。您可以从现有解决方案开始,然后对其进行调整或扩展以满足您的特定需求。

_通用子域_是一个子域,它不包含任何对组织来说特别的东西,但仍然需要整个解决方案才能工作。通过尝试为您的通用子域使用现成的软件,您可以节省大量时间和工作。一个典型的例子是用户身份管理。

值得注意的是,同一个子域可以根据组织的工作分为不同的类别。对于一家专门从事身份管理的公司来说,身份管理是一个核心领域。但是,对于专门从事客户关系管理的公司来说,身份管理是一个通用的子域。

最后,值得指出的是,所有子域对于整体解决方案都很重要,无论它们属于哪个类别。然而,它们确实需要不同的努力,并且可能对质量和完整性有不同的要求。

例子

假设我们正在为小型诊所构建 EMR(电子病历)系统。我们已经确定了以下子域:

  1. _用于管理患者_医疗记录(个人信息、病史等)的患者记录。
  2. _Lab_用于订购实验室测试和管理测试结果。
  3. _调度_用于安排约会。
  4. 文件存档,用于存储和管理附加到患者记录的文件(例如不同的文档、X 射线照片、扫描的纸质文档)。
  5. _身份管理_确保正确的人可以访问正确的信息。

现在,我们如何对这些子域进行分类?最明显的是_文件存档_和_身份管理_,它们显然是通用的子域。但是其他人呢?这取决于是什么让这个特定的 EMR 系统在市场上脱颖而出。

image-20220423143756553

由于我们正在构建 EMR 系统,因此可以很安全地假设_患者记录_是一个核心领域。如果我们要通过智能和创新的调度使所有诊所更有效地工作的系统来占领市场,那么_调度_可能也是一个核心领域。否则,它是一个支持子域,可能建立在一些现有的调度引擎之上。相同的推理可以应用于_实验室_子域:如果我们业务案例的重要部分是患者记录和实验室之间的无缝集成,那么实验室很可能是核心域。否则,它是一个支持子域。

从问题到解决方案

您有时会发现被称为“问题域”的域。这是因为域定义了软件将要解决的问题(毕竟,首先制作软件是有原因的)。Vaughn Vernon 将域分成_问题空间_和_解决方案空间_。问题空间集中在我们试图解决的_业务问题上。_子域属于这个空间。

解决方案空间专注于_如何_解决问题空间中的问题。它更具体、更具技术性并且包含更多细节。那么,我们如何将问题转化为解决方案呢?

无处不在的语言

为了能够为域创建软件,您需要一种描述域的方式。拥有关系数据模型或类似的东西是不够的。您不仅需要能够描述事物及其关系,还需要能够描述诸如事件、流程、业务不变量、事物如何随时间变化等动态。您需要能够与您的开发人员同行和领域专家讨论和推理该领域。你需要的是一种_无处不在的语言_。

无处不在的语言是领域专家和开发人员一致使用的语言来描述和讨论领域。除了代码本身,这种语言是领域驱动设计过程中最重要的可交付成果。该语言的很大一部分是领域专家已经使用的领域术语,但您可能还需要与领域专家合作发明新的概念和流程。因此,领域专家的_积极参与_对于领域驱动设计的成功是绝对必要的。如果客户对投入时间和精力来教授您的领域并帮助您创建一种无处不在的语言不感兴趣,那么您应该尝试说服客户改变主意或选择另一种设计方法。

您可以通过各种方式记录无处不在的语言。一个好的起点是创建术语表。可以使用例如泳道图和流程图以图形方式描述业务流程。UML 可用于描述事物之间的关系,状态图可用于描述随着不同事物在不同流程中移动时状态如何变化。子域也是通用语言的一部分,您甚至可能需要为不同的子域定义不同的语言“方言”。这种无处不在的语言的体现就是_领域模型_,它最终会转化为工作代码。换句话说,域模型_与_数据模型或 UML 类图不同。

无处不在的语言有一个很好的功能,那就是它可以告诉您您是否走在正确的轨道上。如果您可以使用该语言轻松解释业务概念或流程,则意味着您走在正确的轨道上。另一方面,如果您发现自己难以解释某些东西,那么您很可能会从语言中遗漏一些东西,从而也从您的领域模型中遗漏一些东西。发生这种情况时,您应该找一位领域专家并寻找丢失的部分。您甚至可能会偶然发现一个启示,它将您现有的模型完全颠倒过来,并产生一个比您以前拥有的更优越的领域模型。

介绍限界上下文

在一个完美的世界里,只有一种普遍存在的语言和一种模型可以解释单个领域的一切。不幸的是,情况并非如此,除了非常小的和简单的域。业务流程可能重叠甚至冲突。同一个词可能意味着不同的事物,或者不同的词在不同的上下文中可能意味着相同的事物。可能有(而且经常有)不止一种方法可以解决问题空间中的问题,具体取决于您如何看待它。

我们没有试图找到大统一模型,而是选择接受事实,而是引入一种叫做_有界上下文_的东西。有界上下文是领域的一个独特部分,其中_普遍存在的语言的特定子集或方言始终保持一致_。换句话说,我们正在应用分而治之,并将域模型拆分为更小、或多或少独立的模型,并具有明确定义的边界。每个有界上下文都有自己的名称,这个名称是通用语言的一部分。

有界上下文和子域之间不一定存在一对一的映射。由于有界上下文属于解决方案空间,而子域属于问题空间,因此您应该将有界上下文视为许多可能解决方案中的一种替代解决方案。因此,单个子域可以包含多个有界上下文。您可能还会发现自己处于单个有界上下文跨越多个子域的情况。没有规则反对这一点,但这表明您可能需要重新考虑您的子域或上下文边界。

就个人而言,我喜欢将有界上下文视为单独的系统(例如,Java 世界中单独的可执行 JAR 或可部署的 WAR)。一个完美的现实世界例子是_微服务_,其中每个微服务都可以被认为是它自己的有界上下文。但是,这并不意味着您必须将所有有界上下文实现为微服务。有界上下文也可以是单个单体系统内的单独子系统。

例子

让我们重新审视前面示例中的 EMR 系统,更具体地说,是_患者记录_核心域。我们可以在那里找到什么样的限界上下文?现在我不是医疗保健软件方面的专家,所以我只会编一些,但希望你能明白。

该系统支持医生预约和理疗服务。此外,对于新患者,还有一个单独的入职流程,他们接受采访、拍照并进行初步评估。这导致核心域内的以下有界上下文:

image-20220423143832845

  1. _用于管理患者个人_信息(姓名、地址、财务信息、医疗背景等)的个人信息。
  2. 将新患者引入系统的_入职培训。_
  3. 医生在检查和治疗患者时使用的_体检。_
  4. _物理_治疗师在检查和治疗患者时使用的物理疗法。

在一个非常简单的系统中,您可能会将所有内容压缩到一个上下文中,但此 EMR 更先进,并为所提供的每种服务类型提供了简化和优化的功能。但是,我们仍然在同一个核心子域中。

上下文之间的关系

在一个非平凡的系统中,很少(如果有的话)有界上下文是完全独立的。大多数上下文将与其他上下文有某种关系。识别这些关系不仅在技术上很重要(系统如何在技术上相互通信),而且对它们的开发方式(开发系统的团队如何相互通信)也很重要。

识别有界上下文之间关系的最简单方法是将上下文分类为_上游上下文_和_下游上下文_。将环境想象成河流旁边的城市。上游的城市将东西倾倒到河流中,河流到达下游的城市。有些东西对下游城市来说是必不可少的,所以他们从河里取回。其他东西是有害的,会对下游城市造成直接损害(“sh*t rolls downhill”)。

作为上游或下游有其优点和缺点。上游上下文不依赖于任何其他上下文,这在某种程度上使它可以自由地向任何方向发展。但是,任何更改的后果在下游环境中都可能是严重的,这可能反过来对上游环境施加限制。下游上下文受其对上游上下文的依赖性的限制,但无需担心进一步破坏下游的其他上下文,这在某种程度上使下游上下文的开发人员比上游上下文的开发人员更自由。

您可以通过使用从下游上下文指向上游上下文的箭头指向的依赖关系图或使用 U 和 D 角色以图形方式描述关系。

image-20220423143852058

最后请记住,上下文可以同时是上游上下文和下游上下文,具体取决于您所处的位置。

上下文映射和集成模式

一旦我们知道我们的上下文是什么以及它们是如何相关的,我们就必须决定如何整合它们。这涉及几个重要问题:

  1. 上下文边界在哪里?
  2. 上下文将如何进行技术交流?
  3. 我们将如何在上下文的领域模型之间进行映射(即我们如何从一种普遍存在的语言翻译成另一种语言)?
  4. 我们将如何防止上游发生不必要的或有问题的变化?
  5. 我们将如何避免给下游上下文带来麻烦?

这些问题的答案将被编译成_上下文图_。可以像这样以图形方式记录上下文映射:

image-20220423143908462

为了更容易创建上下文映射,有一组现成的集成模式适用于大多数用例。根据您选择的集成模式,您可能需要向上下文映射添加其他信息以使其真正有用。

合作

两种情况下的团队合作。接口——无论它们是什么——不断发展,以便它们适应两种环境的开发需求。相互依赖的功能经过适当的计划和安排,以便它们对两个团队造成尽可能小的伤害。

共享内核

两个上下文共享一个共同的代码库,即内核。任何团队都可以修改内核,但必须先咨询其他团队。为了确保不会引入意外的副作用,需要持续集成(自动测试)。为了使事情尽可能简单,共享内核应该尽可能小。如果大量模型代码最终在共享内核中,这可能表明上下文实际上应该合并到一个大上下文中。

客户供应商

上下文处于上游-下游关系中,并且这种关系是正式的,上游团队是_供应商_,下游团队是_客户_。因此,即使两个团队都可以在他们的系统上或多或少地独立工作,上游团队(供应商)也需要考虑下游团队(客户)的需求。

墨守成规

上下文处于上游-下游关系。然而,上游团队没有动力去满足下游团队的需求(例如,它可能作为服务从更大的供应商处订购)。下游团队决定遵循上游团队的模型,不管它是什么。

反腐层

上下文处于上游-下游关系,上游团队不关心下游团队的需求。然而,下游团队并没有遵循上游模型,而是决定创建一个抽象层来保护下游上下文不受上游上下文变化的影响。这个反腐败层允许下游团队使用最适合他们需求的域模型,同时仍然与上游上下文集成。当上游上下文发生变化时,反腐败层也必须发生变化,但下游上下文的其余部分可以保持不变。将此策略与持续集成相结合可能是一个好主意,其中使用自动化测试来检测上游接口的变化。

开放主机服务

使用明确定义的协议,由明确定义的服务提供对系统的访问。该协议是开放的,因此任何需要的人都可以与系统集成。Web 服务和微服务就是这种集成模式的一个很好的例子。这种模式与其他模式的不同之处在于它不关心上下文和开发它们的团队之间的关系。您最终可能会将开放主机服务模式与任何其他模式结合起来。

使用这种模式的关键是保持协议的简单和稳定。大多数系统客户端应该能够从这个协议中获得他们需要的东西。为剩余的特殊情况创建特殊的集成点。

统一语言

这是我个人认为最难正确解释的集成模式。在我看来,已发布的语言与开放主机服务最接近,并且经常与该集成模式一起使用。记录语言(例如基于 XML)用于系统的输入和输出。只要您符合已发布的语言,就不需要使用特定的库或规范的特定实现。已发布语言的现实世界示例是用于表示数学公式的 MathML 和用于表示地理信息系统中的地理特征的 GML。

请注意,您不一定需要将 Web 服务与已发布的语言一起使用。您还可以进行设置,将文件放入目录并由批处理作业处理,该批处理作业将输出存储在另一个文件中。

分开的方式

这种集成模式的特殊之处在于它根本不执行任何集成。尽管如此,将其保留在工具箱中仍然是一种重要的模式,最终可能会节省大量金钱和时间。当两个上下文之间的集成的好处不再值得付出努力时,最好将上下文彼此分开,让它们独立发展。造成这种情况的原因可能是系统已经发展到不再相关的程度。下游上下文实际使用的上游上下文提供的(少数)服务在下游上下文中重新实现。

为什么战略领域驱动设计很重要?

我相信战略领域驱动设计最初是为大型项目设计的,但我认为你也可以在小型项目中从中受益——即使你最终没有在项目中使用 DDD 的任何其他部分。

就我个人而言,战略领域驱动设计的主要收获如下:

  1. 它引入了边界。在我所有的爱好项目中,范围蔓延是一个不变的因素。最终,它们变得比工作更有趣,或者完全不切实际地独自完成。在处理客户项目时,我必须努力避免因过度思考或过度设计而导致技术范围蔓延。边界——无论它们在哪里——帮助我将项目分成更小的部分,并在正确的时间专注于正确的部分。

  2. 它不需要我找到一个适用于所有情况的超级模型。它认识到,在现实世界中,通常在或多或少明确定义的上下文中存在许多较小的模型。它没有打破这些模型,而是拥抱它们。

  3. 它可以帮助您思考您的系统将带来的价值,以及您应该在哪里尽最大努力获得最大的价值。我从项目中获得了个人经验,正确识别然后专注于核心领域会产生巨大的影响。不幸的是,我还没有听说过战略 DDD,时间和金钱都被浪费了。

我也很了解自己可以用这种方法识别风险:为了找到子域和有界上下文而找到子域和有界上下文。当我学到我喜欢的新东西时,我非常想在现实世界中尝试一下。这有时可能意味着我去寻找不存在的东西。我的建议是始终从_一个核心域_和_一个有界上下文_开始。我仔细地进行域建模,如果存在,其他子域和有界上下文最终会显露出来。