DDD 基础理论

DDD是什么

MVC模式

那我们就先看看没有DDD,软件开发都是怎么做的?

拿大家熟悉的MVC模式举例,这里的Model是数据库模型,业务逻辑在Controller中实现(一般会由Service来辅助实现),View层主要负责视图展示。

MVC模型

对于业务逻辑不复杂的软件开发,MVC是简单高效的方法。但是随着业务逻辑愈来愈复杂,MVC会开始力不从心。主要体现在这几个方面:

  1. MVC模式仅仅反应了软件层面的架构,它不包含业务语言,无法使用该设计直接和业务对话。
  2. MVC模式天然切割了数据和行为,然后用数据库实现数据,用服务实现行为,容易造成需求的首尾分离。
  3. 缺乏明确的边界划分,至少在顶层设计层面没有边界划分的规范要求,更多地是靠技术负责人根据经验进行划分,大规模团队协作容易出现职责不清晰、分工不明确

传统的开发模式或多或少都存在上面的问题。

DDD定义

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

模型在领域驱动设计中,有三个重要用途:

  1. 通过模型直接反映软件实现的结构。
  2. 以模型为基础形成团队的统一语言。
  3. 把模型作为精粹的知识,用于传递。

DDD的价值

因此DDD能够带来这几方面的价值

统一语言:

团队(业务方、产品、设计、技术等)在一个限定的上下文中有意识地形成对事物统一的描述,从而形成统一的概念(模型)。统一语言用于需求文档、PRD文档、系分文档、代码以及日常沟通中,统一的概念和术语可以极大地提升沟通效率和工作效率。

面向业务建模:

领域模型和数据模型分离,业务复杂度和技术复杂度分离。DDD聚焦于领域模型,将技术实现细节从模型中剥离出来,能够更好地降低业务和技术的耦合度。

边界清晰的设计方法:

通过对需求的识别及分类,划分出领域、子域和限界上下文,进而指导团队成员分工协作,从而做到将复杂的问题分而治之地解决。

业务领域的知识沉淀:

通过模型与软件实现关联,统一语言与模型关联,反复论证和提炼模型,使得模型与业务的真实世界保持一致,从而促使业务知识通过模型得以传递和沉淀

DDD的基本概念

统一语言

团队(业务方、产品、设计、技术等)在一个限定的上下文中有意识地形成对事物统一的描述,从而形成统一的概念(模型),这些统一的描述和统一的概念就是统一语言,统一语言主要源自于领域模型的概念与逻辑,作为对业务维度的补充和展开,也会将限界上下文、系统隐喻等纳入到统一语言中。

以“智慧课堂”商业模式中的这句话“用户可以选择自己感兴趣的专栏进行付费订阅”,进行简单的建模。

用户订阅模型

根据这个模型,我们可以形成统一语言:

  • 用户(User)是指所有在“智慧课堂”注册过的人。(来自领域模型概念)
  • 订阅的专栏(Subscription)是指用户付费过的专栏。(来自领域模型概念)
  • 用户可以订阅多个专栏。(来自领域模型逻辑)
  • 订阅。(来自限界上下文)

通过定义与解释,我们使这些词语在其所使用的上下文中没有歧义。再通过这些基础词汇,去描述业务的行为或者规则,慢慢就可以将其确立为跨业务与技术的统一语言了。统一语言是在使用中被确立的。

有了统一语言后,我们就可以很方便的描述需求:

用户(User)可以查阅自己订阅过的专栏(Subscription),也可以查看其中的教学内容。

也可以用来描述测试用例:

当用户(User)已购买过某个专栏(Subscription),那么当他访问这个专栏时,就不需要再为内容付费。

这里仅仅举了两个使用统一语言的场景,当所有工种角色都接受它,用它去描述业务和系统的时候,它才会成为真正的统一语言。

战略设计

在DDD中可以分为战略设计和战术设计,各自包含的内容如下图所示:

战略设计&战术设计

战略设计指的是对整个领域进行分析和规划,确定领域中的概念、业务规则和领域边界等基础性问题。在战略设计中,需要对领域进行全面的了解和分析,探究业务的规则和本质,并且需要考虑到领域的未来发展趋势和可能的变化。领域、子域和限界上下文属于战略设计的范畴。

领域

