DDD学习笔记

210 阅读21分钟

基本概念

核心理念:同构

DDD 的核心理念,其实就是试图给出一套方法框架,告诉我们的需求分析师、架构设计师、程序员怎么去尽最大可能地将“真实物理世界同构化映射为“虚拟代码世界

整体工作流程图

image.png

业务用例规格书

用 UML 描述需求时附带的一份文档。与“业务用例识别”只是 UML 图形列出用例不同,它是对业务用例的一个补充性文字说明,一般长成下图这个样子:

image.png

服务契约设计

对每个限界上下文“北向网关”(即“应用服务”层)需要对外输出那些“可被调用”的服务接口,这一般是这么得到的:

  • 首先,为每个业务用例画 UML 服务序列图。 对每个 UML 业务用例,根据用例规格说明的内容,将其转化为 UML 服务序列图。例如,针对上面的“加商品到购物车”业务用例,我们画出如下的 UML 服务序列图:

img

基于上图,我们就发现“订单上下文”需要给出 2 个服务(即被调用箭头指出来的服务):保存购物车、查询购物车信息,而“商品上下文”需要提供“获取商品信息”服务。当然,需要说明的是:有些情况下,业务用例如此简单,以至于并不需要画 UML 序列图就能够识别出来服务列表——比如:业务用例“查看商品详情”很明显只有“商品上下文”的一个服务“获取商品详情“。

  • 其次,按业务用例给出服务契约表。 将识别出来的服务接口,确定服务方法签名、识别操作类型、确定实现模式(客户/服务端模式、消息订阅模式等),并将最终结果汇总到如下表所示的“服务契约表”中:

img

  • 最后,将整个限界上下文的服务契约表进行汇总。 汇总后的服务契约表,就可以作为限界上下文的“代码开发工作范围”了。
目录结构

我们再来看看使用 DDD 设计后,新的代码结构长什么样。下面是新代码的结构截图(同样注意下面的 1~8 标号):

img

img

