领域驱动设计简要总结

[

Cem Yasar

](https://mcyasar..com/?source=post_page-----30b7d3fd0eac--------------------------------)

大家好,在这篇文章中,我尝试讲述我从 Eric Evans 的领域驱动设计概念中学到的东西。领域驱动设计本质上涵盖了模型驱动设计,因为其基本动机是基于理解的领域创建模型。

作为一名长期的软件开发人员,我参与了许多不同企业的许多项目。我主要从事整体结构工作,遇到了好的分层结构和糟糕的结构化项目。
甚至最新的项目甚至都docker化并在kubernetes环境中发布,甚至应用了GitOps机制。
我想说,即使项目可以是庞然大物,如果分层架构和模块化很好,这是一个非常好的迹象,表明您正在应用一些理解,这实际上是领域驱动设计概念的基础。

让我们通过简单的例子来思考一下。例如,我们可以想到食品订购应用程序。当您想订购食物时,应用程序最初会要求您选择您的位置/地址。
在这里,我们可以考虑地址详细信息的实现以及与该地址详细信息的客户关系。
正如您所看到的,此应用程序中的所有可能的地址位置都随时可用,您可以轻松找到您的街道、大道、街区、公寓等。这不仅仅是为客户提供的简单的基于多列的地址详细信息。
甚至,该食品订单应用程序中的地址定位器模块也可以是另一个基于 API 的应用程序,因为它在食品配送业务和定位客户地址(特别是对于物流)中非常重要。这里的地址概念本身就是一个复杂的模型。
但是,对于其他业务,您甚至可以在客户详细信息中附加简单的地址详细信息。
对于食品配送业务,如果我们使用这种简单的方法,客户和食品服务公司将不会有共同的地址详细信息,并且数以百万计的相同和不相关的地址详细信息将被保存,并且系统在一段时间后将无法工作。
然而,对于其他应用程序,将简单的地址详细信息保存在客户的一列或几列中可能是很正常的。这个例子给出了有关实体和值对象的说明。
对于食品配送系统,客户和地址被概念化为实体,但是其他应用程序中地址的处理方式很简单,这里地址被概念化为值对象。
您可以根据需要以非规范化方式保存值对象或将其保存在不同的表中。如果您使用 RDMS,您很可能将这个简单的地址详细信息保存在不同的表中。

实体应通过其独特的身份进行跟踪。但是,如果地址详细信息作为值对象处理,则无需通过身份跟踪它们,它们只是相关实体的一些额外信息。
Eric Evans 详细说明了这一细节:“值对象被实例化来代表设计元素,我们只关心它们是什么,而不关心它们是谁或是什么”。然而,值对象应该按照 DDD 原则中提到的那样明确表示。
在概念、语言、代码实现上明确是非常重要的。即使地址详细信息存储在客户表(可能是一个单独的表)中,它也应该表示为客户实体对象中的显式对象。
还建议,值对象应该主要是不可变的。此外,正如 Eric Evans 提到的,必要时,值对象可以由不同对象共享以提高性能。因此,在这种情况下,保持不变就变得非常重要。
如果适合实现要求,您还可以将相同的值对象复制到不同的实例中。然而,在某些情况下,您可以使值对象可变。正如埃里克·埃文斯提到的:

“如果 VALUE 频繁变化”,
“如果对象创建或删除的成本很高”,
“如果替换(而不是修改)会干扰聚类(如前面的示例中所述)”,
“如果没有太多的值共享,或者如果为了改进集群或其他一些技术原因而放弃了这种共享”。

我要再次强调的是,在 DDD 原则中,所有事情都明确是非常重要的。明确使每个人都理解相同的事情。
在前面的示例中,如果我们不提及地址作为涵盖某些列(例如街道、城市、国家/地区)的对象,并且如果这些属性直接在客户对象中定义,那么我们就可以失去地址概念的含义,并且有也许其他值应该被称为值对象。
作为值对象的属性的分离和分组使一切都非常明确,如果我们需要一些限制,围绕这些值的一些策略,值对象方法将使我们很容易看到问题。
您可以通过以下链接找到 Milan Jovanovic 提供的详细、简单、清晰的值对象示例和描述:https://www.milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals
另外,您可以检查此链接:https://domaincentric.net/blog/ddd-building-blocks-in-php-value-object

实际上,如果您从事软件开发很长时间,您也一直在考虑将事物概念化并尝试应用好的解决方案,例如以规范化方式或非规范化方式保存数据。
您可能会考虑在公共位置对数据的某些部分进行模板化,并将这些数据与其关系一起保存,并使用此模板化数据来可视化 UI 上的内容,例如公共汽车或飞机上已售完或尚未售完的座位。
由于对于每个相同的巴士模型,座位位置都是相同的,因此无需创建或添加这些座位作为巴士表示的属性。
使用计费系统中使用的巴士模型,您可以在另一个API中检索座位计划,该API主要基于巴士模型,很可能在不同的系统中而不是计费系统中,也许这个系统可以是旅游公司的巴士管理系统,否则,如果您尝试在计费系统中添加每辆公交车的所有座位来管理每次旅行的座位购买,则会产生大量冗余数据。
如果您在分析事物时有这些直觉,那么您就可以很好地理解 DDD。

DDD 也不是构建系统、代码库,它还构建开发团队。埃里克·埃文斯 (Eric Evans) 还根据自己的经验和对团队结构的重要性提出了建议。
并且他反复提到,理解领域是一个渐进的事情,一天或者一个月之内很难掌握领域的所有细节。软件开发是一个循序渐进的事情。与不同行业的领域专家进行交流非常重要。
如果你没有找到合适的领域专家,Eric Evans建议阅读相关业务的相关书籍,比如如果你想开发会计应用程序,你可以阅读有关会计的书籍。

在具有分层架构的经典遗留方法中,我们也创建实体,但是它们是仅包含属性的数据库的纯粹表示。在编码中形式化领域概念并不是一个好方法。
当您查看这些实体时,您只看到数据库表,什么也没有。此外,为了明确领域概念,我们需要收集相关对象、实体、值对象等。我们在哪里可以将这些对象收集在一起?我们可以关注根对象实体。
例如,订单实体。这个根实体在 DDD 中称为聚合根。
正如您可以猜到的那样,在订单实体中,还有其他相关实体和值对象,例如订单行项目等。在经典方法中,我们倾向于组织数据层、业务层等,通过这样做,实际上我们正在失去域关系、描述。
然而,如果我们围绕聚合根组织我们的概念并围绕聚合收集所有相关的交互,那么域概念就会变得更加明确。
在传统方法中,即使我们创建订单行项目存储库和其他独立的数据层对象,但是在 DDD 中,禁止直接访问订单行项目。所有与聚合根中的子实体或值对象的交互都应该通过聚合根完成。
例如,如果我们想要添加、更新或删除订单行项目,我们需要与聚合根(即订单实体)进行交互。通过这样做,我们可以保护域的完整性、聚合的策略或限制。
未来没有人会尝试绕过这些规则并区分域的完整性。如果需要更新某些条件,这种方法使我们通过检查聚合周围的所有条件来仔细思考。埃里克·埃文斯 (Eric Evans) 解释了这样的聚合概念:
“首先,我们需要一个抽象来将引用封装在模型中。 AGGREGATE 是一组关联对象,我们将其视为一个单元以进行数据更改。每个 AGGREGATE 都有一个根和一个边界。边界定义了 AGGREGATE 内部的内容。这
root 是包含在 AGGREGATE 中的单个特定实体。根是允许外部对象保存引用的 AGGREGATE 的唯一成员,尽管边界内的对象可以保存彼此的引用。
除根之外的其他实体都具有本地标识,但该标识仅需要在 AGGREGATE 内进行区分,因为任何外部对象都无法在根实体的上下文之外看到它。

他提到了一些基于聚合的规则:

  • 根实体具有全局身份并最终负责检查不变量。
  • 根实体具有全局身份。边界内的实体具有本地标识,仅在聚合内是唯一的。
  • AGGREGATE 边界之外的任何内容都不能保留对内部任何内容的引用,根实体除外。根实体可以将对内部实体的引用传递给其他对象,
    但这些对象只能暂时使用它们,并且它们可能无法保留引用。
    根可能会将 VALUE OBJECT 的副本传递给另一个对象,并且它发生什么并不重要,因为它只是一个 VALUE,不再与 AGGREGATE 有任何关联。
  • 作为上一条规则的推论,只能通过数据库查询直接获取 AGGREGATE 根。所有其他对象必须通过遍历关联来找到。
  • AGGREGATE 中的对象可以保存对其他 AGGREGATE 根的引用。
  • 删除操作必须立即删除 AGGREGATE 边界内的所有内容。 (使用垃圾收集,这很容易。因为除了根之外没有任何外部引用,因此删除根,其他所有内容都将被收集。)
  • 当提交对 AGGREGATE 边界内的任何对象的更改时,必须满足整个 AGGREGATE 的所有不变量。

可见,聚合根是一个实体,可以包括内部相关实体和值对象。它是该聚合的真实来源。聚合体之间可能存在大量的交互、规则。
因此,您应该考虑什么可以是实体,并且在构建实体之间的关系时,您可以决定哪个实体应该是聚合根。
由于通过聚合根你可以收集尽可能多的规则、限制和交互,因此很容易理解这个聚合内的事物,并且将来聚合内的一致性不会被破坏。
因此,这些聚合将成为该域的模型,例如“订购”域,其中包括订购特定规则、限制和其他要求。甚至,该域的模型中可以有多个聚合根和相关实体。
甚至,在必要时,您可以将另一个聚合根作为属性引用。这些实体、聚合是领域建模的初步尝试。

在这里,引用“Abel Avram 和 Floyd Marinescu 的快速领域驱动设计”中的一段很好的话:

“在具有复杂关联的模型中,很难保证对象变化的一致性。很多时候,不变量适用于密切相关的对象,而不仅仅是离散的对象。
然而,谨慎的锁定方案会导致多个用户毫无意义地相互干扰,并使系统无法使用
.
因此,使用聚合。聚合是一组关联的对象,在数据更改方面被视为一个单元……”

在这里,可能是提一下“限界上下文”的好时机。实际上它是上下文的边界。我相信,再次分享“Abel Avram 和 Floyd Marinescu 的快速领域驱动设计”中的这句话真的很好:

“每个模型都有一个背景。当我们处理单一模型时,
上下文是隐含的。我们不需要定义它。什么时候我们
创建一个应该与其他应用程序交互的应用程序
软件,例如遗留应用程序,很明显
新应用程序有自己的模型和上下文,它们是
与遗留模型及其上下文分离。他们不能是
组合、混合或混淆。但当我们从事大型工作时
企业应用程序,我们需要为每个应用程序定义上下文
 我们创建的模型。

So, here, what does it mean “…**那么,在这里,“……我们需要为每个定义定义上下文”是什么意思?
我们创建的模型。” ?**实际上,通过概述边界来定义上下文是实施之前或实施之前的一组文档。特别是,DDD 中提到了上下文映射概念。再次引用“Abel Avram 和 Floyd Marinescu 的快速领域驱动设计”中的精彩引言:

“虽然每个团队都在研究自己的模型,但了解整体情况对每个人都有好处。上下文映射是概述不同有界上下文以及它们之间关系的文档。
上下文地图可以是如下图所示的图表,也可以是任何书面文档。详细程度可能有所不同。重要的是参与该项目的每个人都分享并理解它。

Eric Evans also mentions about “Domain Vision Statement” concept. He is saying that, “Eric Evans 还提到了“领域愿景声明”的概念。他的意思是,“写一篇关于核心领域及其带来的价值的简短描述(大约一页),即“价值主张”。忽略那些无法将该领域模型与其他领域模型区分开来的方面。展示领域模型如何服务和平衡不同的利益。保持狭窄。
尽早写下此陈述,并在获得新见解时对其进行修改。

所有这些实体,其中一些将是聚合根,我们将在这些上下文中形成聚合。不同上下文之间会存在交互,并且关于如何在 DDD 描述中处理这些交互有很多想法。
例如,“共享内核”、“客户-供应商”、“分离方式”、“开放主机服务”、“反腐败层”方法。实际上这些方法是在必要时才应用的,它们是某种系统设计方法。
您可以在 Eric Evans 的书中找到详细的描述和场景,参见本章:“有界上下文之间的关系”。

Eric Evans 还提到了“核心域”的细节。例如,如果项目重点是电商平台,那么订购、计费等域可以是核心域。然而,报告、组织结构图演示可能不是核心领域。
如您所知,所有软件应用程序都是这些域的元素,例如组织图表应用程序或组件。
他还提到了许多关于如何处理不属于您的核心域的域的优缺点,例如外包、使用第 3 方应用程序等。这些主要被描述为“通用子域”。
他还建议,核心模型或核心模型对于公司开发人员的处理/实现非常重要,因为,如果您将第三方工具用于核心领域,那么将来对于一些额外的要求,您可能无法自己进步,也可能不会生产满足您需求的专用组件。

他建议还创建一个文档来突出显示核心领域,文档名称称为“The Distillation Document”。本文档建议长度为 3 或 7 页。他还提到,“编写团队中的非技术成员能够理解的文档。将其用作描述每个人都需要了解的内容的共享视图,以及所有团队成员可以开始探索模型和代码的指南。”他还提到了这份文件的重要性:

“如果精炼文件概述了核心领域的要点,那么它就可以作为模型变革重要性的实用指标。当模型或代码更改影响蒸馏文档时,需要与其他团队成员协商。
当进行更改时,需要立即通知所有团队成员,并传播新版本的文档。
CORE 之外的更改或未包含在蒸馏文件中的细节可以在无需协商或通知的情况下进行整合,并且其他成员将在工作过程中遇到这些更改。然后开发人员就拥有 XP 建议的完全自主权。

他提供了许多关于简短但清晰、有力的文档的示例和想法。甚至他在一个项目中也遇到过,给他一份长达两百页的项目文档,这让他非常沮丧。
他试图在这份文档中找出核心领域的详细信息。他得到了熟悉该领域的业务分析师的帮助。

实际上,如果您参与过分析项目中的需求,您很可能已经做过类似的事情,例如最初对您自己进行描述领域的摘要文档。
您很可能尝试过分离不同的上下文,并尝试找出上下文之间的一些必要关系。 DDD 原则和概念为您提供了更广泛的策略以及如何处理某些场景的想法。
DDD 原则包括 DevOps 进展、团队设置思路、外包选项等各个方面。然而,它给出了更重要的领域逻辑和建模概念,例如聚合边界以及围绕聚合根封装相关实体和对象。

DDD 中还有一个更重要的事情我应该提到,那就是领域服务。
在传统方法中,在许多项目中,如上所述,我们创建实体作为数据库表的表示,没有任何业务逻辑,然后我们创建其他层以包含基础架构和业务逻辑详细信息,例如业务逻辑服务和数据库相关层、dto 映射层DDD提到,这种方法不是一个好方法,特别是对于可能有很多集成的复杂项目。
正如您所记得的,因为在 DDD 中,域中的上下文由聚合根封装,并且对上下文的所有访问均由聚合根处理。这种方法保护了聚合内的一致性,也保护了域的一致性和显式性。
因为,通过这样做,您还被迫根据聚合边界来构建编程文件夹、包。在很多方面,乍一看,一切都更加明确,编码也更加具有自我描述性。
在 DDD 中,领域服务是在必要时创建的,因为所有与上下文相关的交互都存在于聚合根中,但是,有时,您可能需要上下文甚至域之间的交互,并且您可能找不到聚合中事物的可视化,您可以创建域服务。
这与遗留实现确实是不同的方法。再次,这是埃里克·埃文斯(Eric Evans)的一个很好的配额:

“……例如,如果银行应用程序可以将我们的交易转换并导出到电子表格文件中供我们分析,那么该导出就是一项应用程序服务。银行领域没有“文件格式”的含义,也不涉及业务规则。
另一方面,可以将资金从一个帐户转移到另一个帐户的功能是域服务,因为它嵌入了重要的业务规则(例如,对适当的帐户进行贷记和借记),并且因为“资金转移”是一个有意义的银行术语。
在这种情况下,服务本身并没有做太多事情;它会要求两个 Account 对象完成大部分工作。但是把“转账”操作放在Account对象上就有点尴尬了,因为这个操作涉及到两个账户和一些全局规则。

实际上,在遗留实现中,我们倾向于创建实用程序,而不是将它们称为服务,实际上它们是服务,例如在此引用中,导出电子表格。

实际上,对于领域服务,Eric Evans 最初指出:“在某些情况下,最清晰、最实用的设计包括概念上不属于任何对象的操作。我们可以遵循问题空间的自然轮廓,并将服务明确地包含在模型中,而不是强迫解决问题。”

埃里克·埃文斯的其他一些相关引用:

“服务是一种作为接口提供的操作,它独立于模型中,而不像实体和值对象那样封装状态。服务是技术框架中的常见模式,但它们也可以应用于领域层。

名称服务强调与其他对象的关系。与实体和值对象不同,它纯粹是根据它能为客户做什么来定义的。服务往往以活动而不是实体命名——动词而不是名词。
服务仍然可以有一个抽象的、有意的定义……

应明智地使用服务,不允许剥夺实体和价值对象的所有行为。但是,当操作实际上是一个重要的领域概念时,服务就形成了模型驱动设计的自然组成部分。
在模型中声明为服务,而不是实际上不代表任何内容的虚假对象,独立操作不会误导任何人。良好的服务具有三个特征。
1. 该操作涉及的域概念不是实体或值对象的自然部分。
2. 接口是根据领域模型的其他元素来定义的。 3、操作是无状态的。

在结束之前,我应该明确提及 Ubiquotus 语言。之前我提到过很多关于在与域相关的所有事情上保持明确的内容。 Ubiquotus 语言是让整个理解变得清晰的关键。
相关的关键词和描述甚至被用在实现中,通过构建Ubiquotus语言来讨论与领域相关的事物,使每个人的理解同步化。每个人对普遍语言的理解都是一样的。
因此,特别是对于可以进行多次集成的大型项目,人们彼此了解得非常清楚,并且可以轻松捕获许多概念。因此,无处不在的语言围绕着每个细节,包括实现定义。

总之,DDD 中的上下文和实现方法中的聚合边界使许多事情变得更加明确,甚至通过查看实现,许多概念也可以很容易地理解。
它以强大的方式保护领域理念,以便将来您可以调整持续集成,而不会出现许多概念模糊。
DDD 细节中还有很多其他细节,例如工厂、模块等。我在本文中试图强调领域上下文和相关聚合概念。

 谢谢阅读。

 穆斯塔法·杰姆·亚萨尔