领域(Domain)其实就是一个组织所要做的整个事情,以及这个事情下所包含的一切内容。这是一个范围概念,而且是面向业务的(注意这里不是面向技术的,更不是面向数据库的持久化的),每个组织都有自己的人员、规则和流程,当你为该组织开发软件的时候,你面对的就是这个组织的领域。

例如,在“智学公司”的“智慧课堂”中,领域就是知识付费领域。

子域

子域是指在一个大的领域中,可以进一步划分出来的独立的业务子领域,它们有着自己的业务概念、规则和流程等。

例如,在“智慧课堂”中,子域可以有订阅域、金融域、专栏域等。

为了区分重要性的不同,我们又会将子域划分成核心域、通用域以及支撑域。

核心域

决定公司和产品核心竞争力的子域就是核心域,它是业务成功的主要因素。核心域直接对业务产生价值。例如,在“智慧课堂”中,订阅域就是核心域。

通用域

没有太多个性化的诉求,同时被多个子域使用的、具有通用功能的子域就是通用域。通用域间接对业务产生价值。例如,在“智慧课堂”中,权限域、登录域就是通用域。

支撑域

支撑其他领域业务,具有企业特性,但不具有通用性。支撑域间接对业务产生价值,例如,在“智慧课堂”中,专栏域、评论域就是支撑域。

限界上下文

限界上下文就是业务边界的划分,这个边界可以是一个子域或者多个子域的集合。如何进行划分,一个行之有效的方法是一个界限上下文必须支持一个完整的业务流程,保证这个业务流程所涉及的领域都在一个限界上下文中。限界上下文是微服务拆分的依据,即每个限界上下文对应一个微服务。

例如,在“智慧课堂”中,可以有一个限界上下文叫“专栏订阅上下文”,它就包含了订单域和订阅域。

战术设计

战术设计则是在战略设计的基础上,对领域中的具体问题进行具体的解决方案设计。战术设计关注的是领域中的具体情境和场景,需要针对具体的问题进行具体的分析和设计,以满足业务需求。实体、值对象、聚合、工厂、资源库、领域服务和领域事件就属于战术设计的范畴。

实体(ENTITY)

定义:实体是拥有唯一标识和状态,且具有生命周期的业务对象。实体通常代表着现实世界中的某个概念,实体与领域模型密切相关,它是领域模型中多个属性、操作或者行为的载体。例如:在“智慧课堂”中,专栏、课程文章、订阅都是实体。

实体的业务形态

实体能够反映业务的真实形态,实体是从用例提取出来的。领域模型中的实体是多个属性、操作或行为的载体。

实体的代码形态

  • 失血模型:模型仅仅包含数据的定义和getter/setter方法,业务逻辑和应用逻辑都放到服务层中。这种类在Java中叫POJO。
  • 贫血模型:贫血模型中包含了一些业务逻辑,但不包含依赖持久层的业务逻辑。这部分依赖于持久层的业务逻辑将会放到服务层中。
  • 充血模型:充血模型中包含了所有的业务逻辑,包括依赖于持久层的业务逻辑。
  • 胀血模型:胀血模型就是把和业务逻辑不想关的其他应用逻辑(如授权、事务等)都放到领域模型中。

实体的运行形态:

实体有唯一ID,当我们在流程中对实体属性进行修改,但ID不会变,实体还是那个实体。

实体的数据库形态:

实体在映射数据库模型时,一般是一对一,也有一对多的情况。

结合团队以及兄弟团队的实践,建议实体采用贫血模型,实体和领域服务共同构成领域模型。这样可以使得实体具备业务知识,但又不至于太过臃肿。

值对象(VALUEOBJECT)

定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。值对象没有唯一标识,没有生命周期,不可修改,当值对象发生改变时只能替换。

值对象的业务形态

大多数情况下实体具有很多属性,这些属性一般都是平铺,但有的属性进行归类和聚合后能够表达一个业务含义,就将这些属性封装到一起形成值对象,从而方便沟通而无需关注细节,因此可以说值对象就是用来描述实体的特征。当然实体的单一属性也是值对象。

值对象的代码形态

值对象有两种:单一属性的值对象,例如字符串、整型、枚举等;多个属性的值对象,这时候设计成class,包含多个属性,但是没有ID,值对象中可以嵌套值对象。例如商品实体下的航段就是一个值对象。航段是描述商品的特征,航段不需要ID,可以直接整体替换。商品为什么是一个实体,而不是描述订单特征,因为需要表达谁买了什么商品,所以我们需要知道哪一个商品,因此需要ID来标识唯一性。