对上面的代码标号位置,我来逐个解释如下(需要说明的是:这里目录排序是 IDEA 开发工具自动按字母顺序排序,不是代码设计先后顺序):

  • 标号 1 位置: 这里放的是边缘层(edge)代码。由于“群买菜”小程序前端界面已经开发完成,并且这是一个前后端分离项目,前端代码我并没有打算修改,所以这里就多了个“界面适配”的代码工作。一般来说,这种代码就叫“边缘层”。边缘层放的代码,都是类似这种为了前端界面适配、第三方系统接口适配之类的代码。这种代码,也可以叫做“为前端提供的后端”(Backend for Frontend, BFF)。理论上,这种 BFF 层的代码,可以由前端团队开发的,我可以选择技术栈是 Node.js,使用 js 或 ts 语言进行开发。
  • 标号 2 位置: 这里显示的是“基础层”(foudation)。在 DDD 的系统架构中,限界上下文(具体概念介绍见后面,这里你只需要理解为它类似于子系统或业务模块划分就好)是可以根据“业务子域”不是核心层,而分为“基础层”和“业务价值层”。一般来说,“业务价值层”对应到最核心的业务模块,是一个软件系统的核心竞争力所在,是需要严格按照 DDD 的理念进行战术设计、并采用测试驱动开发模式、投入最懂业务的程序员去工作的;而“基础层”一般都是非核心业务模块,比如:业务相关基础类、工具类、伴生系统的对接等——需要注意的是: “基础层”不是“基础资源层” ,基础层指的是业务模块处于非核心地位、而基础资源指的是数据库、中间件这些技术组件。
  • 标号 3 位置: 这里显示了多个限界上下文,都是以 xxxcontext 这样的目录取名。在“基础层”和“业务价值层”中,都会出现多个“限界上下文”。每个限界上下文可以分离到不同的项目团队去负责、甚至分离到不同的微服务中心中。还是那句话,现在你还不用太深入的理解“限界上下文”,暂时只需要理解它是一种模块划分的说法就好(后面会逐步深入解释)。
  • 标号 4 位置: 这里显示出来了“业务价值层”的代码——也就是该软件系统中需要作为最核心竞争力的那些模块,同样下面也会有多个“限界上下文”。
  • 标号 5 位置: DDD 战术设计软件分层的“菱形架构”下,“领域”(domain)层的代码放这里,也是业务逻辑最核心的代码——所有的“充血”模型代码。从这里开始,我们解释某个“限界上下文”内的代码结构。具体这些代码怎么设计的细节,我们后面会讲,现在你只需要知道这里放的是“业务逻辑核心”即可。
  • 标号 6、8 位置: 在 DDD 战术设计软件分层的“菱形架构”下,为了让“限界上下文”在满足外部的各种调用需求、以及需要调用或与别的“限界上下文”通讯时,不至于因为与本模块业务逻辑无关的、各种外在因素变化而引起本模块内代码逻辑的“动荡不安”,而引入了“北向网关”、“南向网关”概念。分别说明如下:
  • 标号 6 就里面就是“北向网关”的代码,里面又分为 local 和 remote 两个典型的目录。“北向网关”的作用,就是让限界上下文可以向外输出各类应用服务。local 目录下方的是本限界上下文向外提供的“应用服务”,是将 domain 内各种“充血模型”代码进行封装后的、完整的业务逻辑;而 remote 目录下,放的是对 local 目录为了满足“远程调用”而进行的代码封装——如 RPC 调用、跨服务器消息事件订阅等,并不存在任何业务逻辑。
  • 标号 8 里面就是“南向网关”的代码,里面又分为“端口(port)”和“适配器(adaper)”两个典型的目录。“南向网关”的作用,就是是让本限界上下文通过其请求外部资源。典型的 3 类外部资源请求有:访问数据持久层(关系或非关系数据库)、调用别的限界上下文服务(在微服务架构中,往往是 RPC 远程调用)、向别的限界上下文发布消息。我们都知道,这些对外部资源的请求,可能会因为外部资源的技术底层不同,而存在不同的实现方式。为了能够隔离“领域层”对具体技术底层的依赖,就分离出来 port 层和 adapter 层。在 java 语言实现中,port 层就是 interface,没有任何实现代码,只有方法定义;而 adaper 层就是 implemetaion,具体实现到不同持久层(如不同关系数据库 oracle/mysql 等、不同 nosql 数据库 redis/mongodb 等)。然后,根据 IoC(依赖倒置)原则在 java 中通过“依赖注入”来将 adaper 目录下的具体实现与 domain 层的代码连接起来。
  • 标号 7 位置: 这里是“发布语言”(published language, pl)层。说白了,“发布语言”就是让“北向网关”向外输出服务时,能与服务调用者之间有个“统一语言”,比如:输入输出参数的结构性定义、事件消息的格式定义等等。因为,我们是不用将限界上下文内部的“领域”层的内部对象结构“泄露”到外部的,所以我们必须要有这个“发布语言”层。
业务领域&业务子域

