《学习领域驱动设计》作者Vladik Khononov访谈


“与领域专家的互动在实施软件中起着关键作用。你必须确保你理解你正在解决的问题。如果不先了解问题,就无法提供软件解决方案。”
Vladik Khononov 是《学习领域驱动设计》一书的作者。
在这一集中,我们深入讨论了领域驱动设计 (DDD),Vlad 首先分享了为什么理解业务领域在软件工程中至关重要,以及 DDD 如何帮助建立领域专家和软件工程师之间的共识。
然后,Vlad 解释了 DDD 中的两个重要设计,即战略和战术设计,以及它们之间的关系。对于每个设计,Vlad 都谈到了一些重要的模式,例如限界上下文、上下文映射、子域、聚合、实体和值对象。
最后,Vlad 给出了关于将 DDD 应用于棕地项目以及这些项目如何从某些 DDD 实践中获得最大收益的重要提示。  

Vlad (Vladik) Khononov是一位拥有超过20年行业经验的软件工程师,在此期间,他曾为大大小小的公司工作,担任过从网站管理员到首席架构师的角色。Vlad作为一名作家、公共演说家和博客作者,保持着活跃的媒体生涯。他在世界各地就领域驱动设计、微服务和一般的软件架构进行咨询和讨论。Vladik和他的妻子以及他们的猫住在以色列北部。


了解业务领域的重要性
业务逻辑成为软件的核心是有原因的。它是软件被建造的原因。

你建立一个软件不是为了展示你的数据库有多快和可扩展性,或者你的用户界面有多性感。你是为了解决一个特定的商业问题而建立一个软件。这个问题可以是让某人更有效率,简化业务流程。现在,许多企业都是建立在软件之上的。这就是他们的业务领域。

软件工程最大的谎言是 "要成为一名软件工程师,你必须写代码,而我是一个内向到骨子里的人"。我们不是为了写代码而写代码。我们写代码是为了解决某人的问题。这个人,就是领域专家。他们是对我们正在建模的业务领域拥有最多知识的人,并在代码中实现它。为了有效地解决这个问题,我们必须与他们沟通,与他们交谈。我们必须有效地与人进行这种沟通和合作。因此,软件工程不仅仅是关于代码。

这就是为什么与领域专家的互动在实施软件中起着关键作用。你必须确保你了解你正在解决的问题。这很关键。如果不先了解问题,你就无法提供一个软件解决方案。要么它将是错误的解决方案,要么它将是正确的解决方案,但用于不同的问题,两者都是无用的。

如果我们把这个方法论的名字叫做领域驱动设计,我更愿意把它扩大一点,说成是商业领域驱动的软件设计。所以我们这里有三个组成部分。

  • 首先,我们有一个我们必须掌握和理解的业务领域。
  • 第二,我们有软件设计,这就是我们在这里建立的东西。我们在中间有 "驱动 "这个词,这意味着,用数学语言来说,就像软件设计等于业务领域的功能。如果你没有得到正确的业务领域,那么你将得到错误的软件设计。你可能在解决错误的问题,或者你可能在试图解决正确的问题,但却是以一种低效的方式。
  • 其次,在领域驱动设计的指导下,我们有很多设计决策是建立在对业务领域的了解之上的。那是战略性的设计决定,比如我们要用什么粗粒度的组件来实现我们的系统?它们将如何相互沟通?我们如何将这些工作分配给软件工程团队?
  • 我们还有一些战术性的设计决定,比如,我们要用什么设计模式来实现每个组件?我们要如何实现这个业务逻辑?我们将如何协调组件的不同部分的互动?这是我们的内部架构。当然,我们有高层架构。组件之间是如何工作的?

如果你把这个业务领域的知识弄错了,那就会做出一些错误决策,包括战略和战术上的决定。
所以,这就是为什么我们必须确保在我们试图为它设计一个解决方案之前,我们对问题领域有足够的了解

