DDD

战略设计

DDD 主要倡导的是

  1. 事件风暴
    通过事件风暴消除信息不对称,让业务相关人员都参与设计,确定每个业务领域的职责边界
  2. 流程反转
    将常规 MVC 三层架构中自底 (数据模型) 向上的设计方式做一个反转,以业务为主导,自顶 (业务模型) 向下地进行业务领域划分
  3. 业务拆分
    将大的业务需求进行拆分,建立业务领域模型,分而治之
  4. 把业务逻辑转移到领域模型,而不是服务层
  5. 为了与外部系统解耦,通过在内部定义适配器去与外部交互,防止外部代码腐烂内部代码

领域和子域

领域和子域是相对的概念,在一个领域内进行二次划分就是子域,如果把电商平台看成一个大的电商领域,那么订单、物流这些就是它的子域。但如果把订单看成一个领域,那么商品、订单明细等就是它的子域。

把电商系统看成一个大领域,根据功能职责划分为订单子域、物流子域等。分布式系统中,往往我们把这种细粒度划分出来的子域看成微服务。把微服务看成一个大的领域范畴,微服务内部的小模块就是我们的子子域。按照这种方式我们可以建立起一个领域树。

领域

对于同一父级领域而言,根据子域在父级领域下的业务价值又可以将子域划分为核心域、支撑域和通用域

  1. 核心域

核心域是业务系统的核心,它是业务系统核心价值的体现。核心域的划分标准是根据系统的定位而决定的。比如,把桃子树看成一个系统,如果它存在果园中,那么桃子是它的核心域;如果它存在于花园中,那么桃花是它的核心域。

  1. 支撑域

这种子域它本身没有核心域对于业务价值那么突出,但是业务系统根据核心域开展业务时又需要依赖它。比如,安全气囊对于车辆而言,它不会成为车辆这个系统的核心卖点,但是它如果没有,一定会影响到车辆的价值。并且不同的车型,安全气囊的规格(比如大小)也是不一样的,这就是支撑域的业务定制性,强业务相关,但又非核心。

  1. 通用域

通用域的核心诉求是稳定与高兼容性,它能够被移动至其他的领域下。比如,在订单领域中,用户与权限就是它的通用域。同样的,这个子域能够在几乎不修改核心逻辑的情况下被应用至物流领域中。

限界上下文与通用语言

在前面桃树的例子中,在果园跟花园中桃树所对应的核心域是不一样的。

造成这种子域划分差异的原因是什么?

我们从具体的语义环境出发去思考了核心域的划分是导致差异的主要原因。而这种具体语义环境就是上下文。桃子是核心域时,它的上下文是果园;桃花是核心域时,它的上下文是花园。花园跟果园在各自的上下文中开展业务,不会互相入侵上下文。花园的农夫不会去果园养花,果园的农夫不会去花园养果子,这就是不同上下文之间的边界。

限界上下文意味着特定的、具有明确边界的语义环境,定义了领域的业务边界。

在同一个限界上下文中,我们对于领域内所有内容的认知应该都是一致的。相信大家在需求开发过程中遇到过跟产品、业务人员、测试“扯皮”的头疼时刻,为了解决这个问题,我们需要有一套通用语言来消除项目相关的人员对领域内的业务逻辑、流程处理规则、专业术语的信息差。

通用语言表示着对领域内的一切动词、名词、形容词达到了一致的认知。比如,我们在果园的限界上下文里认为桃树是用来生产桃子的,而不是用来开桃花的

战术设计

DDD 的战略设计包含了什么:领域、子域、限界上下文、通用语言、上下文映射图和架构风格。战术设计为了匹配战略设计主要包括以>下概念:聚合、聚合根、实体、值对象、应用服务、领域服务、仓储、事件模型等。

聚合、聚合根、实体与值对象