我们看一下下面这段代码,person这个实体有若干个单一属性的值对象,比如Id、name等属性;同时它也包含多个属性的值对象,比如地址address。

实体&值对象

值对象的运行形态:

值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。当我们修改地址时,从页面传入一个新的地址对象替换调用person对象的地址即可。如果我们把address设计成实体,必然存在ID,那么我们需要从页面传入的地址对象的ID与person里面的地址对像的ID进行比较,如果相同就更新,如果不同先删除数据库在新增数据。

值对象的数据库形态:有两种方式嵌入式和序列化大对象。

案例1:以属性嵌入的方式形成的人员实体对象,地址值对象直接以属性值嵌入人员实体中。

当我们只有一个地址的时候使用嵌入式比较好,如果多个地址必须有序列化大对象,同时可以支持搜索。

嵌入式值对象

案例2:以序列化大对象的方式形成的人员实体对象,地址值对象被序列化成大对象Json串后,嵌入人员实体中。支持多个地址存储,不支持搜索。

序列化值对象

值对象的优势和局限:

  1. 简化数据库设计,提升数据库操作的性能(多表新增和修改,关联表查询)。
  2. 虽然简化数据库设计,但是领域模型还是可以表达业务。
  3. 序列化的方式会使搜索实现困难(通过搜索引擎可以解决)。

聚合和聚合根

多个实体和值对象组成的我们叫聚合,聚合的内部一定的高内聚。这个聚合里面一定有一个实体是聚合根。聚合根的作用是保证内部的实体的一致性,对外只需要对聚合根进行操作。

聚合是一种更大范围的封装,把一组有相同生命周期、在业务上不可分隔的实体和值对象放在一起考虑,只有根实体可以对外暴露引用,这个根实体就是聚合根,聚合也是一种内聚性的表现。

领域、子域、限界上下文、聚合都是用来表示一个业务范围,那他们的关系是怎样的呢?领域、子域、限界上下文属于战略设计,而聚合属于战术设计,聚合的范围是小于前三者的,范围大小图如下:

领域驱动范围划分

工厂

工厂是一种重要的设计模式,DDD只是拿来主义,用到了工厂。考虑使用工厂的主要动机:

将创建复杂对象和聚合的职责分配给一个单独的对象,该对象本身并不承担领域模型中的职责,但是依然是领域设计的一部分。工厂应该提供一个创建对象的接口,该接口封装了所有创建对象的复杂操作过程,同时,它并不需要客户去引用那个实际被创建的对象。对于聚合来说,我们应该一次性地创建整个聚合,并且确保它的不变条件得到满足。

资源库

资源库(Repository)是一种模式,用于封装数据访问逻辑,提供对数据的持久化和查询。它旨在将数据访问细节与领域模型分离,使领域模型更加独立和可测试。资源库提供了一种统一的接口,使得领域模型可以与不同的数据存储方式(如关系数据库、文档数据库、内存数据库等)进行交互,同时也提供了一些查询操作,以便在领域层中进行数据查询。如果我们使用MyBatis的话,Mapper就是对资源库的一种实现。

领域服务

有些领域中的动作看上去并不属于任何对象。它们代表了领域中的一个重要的行为,不能忽略它们或者简单地把它们合并到某个实体或者值对象中。当这样的行为从领域中被识别出来时,推荐的实践方式是将它声明成一个服务,这个服务就是领域服务。

例如,在“智慧课堂”中,订阅(Subscribe)行为是一个非常重要的领域概念,它涉及到订单创建、支付、增加订阅记录等和多个实体相关联的操作,将该行为放到任何一个实体中都不合适,在这种情况下,将“订阅”识别为领域服务是比较合适的。

领域事件

领域事件是发生在领域中且值得注意的事件。而领域事件通常意味着领域对象状态的改变。领域事件在系统中起到了传递消息、触发其他动作的作用,是解耦领域模型的重要手段之一。我们往往利用消息队列来传递领域事件。

例如,在“智慧课堂”中,当用户订阅了一个专栏后,会产生一个“专栏订阅成功”的领域事件,用户成长域会根据这个领域事件决定增加用户积分。