领域驱动设计如何帮助我们
我实际上相信[软件项目的高失败率是可以解决的]。如果我们寻找这么多软件项目失败的原因--如果你看一下关于这个话题的研究,你会发现相当多的项目,如果不是全部,都是因为沟通问题或与沟通有关的东西而失败的。它可以是团队成员之间的沟通,软件工程师和领域专家之间的沟通,管理层和工程团队之间的沟通问题。

领域驱动设计的核心原则是在项目的不同利益相关者之间建立一个共同的理解,这样他们就可以用同样的语言来谈论和推理这个软件系统。在DDD的行话中,这被称为泛在语言。

一旦我们能够用相同的语言促进交流,而不需要在知识到达目的地之前进行多次翻译,一切都会变得更有效率。这可以为我们解决很多的返工,很多的错误假设。而最终,我相信这将导致更多成功的软件项目。

一切都在变化,尤其是当你在一家创业公司工作时。不仅是我们作为软件工程师不知道如何以最有效的方式实现事情,而且商业人士也不知道他们要如何解决他们所关注的某个问题。

他们知道,"嘿,有件事我们想做。我们要怎么做呢?我们将进行迭代。我们将测试一些解决方案,直到我们找到最理想的方案。

在这段时间里,一切都会改变。

对业务领域的理解将不断发展。不仅你要学习越来越多的知识,而且那些业务人员,那些领域专家,他们也要学习。随着他们获得更多的知识,这必须反映在软件设计决策中。

如果这个组件的业务领域发生了变化,它就必须反映在软件设计中。可以有很多类型的变化,从更有效地描述业务领域开始,更好的术语,对业务流程的更好理解。

但同时,整个业务领域也会发生变化。现在,一个公司从一个商业目标开始,然后因为之前的目标在财务上不可行而改变它,这是很正常的。

这必须反映在软件设计中。如果不能对业务领域的变化做出反应,随着时间的推移,就会产生技术债务,最终,它会导致你有非常大的技术债务和一个大的泥球。

DDD战略设计
我认为,战略设计更为重要。因为你可以在只包含战略设计决策的情况下写出相当成功的项目。但如果你只关注战术设计决策,那么,至少在我的经验中,这可能不会有好结果。

  • 首先,战略设计是关于在软件工程师和商业领域专家之间建立一个共同的理解。它是关于培养共同的语言和共同的理解。

要做到这一点,你在交流中使用的语言有几个要求。它的主要要求是每个术语应该有一个而且只有一个意思。

人,与软件不同,他们有时是如此不可预测的。他们可以说一件事,但由于上下文的不同而有不同的意思。在做出软件设计决定时,我们不能允许这种情况发生。我们必须确保每一项知识都是明确的。

领域驱动设计说:"嘿,当你有这样一种依赖于上下文的无处不在的语言时,要在代码中明确地对它进行建模。定义该语言所适用的环境。这是有边界的上下文"。这就是第一个战略性的设计决定。

它确保你有一个由有界上下文所涵盖的模型,它是明确的。它的每个术语都只有一个含义。

由于一些历史问题和错误,(许多人)认为有界的上下文必然是一个微服务。这是不正确的。一个有边界的上下文反而是你最大的有效的单体,因为它包含了一个没有冲突的模型。你可以进一步分解它。我们的想法是找到这些边界,确保你有一个正确的模型在其中,没有冲突或碰撞。

  • 第二,一个系统不会自己构建,为此,我们需要我们,软件工程师。在任何组织中,可能有一个团队的人或多个团队的人在同一个项目上工作。而这又会影响到你如何设计这些有边界的上下文,因为他们必须相互影响。

领域驱动设计说,"嘿,这是你在设计这些组件时必须考虑到的事情,它们将如何被整合。"

这是我们根据业务领域和团队的结构做出的第二个战略设计决定。

工程团队的结构也可以改变。这应该会影响到那些有边界的上下文之间的整合方式。


子域
子域这个词的定义相当具有挑战性。我们的想法是,一个子域是一个业务构建块。

比方说,我们有两家公司在同一行业工作。因此,他们将拥有相同的业务域。但他们不是同一家公司。他们有不同的方式。他们在各自的市场上竞争。他们有不同的产品。因此,区分它们的是它们的子域。