领域/子域是 DDD 战略设计中最核心的业务体现。那么对应到代码层面,领域/子域的概念的呈现方式是什么呢?答案是:聚合。为了描述聚合内部的属性,DDD 定义了实体与值对象的概念。最后,领域的逻辑呈现要在一个限界上下文中才有意义,必然要有一个概念来包括下领域的逻辑与定义业务的边界,这个就是聚合根。

  1. 实体
    实体是描述某一可连续变化的物体。它是具有生命周期的,并且可以通过唯一标识来确定是否为同一个实体。
    实体 = 唯一标识 + 生命周期(可以理解为属性可变)
  2. 值对象
    它与实体定位正好相反,如果一个物体一旦被生成之后就具备不可变性,并且只要它们的属性值一致就可以认为它们是同一个物体。值对象的修改是直接替换,局部更新是不被允许的,更像一组数据。值对象不变性和可移植性可以方便进行传递
    值对象 = 不变性 + 通过属性判断相等(没有唯一标识)
  3. 聚合
    它是领域的抽象体现,包含了当前领域内的一切事务。它在代码层面主要呈现的方式是模块的划分
    (模型,事件,约束,仓库接口)这些东西的组合就是聚合
  4. 聚合根
    如果说聚合是领域的抽象体现,那么聚合根就是领域的具象体现,它是一种特殊的实体。聚合根内部定义了当前领域需要的业务属性(实体与值对象),且包含了该领域内所有的业务逻辑定义,聚合是一种分包思路,可以看作一个领域;而聚合根就是这个领域中的具体逻辑;多个实体实现聚合根的功能点;
    聚合根 = 领域强关联的实体、值对象 + 核心业务逻辑
  5. 实体、值对象与聚合根的关系
    • 第一个,包含关系。如下图所示,聚合根内部能够包含 N 个实体与 N 个值对象,它们作为聚合根的属性。
    • 第二个,生命周期关系。这个从包含角度其实就很明显,聚合根里面包含了实体与值对象。也就是说实体的生命周期是捆绑着聚合根的,由聚合根来维
      护。而值对象不存在生命周期,只能被整体替换。
    • 第三个,标识关系。聚合根本身就是实体,它的 ID 就是它的唯一标识,这个没什么好说的。但是实体的唯一标识是仅针对当前聚合根而言的,就像商
      品实体能够被订单聚合关联,也能被物流聚合关联。值对象在聚合内部的唯一性通过属性相等判断实现。

划分的立场

这里我们以新建用户,新建过程中需要给赋予角色这个需求为例

  1. 角色非独立维护
    整个系统中的角色不是独立开展的业务,比如我们定义了一个角色的枚举类,系统的用户只能关联这个枚举类对应的角色。这个时候,角色在用户聚合根内就是值对象,因为此时角色满足了不变性与属性判断相等这两个条件。
  2. 角色独立维护
    如果角色本身可以独立开展业务,比如系统内管理员可以新增自定义角色,新增用户的时候可以关联到这个角色。超级管理员可以修改角色的名称,此时查看用户关联角色信息时应该是修改后的角色名。
    很明显,这种情况下,角色本身在用户聚合根内是一个可以变的状态,并且如果用户需要感知到角色的可变,只能通过角色的不可变的唯一标识去感知。这种情况下,角色在用户内就是实体

应用服务与领域服务

根据划分后的领域,我们能够确定领域的具象体现——聚合根。此时,原子化的业务逻辑都被定义在了聚合根内部,这也是 DDD 所推崇的解耦与内聚思想。一个聚合根只代表了一个领域的业务,而我们系统的功能体现往往是多个领域聚合协作的,对应了战略设计里面的上下文协作。为了完成这种协作逻辑,战术设计中定义了应用服务层与领域服务层。

应用服务

应用服务可以看作是一个流程编排引擎,它本身不承担任何业务逻辑处理。应用服务可以理解为功能用例层,比如新建用户,这个功能就应该定义在应用服>务层。但是新建用户是一个比较繁琐的流程,比如涉及到关联角色等业务逻辑处理。这些业务逻辑处理应该被定义在用户聚合根内部,而应用服务只负责调用定义在聚合根内部的方法就好了,屏蔽的业务逻辑的具体实现。
应用服务表象定位与 MVC 中的 Service 比较像,但是 Service 内部充满了功能点的逻辑处理,而应用服务相对来说是比较薄的一层,它只做逻辑编排。参数校验、聚合根方法调用、外部服务调用、持久化聚合根等与业务流程走向相关,业务逻辑无关的代码均可定义在此处。
应用服务是整个系统的门面,也是六边形架构中的出入口,外部服务通过访问应用服务提供的接口来执行功能用例。

