本文作为学习笔记,内容来自《极客时间》专栏《手把手教你落地 DDD》,如有侵权请告知,必定及时删除。
2003 年,Eric Evans 写了《领域驱动设计:软件核心复杂性应对之道》一书,正式提出了这种方法。领域驱动设计的英文是 Domain-Driven Design,所以简称 DDD。
1 DDD 基本开发过程
DDD 基本开发过程可以分为两大步骤:模型的建立和模型的实现。
- 模型的建立,又细分为捕获行为需求和领域建模。
- 捕获行为需求。也就是传统软件工程里的“获取需求”。这一步,我们要识别需求里有哪些流程、哪些功能,每个功能由什么人操作,会产生什么结果。DDD 中比较流行的一种方法,叫做“事件风暴”
- 领域建模。也就是通过建立领域模型,把需求里的主要业务知识描述清楚。DDD 的领域模型,大体上相当于传统软件工程中的分析模型。
- 模型的实现,细分为架构设计、数据库设计、编码实现。
- 架构设计。包括进程间和进程内的架构。比如说微服务设计、中台设计都属于进程间架构。而 DDD 分层架构,通常说的是进程内架构。
- 然后就可以根据领域模型进行数据库设计,最后是代码实现。
2 建立模型
2.1 捕获行为需求
捕获行为需求的方法有好几种,在传统的软件工程中,最常用的方法是“用例”,也就是 Use Case。但是,Eric Evans 在《领域驱动设计》这本书里,并没有规定捕获行为需求的具体方法。直到 2013 年,一位叫 Alberto 的 DDD 专家提出了“事件风暴”,也就是 Event Storming。
2.1.1 识别领域事件
所谓领域事件,就是在业务过程中,业务人员要关注的那些已经发生的事儿。比方说,对于电子商务系统,订单已提交、商品已签收等等,都是领域事件。实际上,领域事件表示的是,业务流程中每个步骤引发的结果。事件风暴的作者认为,从结果入手来梳理需求,比从操作入手,更容易把业务想清楚。事件风暴中的“事件”两个字就来源于领域事件。
另外,咱们还要注意领域事件的命名,如果套用英语的语法来说,一般是完成时 + 被动语态。比如说,订单已提交,这个“已”字就是完成时,代表已经发生的事情。而订单已提交也可以说成订单已“被”提交,实际是被动语态,只不过一般把被字给省掉了。
假设我们要做一个项目管理的系统,可以识别出来的领域事件,如下图所示:
2.1.2 识别命令
所谓命令(command),就是引发领域事件的操作,我们可以通过分析领域事件得到。除了识别出命令本身以外,我们通常还要识别出谁执行的命令,以及为了执行命令我们要查询出什么数据。
- “合同已签订”是2.1.1中识别出来的领域事件。
- “签订合同”则代表本小节中表述的命令。
- “销售人员”代表执行这个命令的主体,代表的是一个角色,而不是具体的个人。
- “客户”表示在执行该命令时,需要查询出来的数据。
2.1.3 识别领域名词
这里说的领域名词,是从命令、领域事件、执行者、查询数据里找到的名词性概念。例如,对于签订合同这个命令而言,受到影响的名词性概念是“合同”。
2.1.4 捕获行为需求总结
捕获行为需求三个阶段的作用:
- 领域事件:一般会对应一段代码逻辑,这段逻辑可能会最终改变数据库中的数据。另外,在事件驱动的架构中,一个领域事件可能会表现为一个向外部发送的异步消息。
- 命令:领域建模时,我们可以通过对命令的走查(walkthrough),细化和验证领域模型。在实现层面,一个命令可能对应前端的一个操作,例如按下按钮;对于后端而言,一个命令可能对应一个 API。
- 领域名词:其实识别领域名词的最终目的是要找到领域模型中的对象,一个名词有可能只是一个对象充当的角色,或者对象的属性,还有些名词需要经过合并或拆解后,才是合理的领域对象,而这些需要等到领域建模时才能真正搞清楚。
事件风暴的应用场景:
- 事件风暴主要应用在需求不清晰,或者理解不统一的情况下,通过协作的方式理清业务、达成一致,所以通常对于新项目比较适用。
- 至于遗留系统改造的情况,如果这个系统的知识已经流失得很严重,那么事件风暴仍然是有意义的。但如果大家对这个系统的业务知识很清楚,只是要进行架构改造,那么事件风暴的意义就不大了。
- 如果你的项目里还没有正规的、令人满意的捕获行为需求方法,那就可以使用事件风暴。
2.2 领域建模
领域建模主要有两个目的:
- 将知识可视化,准确、深刻地反映领域知识,并且在业务和技术人员之间达成一致;
- 指导系统的设计和编码,也就是说,领域模型应该能够比较容易地转化成数据库模式和代码实现。
而我们建立领域模型,主要是要识别领域对象(domain object),领域对象之间的关系,以及领域对象的关键属性,必要的时候还要将领域对象组织成模块。
2.2.1 识别领域对象与对象间关系
-
初步识别实体
首先,你可以先假定每个领域名词都是一个实体,把它们用类的符号画出来。
其实,在领域建模阶段,我们主要关注的是实体和它们之间的关系。如果实体的名字已经能清晰说明实体的含义,那我们就不需要加属性了。如果名字还不足以充分表达含义,我们可以写几个关键属性,来辅助说明。
-
识别“一对一”关联
租户和企业具有一对一的关系。在有些情况下,一对一的两个实体确实是可以合并的。这取决于这两个概念的关注点是否相同。
-
识别“一对多”关联
一个组织里面,可以有多个员工。同时呢,一个组织有且仅有一个上级组织,但是一个组织可以有多个下级组织。可以用下面的图来表示这种关联关系。
- 增加约束
凡是约束,必须在程序中的某个地方进行实现,约束也是一种业务规则。
例如,我想表达,一个开发小组的上级只能是开发中心,而不是一个开发小组下面有多个开发中心,就可以在图中添加约束
-
识别“多对多”关联
如果我想表达,一个员工既可以是管理员,又可以是人事人员,同时对于人事人员这个岗位,又可以有多个员工。那么我就可以用下面的图,来表达员工和岗位之间的“多对多”关联。
2.2.2 划分模块
如果2.2.1小节中的领域很多,并且领域的关联关系错综复杂,就会导致领域模型难以理解,造成业务与技术人员的认知过载。我们可以把模型中的业务概念组织成若干高内聚的模块(module),而模块之间尽量低耦合。
有了模块,我们就可以从两个层面理解模型:宏观层面与微观层面
宏观层面只关心模型中有哪些模块,以及模块间的依赖关系,不关心模块内部的细节。为了达到这个目的,我们可以画出更宏观的包图。像下面这样:
这里需要区分依赖与关联:
- 关联表示的是数据上的导航关系。例如,当我们说组织和员工之间具有一对多关联的时候,就意味着,由组织可以找到下面的员工,由员工也可以找到所属的组织。
- 依赖表示的意思更为广泛。如果 A、B 两个元素,有了 A 才能有 B,那么就可以说 B 依赖于 A。
微观层面,也就是深入到模块内部,了解实体和关联等等的细节。微观层面的输出就是各个模块内部对应的领域模型。
通过这种分而治之的方法,我们可以在一定程度上管理复杂性,解决认知过载的问题。
2.2.3 完善领域模型
-
完善业务规则。在领域建模的过程中,我们可以识别出来很多需求移交过程中,没有考虑到的点,可以把这些没有考虑到的点,连同已经识别到的,一起整理成一张业务规则表格。
- 建立词汇表。主要有两个作用:
- 通过词汇表来规范领域模型中的词汇。用于统一语言。
-
可以用于后续的编程命名。
3 设计与实现
领域建模与传统的需求分析区别在于:DDD 强调领域模型要兼顾业务和技术两个视角,避免了传统软件工程中分析模型和设计模型相互割裂的风险。可以概括为:
- 领域模型要和业务需求一致。
- 系统实现要和领域模型一致。
- 领域模型中的每个元素,都应该通过某种方式在系统实现中有所体现。
- 统一沟通业务人员与技术人员的语言。
3.1 数据库设计
经过第2节的建立模型以后,就可以根据模型来设计数据库了,这块内容目前工作中没有涉及,先暂时给自己留个TODO吧,后面用到了再来补充这块笔记。
3.2 分层架构
我们为什么要采用分层架构呢?原因就是为了避免“大泥球”式的代码。“大泥球”式的代码指各种业务逻辑都混杂在一起,通常会出现以下几个问题:
- 首先,很难单独识别出反映领域逻辑的代码,从而难以保证与领域模型的一致性。
- 其次,应该内聚的逻辑分散在不同地方,应该解耦的逻辑又混在一起,造成代码难以理解。
- 再次,修改业务代码,可能会影响技术代码,修改技术代码,又可能会影响业务代码,造成代码很难维护。
- 最后,经过一段时间的维护,代码变得日益混乱,代码中出现大量重复和不一致,经常出现质量问题。
分层架构就是解决大泥球问题的一种最佳实践,可以有两种等价的画法,一种由内而外,另一种自下而上,如下所示:
- DDD 对代码架构最核心的要求就是要将领域层分离出来。领域层封装了领域数据和逻辑,我们前面的领域模型所对应的代码,主要就体现在领域层。只有将领域层独立出来,才能保证与领域模型的一致,也才能让领域层独立演化。这里叫Domain层。
- 领域层封装的逻辑通常是细粒度的,并不适合直接作为 API 暴露给外部。另外,还有一些不属于领域层的横切关注点,比如像事务控制,应该单独处理。所以,我们往往要在领域层外面再加一层,也就是应用层。也就是我们通常写的Service层。应用层本身并不包含领域逻辑,而是对领域层中的逻辑进行封装和编排。
- 除了业务功能之外,程序里还有另一个重要的关注点——输入输出技术。我们的系统要和外界打交道,可以通过不同技术来实现,比如 Restful API、 RPC,以及传统的 Web 页面等等。我们在应用层外面再加一层,专门处理输入输出技术。也就是我们熟悉的Controller层。
- 我们需要一种适配器把具体的持久化技术和Service应用层以及Domain领域层隔离开,而仓库就充当了这种适配器。我们的项目里面,通常把这一层叫做Repository。用于实现访问数据库、下游接口等读、写数据逻辑。
- 到现在为止,我们已经讲了 DDD 分层架构中最主要的几层,但还有另外一些代码没有考虑。比如说,我们写了一些用于字符串和日期处理的工具类,这些工具可能被上面说的任何一层调用。事实上,我们可以认为这些代码和前面说的各层根本不在同一个维度,它们是对各层代码起到公共的支撑作用的。也就是Common层。
这里呢,我对原文中的分层架构做了一定的修改,使其更适合自己目前手上的项目。如下图所示:
如果一个逻辑需要和领域专家讨论才能确认的,就是领域逻辑;如果领域专家根本不感兴趣的,多半就是应用逻辑。