1.1 领域驱动设计是什么

DDD (领域驱动设计)不是一种编码技术,也不是一种特定的编码风格。

DDD是一种模型驱动的设计方法:通过领域模型(Domain Model)捕捉领域知识,使用领域模型构造更易维护的软件。

下面我们将通过与传统软件设计方法中的分析模型的对比来探索一下DDD的基本要素。

1.2 分析模型所面临的问题

传统软件研发的协作流程

1.2.1 流程

在传统的软件开发流程的理想场景是:由需求分析师同客户沟通,待理解客户需求后使用UML将客户的需求表达出来;然后由软件设计师根据分析模型设计出软件的设计模型,软件研发人员再根据设计模型开发对应的软件。

1.2.2 问题

这样的设想其实是一个好的思路,但执行的时候会存在三个问题:

  • 重要角色的丢失。 一般的团队中压根就不存在需求分析师,演化到最后就会变成:业务人员和开发者之间的直接对话,因为二者之间没有共同的语言,所以大家会倾向于寻找一个载体,在这个载体的基础上进行对比、沟通,这个载体通常是同类产品,或是目前已经做好的部分系统界面。虽然拥有了一些载体,但是因为研发与业务之间的天然沟壑,难免会存在理解上的问题,导致沟通困难。这时候,一个懂业务、会沟通的研发人员就会显得尤为重要。
  • 不同角色之间的天然隔阂。 我们知道,模型是对业务的一种抽象,既然是一种抽象,必然会舍弃掉一部分细节,业务人员无法直接阅读和理解模型。而分析模型是需求分析师建立起来的,主要目的其实也只是为了后续的设计模型提供依据,所以分析模型可能并不是完全站在客户角度出发的,这时候模型本身能够给予到的信息是有限的,这导致双方无法直接围绕着分析模型展开讨论。
  • 模型与实现不一致。 在软件的开发过程中,分析模型和设计模型存在混用的情况,要么没有分析模型,要么没有设计模型,甚至两者都没有,并且,同时维护两个模型的成本非常高;随着时间的推移,模型无法反映出真实的业务情况,此时有模型和没有模型其实区别不大了,而系统和业务之间是相关约束的,掌握不了代码实现,需求分析师和客户便无法把控需求。

可以发现,尽管我们最初的设想是好的,但是在实际操作过程中,有太多因素让我们无所适从,这些因素让我们的软件会逐渐走向不可控。

1.3 DDD是如何做的?

DDD协作流程

来看看DDD是如何解决分析模型所存在的问题的:

  • 重要角色的丢失。DDD是一种设计方法,而不是一个框架,所以它并不会要求搭配某些特有角色,那么也就不会因为团队中缺少某个角色而导致流程变得复杂。
  • 不同角色之间的天然隔阂。既然模型是抽象的,不好理解,那如果开发者和业务人员共同创造出一种通用的“语言”,相当于在中间充当了一位翻译,专门用来解读领域模型,这样就能够打破不同角色之间的隔阂了。
  • 模型与实现的不一致。市面上DDD的书籍都会强调“模型就是代码,代码就是模型”,这就要求领域模型和代码实现之间时刻保持关联,模型和代码任何一方需要修改,另一方都要做出响应调整。你会发现,如果我们都不去维护模型和代码之间的关联,软件仍然随时会失控,但这是一个没有办法回避的问题,因为关联只能由人来操作的,而人一定会出错。所以其实DDD没有完全解决”不一致“的问题,DDD的解决方案是:1.减少了一个模型,降低了同时维护多个模型的成本;2.将模型与代码的一致性问题搬上台面,让人重视这个问题。

此外,我们发现,在传统的软件研发流程中,一般只存在业务侧和研发测两层,由于缺乏一个中间层,所以需求和研发之间会存在一些天然的隔阂。而领域驱动设计会通过统一语言+领域模型形成了一个中间的领预测的过渡层,通过中间层强化了沟通效率和表达效果。

1.4 一循环,两关联

这是DDD的关键,在解释之前我们先来了解一下DDD的路径

1.4.1 DDD的两条重要路径

DDD同敏捷一样,都是一种通过不断迭代、反馈、试错的方法。

但敏捷着重于强调价值的交付,DDD强调通过模型来驱动软件的设计。

所以在DDD中两条路径都十分重要:

  1. 需求 -> 模型
  2. 模型 -> 代码实现

根据之前所言,我们实际上是用统一语言来讨论需求,所以实际情况会是:

  1. 需求 -> 模型 -> 统一语言 -> 需求 -> 模型 ....
  2. 模型 -> 代码实现

1.4.2 一循环