领域服务

虽然应用服务与聚合根逻辑几乎已经覆盖了功能点的实现,但是有时还是会出现这样的业务场景:
A 聚合根需要做一个原子化的逻辑处理,但是这个逻辑处理需要 B 聚合根的逻辑协作才能完成。这种场景的实现方式有两种。

  1. 第一种就是在应用服务内先调用 A 聚合处理一下,再调用 B 聚合处理一下,最后再调用 A 聚合收尾逻辑。这种方式符合 DDD 思想,但是对应到应用服务,我明明是一个很原子化的 A 聚合的逻辑处理,居然有三行代码。而这段逻辑会被好几个功能点调用,每次为了完成这个逻辑我就要写三行代码,显然逻辑的原子化不够突出,还容易出 Bug。

  2. 第二种就是应用服务与聚合根都各退一步,在它们中间抽象一层领域服务。把 A、B 聚合协作逻辑定义到 A 的领域服务内,应用服务调用 A 领域服务即可,这样在应用服务上看这段逻辑就很清晰了,也方便进行复用

*领域服务其实是对业务的一种妥协,理想情况下是没有领域服务的。一旦出现了领域服务,一定要确定好这是否在执行一个特别显著的、专属于某个领域的原子化业务逻辑。滥用领域服务很有可能会演化为逻辑又定义在 Service 状况

仓储

为了桥接数据模型与领域模型,DDD 在战术设计中提出了仓储的概念
仓储的定位就是持久化聚合与检索聚合。让应用服务专注逻辑编排,聚合根专注逻辑处理,不用关心领域模型的持久化方式与存储介质。

事件模型

虽然按照上述的方式我们已经可以在战术上切合战略设计,但是貌似应用服务为了完成一个功能要做一些都不是这个功能点的事情。

比如下订单后,给用户增长积分与赠送优惠券的需求。如果在应用服务内实现,用户逻辑处理完,数据入库成功后,再依次调用用户增长积分的外部服务接口与赠送优惠券的外部服务接口。
到这里是不是很奇怪?我一个订单领域,已经把下订单这个事情做完了,但是却还要调用其他的三方服务的接口通知它们订单生成这个事情。如果后续通知的接口越来越多,对于应用服务简直就是灾难。
为了解决这个耦合严重的鸡肋点,DDD 的战术设计中提出了事件模型。下单完成后,发布一个下单完成的领域事件,让需要感知这个事件的服务自行监听并处理,忽略不相关的领域活动。

领域事件的发送成功应该与功能点的事务是一致的,但是领域事件的处理结果不应该与功能点事务一致。
我下订单成功了,发送了创建订单事件,但是积分增长失败了,这时如果让订单生成失败,这显然是不合理的。

事件风暴的重要性

从落地 DDD 的过程来看,有两个问题是最困难且最重要的:一个是界定出一个系统中有多少个聚合,即划分多少个业务模型;另一个是界定出每个聚合之间的限界上下文,即划分清楚领域的业务边界

事件风暴是一套 Workshop(类似于头脑风暴)的方法。它以事件为出发点,通过多人协作来划分业务领域与业务边界。
事件风暴的分析过程就像在讲述一个个的用户故事。通过一个个的用户故事来统一开发人员、业务人员、UX、测试等项目参与者对业务流程的认知,这包括关键的流程、核心的业务规则、系统不同模块的使用。其次是帮助开发人员梳理清楚领域模型与业务边界。

那么用户故事又是怎么分析出来的呢?可以用流程图,时序图等进行表达
也就是说,事件风暴的核心流程就是:用户执行了命令,从而产生了事件。
事件风暴