子域就像拼图一样,完成了公司计划如何在其业务领域取得成功的整个画面。这些是你必须实施的活动,因为你的商业战略,因为你想如何从你的竞争对手中脱颖而出,或者因为你只是必须,因为这是法律,例如,会计,你必须这样做。

子域,作为一个开始的启发式方法,通常与组织单位、部门相关联。但这是一个过于粗略的子域边界。

通常,当我们看得更深的时候,我们可以找到更多的,用软件工程术语来说,相互关联的用例集,它们要么在处理相同的数据,要么在实现相关的业务逻辑。我们能够找到那些相互关联的用例集,它们通常是一个子域的最佳边界。

子域的全部目的不是为了描述组织如何工作。为此,我们需要别的东西。那就是业务能力。这是一个更复杂的模型,因为它是分层的。有不同层次的业务能力。子域是比较简单的。我们这里只有两个层次。我们有业务域,在它下面的是子域。

因此,我们必须把注意力集中在对我们正在建立的软件系统真正重要的东西上。我们必须决定我们找到的子域的边界对我们的目的是否足够好。

根据领域驱动设计,我们有三种类型的子域。

  • 第一种是核心子域

核心子域是公司如何将自己与竞争对手区分开来。这是你有的东西,但竞争对手没有。至少你打算比他们赚更多的钱。因此,这就是各种知识产权,如专利或聪明的算法,是在他们的组织中发明的。他们可能会提出一个新的方法来解决一些问题,或者他们只是优化了一个现有的解决方案,让它以更有效、更便宜的方式得到解决。

这是你希望它留在你的公司里的东西,因为如果你的竞争对手会拥有它,那就是坏消息。这意味着你刚刚失去了一点你的竞争优势。

  • 第二种类型的子域是通用子域

它们与核心相反。所以这里没有竞争优势。这些是行业内所有公司都在使用的解决方案,可能在其他行业也是如此。而这里的好例子是一种加密算法。

所以这是一个通用的子域。一些已解决的问题。你有的问题,但你有一个现有的解决方案。你只需要去使用它就可以了。它可以是购买一个现成的产品,采用一个开源的解决方案。但无论如何,底线是,你和你的竞争对手将使用相同的解决方案,并不影响你的业务。

  • 第三种类型的子域是支持性子域,它们是在中间的某个地方

同样,与通用的一样,这里没有竞争优势。但另一方面,作为核心子域,这是你必须在内部实施的东西。你必须在内部实施它,不是因为你想,而是因为你没有选择。因为没有可用的通用解决方案。

就其性质而言,支持性子域通常比核心子域要简单得多。这涉及到一些简单的实现。因为它很简单,甚至可能有一个通用的解决方案,但你会选择不使用它,而只是实现你自己的,因为推出你自己的解决方案比整合另一个解决方案更容易。实施比整合更容易。

我的经验法则是深入研究一个子域,并决定你是否应该进一步分解它。只有在进一步分解会产生不同类型的子域的情况下。

DDD 战术设计
战略设计是关于你要实施什么。有哪些组成部分?战术设计是关于我们将如何实施它们。

我们有不同的方式来实现这些组件,业务逻辑。这些模式是我们要实现的一种类型的子域的功能。

如果它是通用的,那么我们只是使用一个现有的解决方案。如果它是一个核心子域,我们需要一个设计模式来解决这个核心子域的复杂性。如果这是一个支持性的子域,我们需要简单的模式,因为否则,我们将是过度的工程化。

战术设计决策所涵盖的另一个方面是我们要如何架构组件的整体工作,比如它的内部组件。它将如何与数据库一起工作?它将如何与用户界面一起工作,等等?

我们有架构模式,即分层架构,这通常对支持子域更有用,或者当你要整合一个通用子域时。我们有端口和适配器,这也叫六边形架构,这也叫洋葱式架构,这也叫清洁架构。

还有我们有CQRS架构。你要使用哪种模式来设计你的架构和实现业务逻辑,取决于所涉及的业务子域的类型。