首先, 我们需要了解 DDD 中反复提到的“业务领域”和“业务子领域”的概念,并将“业务子域”区分出 3 个类别:核心子域、通用子域、支撑子域

  • 所谓的“业务领域”,指的就是“业务领域模型”。因为 DDD 是“领域驱动设计”,所以“业务领域模型”是 DDD 的核心概念来源。而”业务领域“就是所开发软件系统需要对应的现实世界的某个“业务范畴”,比如:电商、物流、通信、交通、医疗、教育等等。
  • 在“业务领域”下,我们可以将其业务内容分为多个“子领域”,比如:“电商”业务领域下,有“客户”、“商品”、“库存”等等多个子领域。
  • 需要注意的是: “业务子领域”的划分其实是不考虑将来软件系统会怎么建设的,而更多是考虑现实业务中需要怎么区分这些业务单元。当然,有可能系统实际实现后,系统的模块也会有跟“业务子领域”一样的划分——但这只是“系统同构化映射现实业务”,并不是系统模块划分在影响业务单元划分。也就是说: “业务子领域”划分是“因”“软件模块”划分是果。甚至有可能根据实际项目的现实条件,软件系统并没有按照“业务子领域”进行分块。比如:重用一个既有的整体的 SAP ERP 系统来实现“采购”、“库存”、“财务”多个“业务子域”的支撑。
  • “业务子领域”按照如下的识别规则划分为 3 类:核心子域、通用子域、支撑子域。
  • “通用子域” 就是那些无论在哪个行业、哪个业务领域下都可以通用的“业务子域”,比如:角色权限、组织管理、员工、IM 即时消息等等。这些“通用子域”所对应的软件实现,很多时候是可以直接在市面上采购成熟产品来实现的,不见得非要本项目的软件团队来实现。
  • “支撑子域” 是那些虽然有本行业、本业务领域下特定的业务知识,但并不是本业务领域下最核心的业务单元。比如:“电商”业务领域下的“库存”、“物流”、“财务”就不是核心业务单元,而只是支撑“订单”、“客户”等核心业务单元的、后台人员所使用的与直接客户无关的业务单元。
  • “核心子域” 就是那些体现本“业务领域”最核心业务价值的业务单元。当然,这其实是个“相对概念”,是在特定语境下才有效的说法。比如:在“电商”业务领域中,“物流”就是个“支撑子域”;但在“快递”业务领域中,“物流”就是个“核心子域”。
限界上下文

目标系统内部最粗粒度的模块划分,这个粒度的模块划分往往是后续“微服务”怎么切分的主要依据。

这种限界上下文的划分,最理想的情况就是一个“业务子域”对应一个“限界上下文”(所谓“同构映射”) 。但在实际软件系统设计中,可能因为“第三方伴生系统”、“遗留系统”等情况的存在,而不得不出现“错配”的情况,就如下图所示——也就是说:只有所有“业务子域”对应的业务逻辑都全部由目标软件系统“全新实现”才可能出现“最理想情况”。

限界上下文映射

就是搞清楚这些“限界上下文”模块之间是怎样的协作关系(调用关系、事件通知关系等等)

实体对象

是对象模型中最主要的类,需要数据生命周期管理的、根据 ID 标识而不是属性来判断是否同一个对象的类。如:订单、订单行等。

实体通过标识符(ID)来进行识别,属性值的变化无关紧要。标识符相同的两个实体视为同一个实体,即使其余的属性值完全不同。标识符不同的两个实体视为不同的实体,即使其余的属性值都相同。实体的重点是通过ID表明它 “是谁” ,而不是通过属性表明它 “是什么样子”

领域模型中还存在一类对象,它们用于描述领域实体的某个方面,而本身没有概念标识,我们关注的是它 “是什么样” 而不关心它 “是谁” ,我们把这种类型的对象成为值对象。它们本质上是 “披着对象外衣的值”

值对象

不需要数据生命周期管理的(往往作为实体对象的属性存在)、只要属性发生变化就是另一个对象的类。如:地理区域(含省市区)、家庭住址(含经纬度定位和详细地址)、身份证号(含编码规则)、姓名(含姓+名)。注:可以有自己的行为方法,和实体对象的区别是不需要生命周期管理。

不可修改:每次保存都是先删除值对象,再创建新的值对象

值对象和简单值(数字、字符串、枚举等)具有同等的地位,低于实体的地位。和简单值一样,值对象是实体的内部状态的一部分,位于实体的边界之内,不具有独立的生命周期。

值对象没有标识概念,其意义完全体现在它的属性上。所有的“5美元”都是相同的,区分“这个5美元”和“那个5美元”没有意义。相同类型的两个值对象,如果它们的属性值完全相同,就可以认为是等同的,可以相互替换;只要有一个属性值不同,就认为是不同的两个值对象。我们愿意交换相同面值的两张美元(值对象)因为它们是等价的;但绝对不愿意交换相同体重的两个婴儿(实体),因为每个人都只想要自己的孩子。

值对象在本质上是不可变的。改变了属性值的值对象实质上不再是原来的值对象,而是另一个值对象。因此对于实体的值对象属性来说,没有修改,只有替换。如果订单的金额从5美元改成了7美元,就是将原来的5美元扔掉,换上一个7美元,而不是将原来的那个5美元的amount属性值改成7。