因此,在分析一个业务系统前,首先要做的就是搞清楚我们想要的业务结果(事件)是什么,从事件出发开始反推产生事件的动作、外部因素与业务规则。再根据动作进行反推分析本系统内的动作汇聚发起点的业务汇聚在何处。
汇聚点即为某一个业务领域的聚合,一个个事件与动作的组合就是领域的业务逻辑,根据业务逻辑来设计领域所需要的属性。

架构分层

简单的把MVC的三层映射为如下图的四层

它的分层思想依赖关系即符合了 DDD 的在战术设计上的分层,又跟 MVC 的分层极为类似。从上往下,用户接口层对应了 MVC 中的 Controller 层,MVC 中的 Service 层被拆分成了应用层(用于编排逻辑)和领域层(实际业务逻辑编写),最底层的基础设施层对应了 MVC 中的 Dao 层及其他工具层。

传统四层

在该架构中,上层模块可以调用下层模块,反之不行。即
Interface ——> application | domain | infrastructure
application ——> domain | infrastructure
domain ——> infrastructure

分层作用
分层 | 英文 | 描述 |
– | –| –|
表现层| User Interface |用户界面层,或者表现层,负责向用户显示解释用户命令|
应用层 |Application Layer |定义软件要完成的任务,并且指挥协调领域对象进行不同的操作。该层不包含业务领域知识。|
领域层| Domain Layer |或称为模型层,系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手|

基础设施层| Infrastructure Layer |主要有2方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;一是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现;|

乍一看,这个分层思想好像很合理,与 MVC 的分层思想不冲突,而且我们也能按照 DDD 的思想去开展业务。但是我们从层级依赖上来看一下,上层依赖下层。在 MVC 的分层下,我们通常会认为越在下面的层,它距离实际的功能点的逻辑是越来越远的。也就是说一些通用的工具类、系统配置、消息发送接收配置、外部接口调用封装等通用型的功能都会被集中定义到基础设施层中。而这时,领域层却依赖了基础设置层,让本应该纯粹只处理限界上下内文的领域受到了外部服务或者一些配置的污染。而且我相信一旦有了基础设施这样一个大杂烩层之后,总会有那么几个人,把一些本应该放在领域里面的逻辑定义在了基础设施里面,逐渐你的架构就又开始退化。

为了解决这个问题,世界级编程大师 Robert C. Martin 提出了改进四层架构的思想:依赖倒置。他认为:

高层模块不应该依赖于底层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

在四层架构的世界中,上级需要做什么事情都是需要下级实际拥有这个能力,上级直接调用才能完成。而依赖倒置之后,只要下级定义了能力的接口,上级就可以通过依赖注入的方式来直接注入接口,调用接口方法即可。而下级对应的接口逻辑实现,被放置在基础设施层,提到了最上层,如下图所示:

各层直接用接口进行隔离,就可以把模块进行充分的解耦合
依赖倒置原则的包含如下的三层含义:
1.高层模块不应该依赖低层模块,两者都应该依赖其抽象
2.抽象不应该依赖细节
3.细节应该依赖抽象

高层模块不依赖低层模块:那就可以在domain层定义存储的接口,如AARepository,但是不写具体的技术实现
抽象不依赖细节:在domain层里,不依赖其他包的类,如用到数据存储时,直接调用domain的抽象接口即可
高层通过依赖注入的方式,将基础设施的实现传到domain层中

依赖倒置四层

如此一来,我们的架构就不再是分层的结构(从上往下调用)。而是将抽象全部堆在domain层,将细节全部往application和infrastructure去推。而越抽象越稳定,所以通过这种做法能够有效减少业务的变更。目的是让里面的于领域相关的东西稳定,少修改,业务无关的的放在外层,方便进行替换修改外层的东西

依赖倒置四层演化的六边形架构
从外往里看,领域模型(对应领域层)完全独立,可以自由地开展自己的业务。应用服务包含了领域服务进行逻辑编排,完成功能点的业务组装。并且应用服务作为业务系统的统一门面,提供各种适配的接口给外部来访问。

六边形架构