第一条重要路径是一个循环:

通过将需求凝练到领域模型中,然后再从领域模型中提取统一语言,后续再使用统一语言来进一步讨论需求。

Eric Evans 提倡了一种叫做知识消化(Knowledge Crunching)的方法帮助我们去提炼领域模型。知识消化是一个反复咀嚼的过程,是通过不断迭代循环,最后得到相对较好的领域模型的方法。

所以,存在这样一个循环是一种不断讨论、不断迭代、不断试错后的一种很自然的结果。

DDD过程中的循环

1.4.3 代码关联模型

第二条路径是让模型关联代码实现,这很好理解,让领域模型能够真正意义上去代表代码。这样我们在沟通需求的时候才不用去关心那些琐碎的技术细节(事实上没人能够完全记得住所有的技术细节),只有让模型=代码,大家花了很多时间的沟通结果不会因为技术实现的问题而付之东流。

让代码关联模型,这是知识消化法循环得以进行的前提条件,一旦二者关联不上,循环就会被迫中止。

1.4.4 问题

额,这一节的标题是”一循环,两关联“,上面👆🏻已经讲了”需求-统一语言-领域模型“三者之间的关联,以及”代码关联模型“,还有一个关联呢?

不要急,让我们重新审视一下”一循环“。

循环中存在三个元素:需求、统一语言、领域模型。两两关系分别是:

  1. 需求 -> 领域模型;
  2. 领域模型 -> 统一语言;
  3. 统一语言 -> 需求;

这三个当中,哪一个应该关联起来呢?

  • 首先排除掉第三点。因为统一语言仅仅是用于讨论需求的一种工具,二者无须直接关联。

  • 再来看第一点”需求 -> 领域模型“,需求其实是没办法同领域模型做关联划上等号的。因为需求一定是远远大于领域模型本身,需求的持续变化的,领域模型是需要不断地完善的,让领域模型反映需求本身是我们的目标,但是他们无法完全保持一致。

  • 所以答案就出来了,最后的一个关联的”领域模型 -> 统一语言“。

1.4.5 模型关联统一语言

模型关联统一语言也是知识消化的必要条件,如果统一语言和模型不匹配,即统一语言无法解释模型。

此时,循环亦会中止。

统一语言与模型不变匹配的场景

二、 DDD的重要概念

DDD中有两个重要特征。上面已经提到一个重要的特征:统一语言。

2.1 统一语言(Ubiquitous Language)

统一语言是团队中共享的通用语言,领域专家和开发者使用统一语言进行交流。

统一语言也是需要不断演化改变的。

通用,并不是万能的,统一语言作用于仅限于某个“领域”。

下面再简单说一下另外一个重要特征:限界上下文。

2.2 限界上下文(Bounded Context)

DDD是基于领域模型驱动的设计,同领域相关的问题都应该关注。

领域相关的问题有:领域之间的边界、领域的聚合、交互等。所以领域的划分必然会成为DDD的重要关注点,DDD中采用限界上下文来划分显式的边界。一个领域就作用在一个限界上下文中,统一语言的作用于也是在一个限界上下文内,领域之间的交互也是不同限界上下文之间的交互。

现在,我们暂时不需要对它关心太多,只要知道它是一种边界的概念就可以了。

2.3 战术设计和战略设计

DDD的书籍总是会看到这两个词语。

  • 战略设计。主要考虑如何将这个复杂的业务需求合理的分成多个部分,从而分而治之,属于一种战略的设计。战略设计会受到具体业务的影响,重点关注最重要的领域。
  • 战术设计。关注领域内部的设计,比如代码如何分层、如何保障代码的可读性、如何更好地表达模型等等。

三、总结

3.1 领域驱动设计的作用

  • 通过模型反映软件实现(Implementation)的结构;
  • 以模型为基础形成团队的统一语言(Ubiquitous Language);
  • 把模型作为精粹的知识,以用于传递。模型是抽象出来的,比代码更简洁,因而有更低的传递成本;

3.2 两循环一关联

  • 关联模型与软件实现
    这是知识消化可以顺利进行的前提与基础。它将模型与代码统一在一起,使得对模型的修改,就等同于对代码的修改。

  • 基于模型提取统一语言
    将业务方变成模型的使用者。那么通过统一语言进行需求讨论,实际就是通过模型对需求进行讨论。

  • 持续精炼模型(一循环)

    通过知识消化法,让“需求-模型-统一语言”三者在循环中统一,不断从需求中提炼知识到模型中。

参考资料

  • 《实现领域驱动》Vaughn Vernon
  • 《如何落地业务建模》八叉