聚合和聚合根

在实体对象中,有些实体对象是不需要单独出现的、总是跟着另一个实体对象的出现而出现、消亡而消亡的,如:”订单行”总是随着“订单”的出现而出现、消亡而消亡。往往,我们在完成“对象模型和关系识别”后,列出了很多实体对象,这些对象可按照它们之间的“绑定存亡关系”进行分组,分组后有一个实体对象是唯一的访问入口。这种分组就叫 “聚合” ,而那个作为唯一的访问入口的实体对象就是 “聚合根” ——一般情况下,给“聚合”的命名就是“聚合根”对象的名称,如:“订单”聚合可能就包含了“订单”、“子订单”、“订单行”、“商品快照”等多个对象(后面的对象都不需要单独出现)。需要说明的是:值对象是“附庸”在实体对象上出现的

领域服务

实体对象、值对象都是有行为的(也就是方法逻辑),很多业务逻辑就直接在这两类对象中实现了。那么,有了“聚合”就可以将很多业务逻辑在“聚合”内部的各个实体对象、以及伴随的值对象的方法逻辑中实现。但是,仍然有一些不能明确划分到单个聚合”职责“中去实现的业务逻辑,而这些逻辑就需要在“领域服务”中去实现。比如:“为当前登录用户创建订单”这一业务逻辑里面,既包含要根据当前登录用户的 ID 找到对应的客户资料,还包含根据请求输入信息创建订单记录。那这些逻辑,到底应该是由“客户”聚合中实现呢?还是在“订单”聚合中实现?显然,无论放在“客户”还是“订单”聚合中,都是不合适的,所以就有了“为当前登录用户创建订单”这一领域服务存在的必要性。

战略设计

主要完成如下 3 方面工作:

  • 系统上下文定义——将目标系统与伴生系统进行职责分工。

  • 限界上下文识别及其对应的上下文映射

  • 战略层面的技术决策——主要包括:

    1)“系统整体”角度的软件分层(区分边缘层、业务价值层、基础层)

    2)系统整体角度的架构性决策:微服务的划分、事务一致性策略(全局一致性 vs 最终一致性)、数据库架构(单体数据库 vs 分布式数据库);

    3)必要的各限界上下文的技术栈选择(开发语言、技术组件、使用规则引擎考量、使用 CQRS/命令总线考量、事件消息机制等);

战术设计

主要完成 4 方面工作:

  • 对象模型的概念分析——识别出有多少实体对象、值对象。
  • 聚合设计——将实体对象进行聚合分组,选出每个聚合的“聚合根”。
  • 服务设计——设计出“领域服务”,并在“领域服务”的基础上设计“应用服务”。这两者的区别,以及如何设计在后面的实例中会介绍。
  • 战术层面的技术决策——包括但不限于:如何在前端界面和后端服务之间传输数据(VO: view object)、以及微服务远程服务调用时如何传输数据(DTO:data transfer object)、使用开发语言的哪个持久化框架实现数据持久保存(如 JPA/Mybatis 等)、如何实现资源库端口等。
聚合设计

在实体对象中,有些实体对象是不需要单独出现的、总是跟着另一个实体对象的出现而出现、消亡而消亡的,如:”订单行”总是随着“订单”的出现而出现、消亡而消亡。往往我们在完成“对象模型和关系识别”后,列出了很多实体对象,这些对象按照“绑定的存亡关系”可以进行分组,分组后有一个实体对象是唯一的访问入口。这种分组就叫 “聚合” ,而那个作为唯一的访问入口的实体对象就是 “聚合根” ——一般情况下,给“聚合”的命名就是“聚合根”对象的名称,如:“订单”聚合可能就包含了“订单”、“子订单”、“订单行”、“商品快照”等多个对象。需要说明的是:值对象往往是“附庸”在实体对象上出现在聚合中的。

服务设计