聚合模式
聚合是一个既包含数据又包含所有行为的对象,所有影响数据的业务逻辑。换句话说,所有能够改变聚合的数据的东西都应该驻留在聚合的边界内。任何外部组件都不应该能够改变其数据。

通常,我们在实现一些复杂的业务逻辑时使用这种模式。如果它很复杂,如果你允许外部组件去直接修改你的数据,那么迟早会有一些业务规则影响这些数据。而这些规则将被重复。重复业务逻辑通常不是一个好主意,但重复复杂的业务逻辑通常是一个糟糕的主意。而这正是聚合模式所要防止的。

聚合不是数据库中的一个平面记录。相反,它是一个对象的层次结构。所有的数据都可以驻留在同一个聚合中。

下一个问题是,我们在哪里停止把东西塞进聚合体?我们在哪里决定不再把东西放进去?

聚合体不仅是数据和行为的边界,而且也是交易的边界。意思是说,聚合的数据的所有变化都应该在一个原子事务中提交。

所以我们必须寻找更小的边界,更有效的边界。[Vaughn Vernon]所说的,使用事务边界作为你选择聚合边界的启发式方法。确保它只包括那些对于实现其中的业务逻辑必须是强一致的数据。所以,无论什么必须是强一致性的,都要把它留在聚合中。其他一切可以最终保持一致的东西,把它放到另一个聚合或另一个组件中。但在聚合中,你应该只有那些必须是强一致性的数据。

你怎么知道你是需要强一致还是最终一致呢?试着评估这两条路径。如果某个东西最终一致,会发生什么?你能正确实现所有这些业务规则和业务逻辑吗?还是会导致你的数据被破坏?当你以这样的方式进行分析时,通常情况下,你需要的信息是强一致的,而你可以拥有最终一致的信息,这是很明显的。

这就是一个总括性的东西。它包含了数据、影响数据的行为,以及围绕所有这些数据的交易边界。

因为所有的行为都属于聚合,所以我们必须公开一个公共接口,让我们能够执行它。通常我们称它为一个命令。所以我们要在一个聚合体上执行一个命令。我说的命令是指,不是你放在消息总线上的一些消息,或者其他什么。不,这是一种进攻性的说法,即你有一个公共方法。这应该是修改聚合体内容的唯一方法。

聚合体的公共接口中非常重要的部分是领域事件。因为有时聚合体可能相当大。你最好让它们小一点,但这取决于你的业务领域。有一些重要的业务事件会在其中发生。领域事件必须用无处不在的语言,用业务的语言来表述。

因此,一旦变化,聚合被提交到数据库,我们需要发布该领域事件,以便任何对它感兴趣的组件能够订阅,并做一些与该领域事件发生有关的事情。

实体模式
当我们看到实体与聚合体在同一层面上讨论时,我们开始认为这是两种你可以选择的模式,这是不对的。一个聚合体是一个实体。聚合体是一个对象的层次结构。所以它是一个实体的层次结构。一个实体是聚合中的一个构建块。

我们需要实体的主要原因是为了将它与值对象区分开来。所以值对象是这里重要的东西,而不是一个实体。他们之间的区别,正如你所说,一个实体有一个ID。另一方面,一个值对象,可以只通过它的值来识别。

这种区别允许我们在代码中把这些值对象建模为不可变的对象。作为实体,值对象是不独立的。你不能单独使用一个值。它必须描述一些东西。所以它是驻扎在一个聚合体中的实体的一个属性。

这就是这三种核心模式的层次结构。聚合体包含由值对象描述的实体。


为遗留系统实施DDD
不知何故,我们有一个错误的理解,即领域驱动设计只能应用于绿地项目,但这是非常罕见的。我们的大部分工作都是棕地项目。具有讽刺意味的是,棕地项目可以从一些领域驱动设计的处理中获益最大。

在棕地项目中实施领域驱动设计时,你需要的主要东西是耐心。你必须对一切都有耐心。

在这些情况下,我通常建议从绘制现有解决方案开始。所以,你有这个棕地项目,你想要发展,但在你开始考虑你想去哪里之前,你必须确定你知道你的起点。因此,你必须绘制你所拥有的那些有边界的背景。