为什么需要DDD

  • 复杂系统设计:系统多,业务逻辑复杂,概念不清晰,有什么合适的方法帮助我们理清楚边界,逻辑和概念?
  • 多团队协同:边界不清晰,系统依赖复杂,语言不统一导致沟通和理解困难。有没有一种方式把业务和技术概念统一,大家用一种语言沟通。例如:航程是大家所理解的航程吗?
  • 设计与实现一致性:PRD,详细设计和代码实现天差万别。有什么方法可以把业务需求快速转换为设计,同时还要保持设计与代码的一致性?
  • 架构统一,可复用资产和扩展性:当前取决于开发的同学具备很好的抽象能力和高编程的技能。有什么好的方法指导我们做抽象和实现。

DDD的价值

  • 边界清晰的设计方法:通过领域划分,识别哪些需求应该在哪些领域,不断拉齐团队对需求的认知,分而治之,控制规模。
  • 统一语言:团队在有边界的上下文中有意识地形成对事物进行统一的描述,形成统一的概念(模型)。
  • 业务领域的知识沉淀:通过反复论证和提炼模型,使得模型必须与业务的真实世界保持一致。促使知识(模型)可以很好地传递和维护。
  • 面向业务建模:领域模型与数据模型分离,业务复杂度和技术复杂度分离。

DDD架构

DDD大体结构

  • 用户接口层:调用应用层完成具体用户请求。包含:controller,远程调用服务等
  • 应用层App:尽量简单,不包含业务规则,而只为了下一层中的领域对象做协调任务,分配工作,重点对领域层做编排完成复杂业务场景。包含:AppService,消息处理等
  • 领域层Domain:负责表达业务概念和业务逻辑,领域层是系统的核心。包含:模型,值对象,域服务,事件
  • 基础层:对所有上层提供技术能力,包括:数据操作,发送消息,消费消息,缓存等
  • 调用关系:用户接口层->应用层->领域层->基础层
  • 依赖关系:用户接口层->应用层->领域层->基础层

事件风暴

参与者

除了领域专家,事件风暴的其他参与者可以是DDD专家、架构师、产品经理、项目经理、开发人员和测试人员等项目团队成员。

事件风暴准备的材料

一面墙和一支笔。

事件风暴的关注点

在领域建模的过程中,我们需要重点关注这类业务的语言和行为。比如某些业务动作或行为(事件)是否会触发下一个业务动作,这个动作(事件)的输入和输出是什么?是谁(实体)发出的什么动作(命令),触发了这个动作(事件)…我们可以从这些暗藏的词汇中,分析出领域模型中的事件、命令和实体等领域对象。

实体执行命令产生事件。

业务场景的分析

通过业务场景和用例找出实体,命令,事件。

领域建模

领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。领域模型利用限界上下文向上可以指导微服务设计,通过聚合向下可以指导聚合根、实体和值对象的设计。

如何建模

事件风暴领域建模

  • 用例场景梳理:就是一句话需求,但我们需要把一些模糊的概念通过对话的方式逐步得到明确的需求,在加以提炼和抽象。
  • 建模方法论:词法分析(找名词和动词),领域边界
  • 模型验证

事件风暴小结

事件风暴法通过头脑风暴发现领域事件,以“对于事件的响应”为主要维度寻找事件间的关联,它是一种简单明快的事件建模方法。

但是事件风暴也有一些不足之处,一方面是事件风暴的模式偏重,需要不同角色的成员集体参与,涉及的人员多、流程长。另一方面,也是最关键的一点,事件风暴的成功关键在于收敛逻辑。

事件风暴小结

在发散阶段,所有参与者可以天马行空。不过这样的方法在产生有效信息的同时,也会产生大量的噪音。但是在收敛阶段,则会按照某一逻辑主线,合并相似概念,过滤无用信息。那么我们可以很容易地想到,如果主持人采用不同的逻辑去收敛事件,最后获得的结果也可能不尽相同。

因此事件风暴法极度依赖主持人的经验与判断,最终结果自然就会存在一定的随意性。这也使得事件风暴法变成了那种“一学就会,一用就废”的方法。有经验的老手越用越顺手,而初学者往往不得要领。

既然最终事件流的质量取决于收敛逻辑,那么我们为什么不直接从收敛逻辑出发,通过引导 - 分析直接获取事件流呢?的确可以这么做,而四色建模法也正是这样一种从收敛逻辑出发的强分析法。

参考资料:

领域驱动设计DDD|从入门到代码实践——阿里开发者

迄今为止最完整的DDD实践——阿里开发者