a)实体对象、值对象都是有行为的(也就是方法逻辑),很多业务逻辑就直接在这两类对象中实现了。也就是说,有了“聚合”(里面包含多个实体对象、值对象)的设计,就可以将很多业务逻辑在“聚合”内部的各个实体对象、以及伴随的值对象中方法逻辑中得到了满足;

b)对于那些不能明确划分到单个聚合中去实现的业务逻辑,就需要通过“领域服务”、“应用服务”去实现

菱形架构

在对整个系统进行“分层架构设计”之前,我们还需要先熟悉一个软件架构模式——菱形架构,如下图所示:

img

对“菱形架构”的说明如下:

  • 菱形架构的基础,是依赖于“领域层”的界定。这涉及到后面才会进行的 DDD 战术设计内容,这里不做展开,您只需要知道:“领域层”放的是业务领域的关键业务知识,包括“聚合(内含实体对象、值对象)”和“领域服务”两类内容。事实上,我们所追求的“高内聚”主要体现的“领域层”,因为业务需求变化而引起的变化,我也希望主要通过“领域层”的“聚合”和“领域服务”的变化来实现。所以说,“领域层”是 DDD 的“核心代码所在地”。
  • 所有非“领域逻辑”层的代码,我们都希望封装到“北向网关”或“南向网关”中去。具体说明如下:
  • “北向网关”其实就是上下文向外提供服务、或接受消息通知的入口处。如果上下文只需要向外提供同一个进程内部的应用服务调用接口、或接受消息通知(通过消息总线),则需要“本地”服务;如果上下文需要作为独立进程(这时候一般是云原生的独立“微服务”)向外输出服务、或接受消息通知(通过消息中间件),则需要将“本地服务”包装为“远程”服务。
  • 也就是说:北向网关中的“本地服务”和“远程服务”是一一对应的,“远程服务”只负责将远程调用的出入参做格式转换并传给“本地服务”,而“本地服务”负责调用领域层的“聚合”或“领域服务”来完成具体的业务逻辑(关于“聚合”和“领域服务”的内容,我会在本专题后面的章节中演示)。
  • “南向网关”其实就是上下文用来将这 3 类技术细节从业务逻辑中“剥离”出来的主要手段(图中只画了第一种):访问底层数据库、调用其它上下文服务、向其它上下文发布消息通知。这 3 个技术细节的封装,在 DDD 战术设计中分别叫“资源库”、“客户端(也可以叫防腐层——ACL)”、“消息发布者”。
  • 从图中可以看出,“南向网关”有“端口”和“适配器”两种角色。简单点来说,“端口”是抽象接口(以 java 为例就是 interface),而“适配器”则是“接口实现”(以 java 为例就是实现了对应 interface 的类)。并且,一般来说“适配器”都是通过依赖注入(DI,控制反转 IoC 的一种)的方式集成到限界上下文的代码中(java spring 中一般是 bean 注入)。
  • “南向网关”区分“端口”和“适配器”两个角色的好处:一方面是可以让限界上下文内的任何代码都不直接依赖于具体的底层技术细节,如:采用哪种数据库(oracle 还是 mysql、甚至 nosql 数据库)、怎么调用其它上下文服务(本地调用还是远程 RPC)、怎么发布消息通知(本地消息总线、还是消息中间件);另一方面的好处是允许我们随时将构建在一个“微服务”甚至“单体应用”中的多个限界上下文进行拆分到多个进程(即多个“微服务”中),而并不会引起“领域层”、以及“北向网关”的任何代码修改(只要替换并重新打包被依赖注入的“适配器”类即可)。
  • 很明显可以看出,“菱形架构”其实是限界上下文内部所采用的一个“软件架构模式”。而在整个系统范围内,因为包含多个限界上下文,DDD 设计理念并没有要求所有的上下文都严格遵循“菱形架构”——而完全可以根据实际需要(尤其是“基础层”的上下文),视情况而采用其它架构模式(如 MVC 三层架构、大数据计算架构等)。
类图

UML类与类之间关系.png

UML类图示例.png

学习网站

DDD 实战 xie.infoq.cn/article/f3b…