现在,当然,这些并不是领域驱动设计的有界背景。它们不一定包含连贯的模型。但是,这些仍然是你想要改变的组件。所以你必须找到它们,映射出你所拥有的有边界的上下文。你必须绘制它们是如何相互作用的,这些集成模式。

一旦你做到这一点,下一步就是开始学习业务领域。而通常它是通过建立一种普遍的语言来完成的。在现实生活中,这很有挑战性,特别是对于棕地项目。因为如果有一种语言在公司里已经被使用了一段时间,那么要改变它就需要很多时间。

在这种情况下,我建议要做榜样,去和领域专家交谈。确保你理解他们。确保你们能用同样的语言交流。尝试找出一些已经在代码中实现的关于业务领域的错误假设。一旦你开始培养共同语言,至少自己在代码中使用它,当你与人交谈时。

这种普遍的语言要在组织中被采用需要时间。但还是那句话,要成为榜样。确保你所生产的任何东西都是通用的语言。当与人沟通时,要诉诸于泛在语言背后的原则。

为什么我们要确保每个术语只有一个意思?因为我们想消除假设。我们想消除交流中的不一致。因此,当你与人交谈时,你可以把这些原则作为一个理由,尝试改变他们使用企业术语的方式。


技术领先的智慧
将领域驱动设计的思想应用到各个地方。

思考问题和解决方案以及它们之间的联系。

当有人告诉你一个最佳实践,比如一些设计模式或技术解决方案,是绝对的最佳实践,应该到处使用时,不要相信他们。相反,把它当作一个问题的解决方案。一旦你有了这个解决方案,要问它解决的是什么问题?这一定是我的问题吗?这既适用于最佳实践,也适用于他们所说的反模式。

无论别人试图说服你或向你推销什么,都要把所有的东西当作解决某个问题的方案。识别解决方案,并寻找它所解决的问题。

Cynefin模型
当谈及评估我们正在解决的问题,我们正在处理的问题时,我喜欢使用Cynefin模型。Cynefin模型是一个决策框架,用于帮助领导者在不同类型的问题领域采取行动。让我们来讨论五个中的三个。

第一个是 "明显的"。这些是明显的问题领域。你知道你有什么问题,你也知道如何解决它。

第二个是 "复杂的"。"复杂 "意味着你有一个问题,你没有解决方案,但你知道有一个人你可以咨询,他是该领域的专家,他们会给你一个解决方案。它可以是一个人,可以是一本书,可以是谷歌搜索或其他。

第三种是 "复杂 "的领域。"复杂 "意味着我们在一个因果关系之间有一个更松散的关系。换句话说,我并不确定做某件事的结果是什么,没有书、没有外部专家能告诉我们。因此,为了做出决定,我们必须进行一个安全的实验。它不应该是大的东西,也不应该是一个巨大的工程。我们只需要做一些小的实验,以更好地了解该领域的性质。

这三个领域可以与DDD的子领域相关联。因此,"明显的",可能是 "支持"。"复杂的",我们需要咨询某人,这就是通用的子域。而 "复杂",这些是核心子域。

核心子域是复杂的,这种复杂性导致我们对做某件事的结果不那么确定。我们正在做的安全实验会影响到整个问题的格局,因为它可以引发一些突发行为。

牢记这一点是至关重要的,要区分我们可以向人咨询的东西。复杂的,需要不同的,让我们称之为思维模式来处理这样一个问题的东西。这很重要,因为在我们的文化中,我们庆祝自信。

不要害怕冲突
我在这里的建议是,不要试图逃避这样的冲突,而是要拥抱它们,因为销售东西的唯一方法是诉诸于解决方案背后的原则。

在销售领域驱动设计或任何东西时,要诉诸于这些解决方案背后的原则。为什么它们会被设计成这样?

有这种冲突的另一个好处是,它会发现你对领域驱动设计的理解在你试图销售的方式上的一些盲点。

一旦你明确了解决方案的那个领域,它将帮助你做一些进一步的研究,更好地理解它,了解也许你所遗漏的关键部分。