深入浅出DDD
转载:深入浅出 DDD
1. DDD是什么,为什么要用DDD
初学 Spring 或 SpringBoot 进行 Web 开发的时候,接触到的代码分层都是 MVC 架构。
MVC,全称 Model View Control(模型-视图-控制器),其分层定义如下。
M(模型层/ DAO 层) :业务数据载体层。
V(视图层/ Controller 层) :展现给用户的数据表示层。
C(控制层/ Service 层) :接受 V(视图层)传递过来的请求进行业务逻辑处理,并将处理后的 M 层(模型层)数据返回给 V(视图层)。
MVC架构的优势:
分层简单易懂;
层级逻辑替换方便;
可以降低层与层之间的依赖;
有利于标准化;
利于层与层逻辑的复用。
但是,随着系统功能迭代,业务功能越来越丰富之后,【控制层】里面对于业务逻辑处理的代码也越来越多,维护成本也越来越高。
因为,控制层就像个万能容器,什么代码都往里面写,承载了它不该承受的业务逻辑。反观模型层与视图层,空空如也。业务逻辑不是跟着业务模型走的,而是在现有数据模型的情况下,或者先设计数据模型的情况下去迭代了业务需求。业务模块之间的边界被淡化,控制层内逻辑只要能实现需求,想怎么写就怎么写,没有一个规范与规约。
为了解决常规中大型 Web 系统 MVC 架构下存在的弊端问题,这就引出了 DDD。在本小册中,我会带你看 DDD 如何从业务模型与业务边界出发去设计代码层级结构,将散落的、重复的逻辑内聚到业务模型中。从基础概念出发到落地实战演示,带你进入 DDD 的世界。
1.1 DDD 是什么
在使用 MVC 架构进行开发时,用户需求从被提出到落地,会经历下图的生命周期:
可以看到,用户需求会被生命周期每层参与人理解转化。用户提出的 A 需求到了产品那里可能被理解成了 A1;需求评审结束后,进行编码开发需求之前,往往最先做的一步就是设计表结构,此时需求可能被理解成了 A2;等表结构设计完成,再“自底向上”设计 DAO、Service、Controller,最终产出了 A3。
为了解决上述问题,DDD 所要做的就是:
通过事件风暴消除信息不对称,让业务相关人员都参与设计,确定每个业务领域的职责边界,统一语言;
将常规 MVC 三层架构中自底 (数据模型) 向上的设计方式做一个反转,以业务为主导,自顶 (业务模型) 向下地进行业务领域划分,自顶向下;
将大的业务需求进行拆分,建立业务领域模型,分而治之。
说到这里,对于 DDD 与 MVC 的区别,你可能还是有点模糊。这里我们就以电商订单场景为例
分析下。
假如我们现在要做一个电商下单的需求,这会涉及到用户选定商品、下订单、支付订单、订单发货等步骤。
MVC 架构。 常见的做法是在分析好业务需求之后,就开始设计表结构了,订单表、支付表、商品表等。然后编写业务逻辑,但这仅仅是第一个版本的需求。不久后功能迭代了,订单支付后可以取消,下单的商品可以退换货,那是不是又需要进行加表?紧跟着对应的实现逻辑也需要修改?功能不断迭代,代码就不断地层层往上叠。这就是面向数据库编程。
DDD 架构。 首先进行业务边界划分,这里面核心是订单,那么订单就是这个业务领域里面的聚合逻辑体现。支付、商品信息、地址等都是围绕着订单展开。订单本身的属性确定之后,地址等信息只是一个属性的体现。当你将订单的领域模型构建好之后,后续的逻辑边界与表结构设计也就随之而来了,功能点无非就是对订单聚合内的业务逻辑编排组合罢了,让业务逻辑的实现最原子化。
1.2 DDD 如何解决 MVC 痛点
业务的交互方式要分为两种:系统内部交互,系统与外部交互。
MVC 分层下,不论是系统内交互还是系统与外部交互,逻辑都是按照功能点被杂糅在一起。Service 层利用 DO、DTO、VO 等业务 POJO 作为数据载体,完成了所有模型之间的逻辑处理、数据转换等跟业务有关或者无关的事情。
Service 层臃肿且条理不清晰。而这些 POJO 除了字段属性,内部没有任何的业务逻辑,这就是典型的贫血模型。此处需要了解下贫血模型,充血模型,失血模型。
DDD 核心思想是什么呢?解耦与内聚!建立领域模型形成聚合根,将原先散落在 Service 层的业务逻辑收拢到领域模型内部,变成充血模型,聚合即为业务。
业务不是像炒大锅饭一样混在一起,而是一道道工序复杂的美食,都有它们自己独立的做法。
1.2.1 系统内部交互
DDD 的价值观里面,任何业务都是某个业务领域模型的职责体现。
为了完成某一个需求功能,将核心的业务逻辑定义在领域内部,应用服务层(APP层)编排调用领域中的业务方法来实现功能点的需求。
也就是说,业务功能是领域所供的能力的组合。
这样,每个领域只会做自己业务边界内的事情,最小细粒度地去定义需求的实现。原先模型层空空的贫血模型摇身一变,变成了充血模型。
进到应用服务层,你的代码就是你的业务逻辑。逻辑清晰,可维护性高!
1.2.2 系统与外部交互
假如微服务体系下,有一个下订单的需求。在通过订单服务下订单前,需要先请求用户服务获取下单用户的个人信息,如下图,
用户服务在版本 A 时获取用户详情的接口是 interfaceA,版本 B 时换成 interfaceB。那么就会出现,需要修改订单服务中获取用户信息的逻辑。
如果类似的逻辑散落在系统的很多地方,就会出现外部系统的业务逻辑变更,造成了本系统的大量依赖变更。这里引入防腐层。
为了解决这个痛点问题,DDD 通过定义适配器包装对外部系统的依赖。系统内部直接依赖适配器,由适配器去调用外部接口,减小外部系统的变动对本系统业务逻辑的影响。这里其实也是面向接口编程。
1.3 DDD 的优势是什么
从业务出发,自顶向下设计系统,优先考虑领域模型,而不是切割数据和行为,告别贫血模型;
领域设计简化复杂业务,内聚逻辑实现,准确传达业务规则,分而治之;
应用服务层的编排即展示了业务逻辑,增强了代码的可读性与可维护性;
消除业务参与人员的信息不对称,提升协助效率,统一语言;
将外部系统等不可控因素转化为可控因素,减小系统间依赖,适配器;
适合于业务复杂的中台化的系统设计。
1.4 到底什么样的系统适配 DDD
DDD 其实在十几年前就已经被提出来了,但为什么是近几年才开始逐渐进入大众的视野呢?
总结起来,主要有以下几条原因。
DDD 的结构不像 MVC 结构那么简单,分层更加复杂。
消除信息不对称的成本比较大,需要多方人员协作讨论业务模型。
迭代快的小系统不如直接使用 MVC 做好代码规范能够更快地上线。
因此,不适配 DDD 的系统是什么呢?
中小规模的系统,本身业务体量小,功能单一,选择 MVC 架构无疑是最好的。
项目化交付的系统,研发周期短,一天到晚按照甲方的需求定制功能(这种本身业务需求边界就不清晰,功能的可持续迭代性就很差,而且这种系统一般就是一口价买卖),这种也最好选择 MVC。
那相反地,适配 DDD 的系统是什么呢?中大规模系统,产品化模式,业务可持续迭代,可预见的业务逻辑复杂性的系统。
总而言之就是:
你还不了解 DDD 或者你们系统功能简单,就选择 MVC;
你不知道选用什么技术架构做开发,处于业务探索阶段,选用 MVC;
其他时候就酌情考虑 DDD
2. 战略设计:业务内聚与解耦
DDD 的核心战略目标是解耦与内聚,为了完成这个战略目标,它定义了领域、子域、限界上下文、通用语言、上下文映射图和架构风格的概念。
2.1 领域与子域
DDD 的英文是 Domain Drive Design,直译过来就是领域驱动设计。由此可见,领域是 DDD 架构落地设计的核心。
那么,领域的概念是什么? 百度百科上对此的翻译是:
一种专门活动或事业的范围、部类或部门。
而在 DDD 中,领域本身并不是一个学术性很强的概念,任何边界明确的业务都能被称为领域。比如,一个电商平台中,订单、物流、支付等都是这个平台的领域。
针对一个领域做二次划分它就是子域了。领域和子域都是相对的概念。如果把电商平台看成一个大的电商领域,那么订单、物流这些就是它的子域。但如果把订单看成一个领域,那么商品、订单明细等就是它的子域。
如下图,我们可以把电商系统看作领域,然后进行这样的划分:
把电商系统看成一个大领域,根据功能职责划分为订单子域、物流子域等。分布式系统中,往往我们把这种细粒度划分出来的子域看成微服务。把微服务看成一个大的领域范畴,微服务内部的小模块就是我们的子子域。按照这种方式我们可以建立起一个领域树
。
对于同一父级领域而言,根据子域在父级领域下的业务价值又可以将子域划分为核心域、支撑域和通用域。
2.1.1 核心域
核心域是业务系统的核心,它是业务系统核心价值的体现。核心域的划分标准是根据系统的定位而决定的。比如,把桃子树看成一个系统,如果它存在果园中,那么桃子是它的核心域;如果它存在于花园中,那么桃花是它的核心域。
2.1.2 支撑域
这种子域它本身没有核心域对于业务价值那么突出,但是业务系统根据核心域开展业务时又需要依赖它。比如,安全气囊对于车辆而言,它不会成为车辆这个系统的核心卖点,但是它如果没有,一定会影响到车辆的价值。并且不同的车型,安全气囊的规格(比如大小)也是不一样的,这就是支撑域的业务定制性,强业务相关,但又非核心。
2.1.3 通用域
通用域的核心诉求是稳定与高兼容性,它能够被移动至其他的领域下。比如,在订单领域中,用户与权限就是它的通用域。同样的,这个子域能够在几乎不修改核心逻辑的情况下被应用至物流领域中。
2.2 限界上下文与通用语言
在前面桃树的例子中,在果园跟花园中桃树所对应的核心域是不一样的。
造成这种子域划分差异的原因是什么?
我们从具体的语义环境出发去思考了核心域的划分是导致差异的主要原因。而这种具体语义环境就是上下文
。桃子是核心域时,它的上下文是果园;桃花是核心域时,它的上下文是花园。花园跟果园在各自的上下文中开展业务,不会互相入侵上下文。花园的农夫不会去果园养花,果园的农夫不会去花园养果子,这就是不同上下文之间的边界。
限界上下文意味着特定的、具有明确边界的语义环境,定义了领域的业务边界。
在同一个限界上下文中,我们对于领域内所有内容的认知应该都是一致的。相信大家在需求开发过程中遇到过跟产品、业务人员、测试“扯皮”的头疼时刻,为了解决这个问题,我们需要有一套通用语言来消除项目相关的人员对领域内的业务逻辑、流程处理规则、专业术语的信息差。
通用语言表示着对领域内的一切动词、名词、形容词达到了一致的认知。比如,我们在果园的限界上下文里认为桃树是用来生产桃子的,而不是用来开桃花的。
2.3 上下文映射图
电商领域中下有订单领域、物流领域等子域。商品这个属性在订单上下文与物流上下文中都是存在的,只是在不同的上下文中地位不同而已。但是,商品的信息会随着业务的进行从订单上下文流转到物流上下文。这种上下文之间的协作模式可以用上下文映射图表示。
上下文映射图分为以下几种方式。
2.3.1 合作关系
A、B 两个限界上下文是为了完成某一功能建立起合作关系。同时成功,同时失败,合作的频率与它们的耦合程度是成正比的。如果它们之间的耦合程度愈演愈烈,则需要考虑是否两个限界上下文应该合并,它们本身就是一个上下文。
2.3.2 共享内核
如果两个上下文在各自开展业务的过程中都需要使用到一个公有的能力点,则将这个公有的逻辑子集给抽离出来共享,类似于基础工具能力。这个子集的变化将影响所有被关联的限界上下文内部逻辑。
这里需要注意共享内核
与通用域
的区别。共享内核的定位是工具基础能力infra,是为了提供领域完成业务所需要的能力。通用域本质上还是一个子域,它可以去使用共享内核,而共享内核不能关联通用域。
2.3.3 客户方-供应方开发
这种在上下游依赖关系的系统中比较常见,它们由两个不同的团队维护。上游需求开发完,下游使用上游提供的能力再进行开发。
2.3.4 追随者
类似于客户方-供应方开发模式,但是上游不提供能力,只提供模型。
2.3.5 防腐层
上下文 A 与上下文 B 之间不直接进行交互,而是通过定义一个防腐接口进行交互。上下文 A 指定依赖接口的标准,而上下文 B 给接口所提供的逻辑上下文 A 无法直接感知。
DDD 中防腐层是系统内上下文与系统外上下文交互的最主要手段。
2.3.6 开放主机服务
与防腐层有点类似,防腐层是把防腐能力定义在了调用方,而开放主机是在被调用方定义了调用规则或者接口协议,由调用方来调用标准接口。
2.3.7 发布语言
类似于开放主机服务与防腐层的逻辑,只不过把规范定义在两个上下文之间,它们之间的协作通过统一的标准进行交互。比如,上下文 A 与上下文 B 之间通过 MQ 中间件进行交互。
2.3.8 另谋他路
这种关系模式在大型应用系统特别常见。两个上下文之间毫无关联,独立开展业务。
2.3.9 大泥球
这种模式在历史项目中比较常见,业务迭代事件长,内部逻辑复杂,业务边界梳理困难。为了不让这种情况往外扩散,把这个系统当成一个黑盒子,只用它提供的接口能力,不严格定义它内部逻辑边界。
2.4 架构风格
下图是 DDD 的六边形架构:
在这个架构下,领域模型是逻辑处理的出入口,应用服务层是业务功能点的出入口,是整个系统对外的门面。一切外部输入均需要通过应用服务层来处理,再通过应用服务层返回。这种方式下,我们平等地看待例如 Web、RPC、MQ 等外部服务,认为它们都属于用户接口层。所有的外部服务通过应用服务提供的接口来访问领域模型。
3. 战术设计:战略思想的落地与实践
战术设计为了匹配战略设计主要包括以下概念:聚合、聚合根、实体、值对象、应用服务、领域服务、仓储、事件模型等。
3.1 聚合、聚合根、实体与值对象
领域/子域是 DDD 战略设计中最核心的业务体现。那么对应到代码层面,领域/子域的概念的呈现方式是什么呢?答案是:聚合
。为了描述聚合内部的属性,DDD 定义了实体与值对象的概念。最后,领域的逻辑呈现要在一个限界上下文中才有意义,必然要有一个概念来包括下领域的逻辑与定义业务的边界,这个就是聚合根
。
3.1.1 实体
实体是描述某一可连续变化的物体。它是具有生命周期的,并且可以通过唯一标识来确定是否为同一个实体。
比如,现在有两个长相一模一样的双胞胎分别叫张三与张四。他们刚出生的时候什么都不会,随着年纪的增长,张三成为了科学家,张四成为了企业家。但是他们并不会因为各自的身份属性变更,而导致他们不是张三与张四了,因为本质上他们这个人的唯一标识在成长过程中一直未改变。
实体 = 唯一标识 + 生命周期(可以理解为属性可变)
3.1.2 值对象
它与实体定位正好相反,如果一个物体一旦被生成之后就具备不可变性,并且只要它们的属性值一致就可以认为它们是同一个物体。
比如,双十一我们在淘宝购买商品的订单,订单中会包含地址,地址就是典型的值对象。只要省、市、区与详细地址一致,就判断它是同一个地址,并且这个地址一旦确认下来之后就不会产生属性的变更。
值对象 = 不变性 + 通过属性判断相等(没有唯一标识)
3.1.3 聚合
它是领域的抽象体现,包含了当前领域内的一切事务。它在代码层面主要呈现的方式是模块的划分。
比如下图,我定义了一个用户领域,那么我会划分出一个用户的聚合包,把专属于用户领域的内容放在 com.baiyan.ddd.domain.aggregate.user
这个包下。
3.1.4 聚合根
如果说聚合是领域的抽象体现,那么聚合根就是领域的具象体现,它是一种特殊的实体。聚合根内部定义了当前领域需要的业务属性(实体与值对象),并且包含了该领域内所有的业务逻辑定义。
比如订单这个领域,它的具象体现就是订单聚合根。订单聚合根内部包括了订单明细实体、地址值对象等各种属性。
在订单聚合根内部定义了订单领域的业务逻辑方法。
聚合根 = 领域强关联的实体、值对象 + 核心业务逻辑
3.1.5 实体、值对象与聚合根的关系
到这里为止,我想你应该明白了实体、值对象、聚合根的概念了。
那它们之间的关系是什么呢?
第一个,包含关系。如下图所示,聚合根内部能够包含 N 个实体与 N 个值对象,它们作为聚合根的属性。
第二个,生命周期关系。这个从包含角度其实就很明显,聚合根里面包含了实体与值对象。也就是说实体的生命周期是捆绑着聚合根的,由聚合根来维护。而值对象不存在生命周期,只能被整体替换。
第三个,标识关系。聚合根本身就是实体,它的 ID 就是它的唯一标识,这个没什么好说的。但是实体的唯一标识是仅针对当前聚合根而言的,就像商品实体能够被订单聚合关联,也能被物流聚合关联。值对象在聚合内部的唯一性通过属性相等判断实现。
3.1.6 建立实体、值对象与聚合根关联
明白了以上的概念后,在领域建模过程中怎么来划分实体、值对象与聚合根呢?
这里我们以新建用户,新建过程中需要给赋予角色这个需求为例,给你讲解这个划分思路。我们分别根据角色的不同定位(角色能不能独立开展业务,是否有独立的生命周期)来划分一下这里的关联关系。
角色非独立维护
整个系统中的角色不是独立开展的业务,比如我们定义了一个角色的枚举类,系统的用户只能关联这个枚举类对应的角色。这个时候,角色在用户聚合根内就是值对象,因为此时角色满足了不变性与属性判断相等这两个条件。
角色独立维护
如果角色本身可以独立开展业务,比如系统内管理员可以新增自定义角色,新增用户的时候可以关联到这个角色。超级管理员可以修改角色的名称,此时查看用户关联角色信息时应该是修改后的角色名。
很明显,这种情况下,角色本身在用户聚合根内是一个可以变的状态,并且如果用户需要感知到角色的可变,只能通过角色的不可变的唯一标识去感知。这种情况下,角色在用户内就是实体。
3.2 应用服务与领域服务
根据划分后的领域,我们能够确定领域的具象体现——聚合根。此时,原子化的业务逻辑都被定义在了聚合根内部,这也是 DDD 所推崇的解耦与内聚思想。一个聚合根只代表了一个领域的业务,而我们系统的功能体现往往是多个领域聚合协作的,对应了战略设计里面的上下文协作。
为了完成这种协作逻辑,战术设计中定义了应用服务层与领域服务层。
3.2.1 应用服务
应用服务可以看作是一个流程编排引擎,它本身不承担任何业务逻辑处理。应用服务可以理解为功能用例层,比如新建用户,这个功能就应该定义在应用服务层。但是新建用户是一个比较繁琐的流程,比如涉及到关联角色等业务逻辑处理。这些业务逻辑处理应该被定义在用户聚合根内部,而应用服务只负责调用定义在聚合根内部的方法就好了,屏蔽的业务逻辑的具体实现。
应用服务相对来说是比较薄的一层,它只做逻辑编排。参数校验、聚合根方法调用、外部服务调用、持久化聚合根等与业务流程走向相关,业务逻辑无关的代码均可定义在此处。
应用服务是整个系统的门面,也是六边形架构中的出入口,外部服务通过访问应用服务提供的接口来执行功能用例。
3.2.2 领域服务
虽然应用服务与聚合根逻辑几乎已经覆盖了功能点的实现,但是有时还是会出现这样的业务场景:
A 聚合根需要做一个原子化的逻辑处理,但是这个逻辑处理需要 B 聚合根的逻辑协作才能完成。
这种场景的实现方式有两种。
第一种就是在应用服务内先调用 A 聚合处理一下,再调用 B 聚合处理一下,最后再调用 A 聚合收尾逻辑。这种方式符合 DDD 思想,但是对应到应用服务,我明明是一个很原子化的 A 聚合的逻辑处理,居然有三行代码。而这段逻辑会被好几个功能点调用,每次为了完成这个逻辑我就要写三行代码,显然逻辑的原子化不够突出,还容易出 Bug。
第二种就是应用服务与聚合根都各退一步,在它们中间抽象一层领域服务。把 A、B 聚合协作逻辑定义到 A 的领域服务内,应用服务调用 A 领域服务即可,这样在应用服务上看这段逻辑就很清晰了。
领域服务其实是对业务的一种妥协,理想情况下是没有领域服务的。一旦出现了领域服务,一定要确定好这是否在执行一个特别显著的、专属于某个领域的原子化业务逻辑。滥用领域服务很有可能会演化为逻辑又定义在 Service 状况。
3.3 仓储
我们知道为了内聚业务逻辑,应用服务层编排的都是聚合根的业务逻辑,也就是说我们一直在应用服务内操作的都是领域模型。但是领域模型是针对于业务层面的,而领域模型处理完业务之后需要通过数据层存储。数据层对应的是数据模型,为了桥接数据模型与领域模型,DDD 在战术设计中提出了仓储的概念。
仓储的定位就是持久化聚合与检索聚合。让应用服务专注逻辑编排,聚合根专注逻辑处理,不用关心领域模型的持久化方式与存储介质。
3.4 事件模型
虽然按照上述的方式我们已经可以在战术上切合战略设计,但是貌似应用服务为了完成一个功能要做一些都不是这个功能点的事情。
比如下订单后,给用户增长积分与赠送优惠券的需求。如果在应用服务内实现,用户逻辑处理完,数据入库成功后,再依次调用用户增长积分的外部服务接口与赠送优惠券的外部服务接口。到这里是不是很奇怪?我一个订单领域,已经把下订单这个事情做完了,但是却还要调用其他的三方服务的接口通知它们订单生成这个事情。如果后续通知的接口越来越多,对于应用服务简直就是灾难。
为了解决这个耦合严重的鸡肋点,DDD 的战术设计中提出了事件模型。下单完成后,发布一个下单完成的领域事件,让需要感知这个事件的服务自行监听并处理,忽略不相关的领域活动。事件驱动。
领域事件的发送成功应该与功能点的事务是一致的,但是领域事件的处理结果不应该与功能点事务一致。我下订单成功了,发送了创建订单事件,但是积分增长失败了,这时如果让订单生成失败,这显然是不合理的。
4. 什么是事件风暴?
常规的业务需求到功能代码的转换流程如下图所示:
可以看到:用户的需求经过层层的转换才被转发到了研发人员这里,研发人员又会根据会议中获取到的需求自我转化功能的实现。
相信这是大多数研发同学开发需求的流程,那这种流程和方式会存在什么问题呢**?一个需求经过了这么多道的层层转化,需求在每一层都会经过不同人的理解转化,会导致信息不对称问题的出现**。
那么 DDD 是如何消除需求分析与同步过程中的信息不对称呢?
从工程角度来看,DDD 很多专有名词与结构划分都是基于解耦模式下的套路。从落地 DDD 的过程来看,有两个问题是最困难且最重要的:一个是界定出一个系统中有多少个聚合,即划分多少个业务模型;另一个是界定出每个聚合之间的限界上下文,即划分清楚领域的业务边界。
为了解决以上两个痛点问题,一种被叫作事件风暴的轻量型系统分析方法被提出。
4.1 事件风暴的概念及流程
事件风暴是一套 Workshop(类似于头脑风暴)的方法。它以事件为出发点,通过多人协作来划分业务领域与业务边界。
事件风暴的分析过程就像在讲述一个个的用户故事。通过一个个的用户故事来统一开发人员、业务人员、UX、测试等项目参与者对业务流程的认知,这包括关键的流程、核心的业务规则、系统不同模块的使用。其次是帮助开发人员梳理清楚领域模型与业务边界。
那么用户故事又是怎么分析出来的呢?
事件: 代表了某一个业务行为,是事件风暴中的核心概念,所有的分析都以事件为核心展开。描述的形式为“宾语+动词”的过去式。例如,合同已被签署、资料已被上传,等等。使用橙色的便利贴标示。
命令/动作: 表示产生事件的对象,执行了动作之后就会产生相应的事件。例如,“签署合同”命令导致“合同已被签署”事件。使用蓝色的便利贴标示。
角色/执行者: 表示产生命令的对象。例如,顾客执行“签署合同”动作,这里的顾客即为角色/执行者。使用黄色便利贴标示。
也就是说,事件风暴的核心流程就是:用户执行了命令,从而产生了事件。
下图是事件风暴对于用户故事的分析的完整流程:
策略/业务规则: 当产生事件时,需要进行某些业务相关的规则校验,例如“合同已被签署后”事件,根据签署合同的类别产生“发送优惠券”的动作。使用粉色便利贴标示。
数据/读模型: 事件产生后的另一个结果往往是呈现用户所关心的数据在系统界面。例如,当用户执行“签署合同”的命令之后,生成了“合同已被签署”事件,此时呈现在用户面前的应该是被签署后的合同信息。这样的数据我们使用读模型表示。使用绿色便利贴标示。
外部系统: 事件并不一定由执行者执行命令产生,也可能由一个外部系统产生。例如,“合同已被签署”事件完成后通知给财务系统,财务系统触发“发起扣款”动作,产生“扣款已完成”事件。使用红色便利贴标示。
因此,在分析一个业务系统前,首先要做的就是搞清楚我们想要的业务结果(事件)是什么,从事件出发开始反推产生事件的动作、外部因素与业务规则。再根据动作进行反推分析本系统内的动作汇聚发起点的业务汇聚在何处。
汇聚点即为某一个业务领域的聚合,一个个事件与动作的组合就是领域的业务逻辑,根据业务逻辑来设计领域所需要的属性。
4.2 事件风暴的开展事项
第一个事项,参与人员。 事件风暴采用 Workshop 的方式。任何与项目相关的业务人员、架构人员、研发人员等都可以参与其中。
第二个事项,准备工作。 需要准备一面大的画板或者墙,以及数张不同颜色的便利贴(包括蓝色、黄色、红色、橙色、绿色、粉色、紫色),不同颜色的便利贴对于事件风暴有不同的意义。
第三个事项,建模讨论。 常规情况下还是由产品经理先讲解自己梳理的需求点,划分事件,以事件为中心点扩散推导出第一个版本的用户故事。与会人员对于上面张贴出的流程进行头脑风暴,对于需要补充的流程节点使用特定颜色的便利贴进行张贴。讨论结束后,对于事件风暴结果进行拍照或者以其他记录方式存档。
5. 分层详解:如何明确各层级的使用场景与方式
5.1 应用服务
在战术设计中提到过应用服务是比较“薄”的一层,但是它却能包含参数校验、权限控制、事务控制与逻辑编排这么多的功能。
这里应该只有逻辑编排,参数校验和权限控制应该是在ui层。
什么样的代码算业务逻辑?什么样的代码算编排逻辑呢?
这个重要判断标准就是你的代码是不是跟业务流程分支走向相关的,如果是,那就是编排逻辑;如果不是,那就是业务逻辑。
5.2 领域服务
领域模型需借助其他领域模型的能力来完成当前领域模型的原子化业务逻辑,为了不污染领域模型,建立领域服务来充当桥梁。
5.3 Interface(灰度层)
6. 仓储层(Repository):串联数据模型与领域模型的桥梁
当领域模型一旦建立之后,你不应该关心领域模型的存取方式;仓储就相当于一个功能强大的仓库,你告诉它唯一标识,例如用户ID,它就能把你想要的数据组装成领域模型一口气返回给你。 存储时也一样,你把整个用户领域模型给它,至于它怎么拆分,放到什么存储介质(DB、Redis、ES 等),这都不是你业务应该关心的事。你完全信任仓储能帮助你完成数据管理工作。
6.1 为什么要用仓储
区分数据模型和领域模型。
数据模型, 仅仅只是一个底层的数据结构,也就是传统的 ER 模型,内部没有任何业务逻辑;
领域模型, 模型本身即是业务逻辑的体现,基于该模型的原子化业务逻辑均是内聚在模型内部的。
数据模型只存在于数据层,领域模型在领域层,而衔接了这两层的关键对象,就是仓储。
仓储所要做的就是让业务专注于自己的逻辑处理,防腐了数据模型变更对于领域模型的影响,让领域模型可以不受存储介质限制来定义业务属性,能够独立开展业务。
6.2 如何落地仓储
6.2.1 落地流程
第一个,入参指令化。 增删改的入参有两种类型:一种是直接参数,另一种是 Command。Command 表示指令,需要完成一个变更行为。参数的方式是因为有的场景实在太简单了,只需要一两个参数,可以不做方法包装。但是我这里还是建议你只要是对数据做增删改操作,入参哪怕只有一个参数也包装成一个Command。代码的语义化更强,方法作用一目了然。Command,Query和Event。
第二个,Command 转聚合。 Command 参数仅仅为用户交互层的外部输入,最后业务逻辑的处理还是需要转换成聚合来完成。如果是新增类型的执行,转换逻辑简单的情况下,在 Command 内部定义一个 toDamain 的方法转化;转换逻辑复杂的情况下,则使用工厂类去新建聚合。
第三个,Converter 衔接模型。 聚合是针对业务而存在的充血模型,虽然在大多数领域建立完成后,它的属性可以跟表字段一一对应起来。但是它们的系统定位还是不同的,桥接领域模型和数据模型的桥梁就是 Converter。
6.2.2 仓储规范
聚合和仓储之间是一一对应的关系。仓储只是一种持久化的手段,不应该包含任何业务操作。从抽象角度看,不同业务的仓储的对外呈现方式应该是一致的,因此,仓储也有它自己对外呈现的统一规范。
第一,统一接口方法,无底层逻辑。仓储的接口严格意义上只有 save、saveAndFlush、delete、byId 方法。比如,领域模型的修改新增均使用统一的 save 方法,仓储负责将领域模型保存至存储介质中。
第二,出入参仅为领域模型与唯一ID。仓储对外暴露操作的是领域模型,并且它的接口是存在于领域层的,无法感知到底层的数据模型。这个在工程分包上就会做依赖限制,保障仓储的功能统一性。
第三,避免一个仓储走天下。类似于 Spring Data、JPA 这样的 ORM 框架会提供通用的仓储接口,通过注解实现接口访问数据库的能力。通用的仓储接口本身就违背了仓储层设计的初衷,业务模型与数据模型又被捆绑在一起。并且如果后续数据的存储介质发生改变,比如 MySQL 转 ES,或者查询 DB 前,走一下缓存,扩展极为困难。
第四,仓储只做模型的转换,不处理业务逻辑。首先要清楚的是,仓储是存在基础设施层的,并不会去依赖上层的应用服务、领域服务等(如下图)。仓储内部仅能依赖 Mapper、ES、Redis 这种存储介质包装框架的工具类。比如 save 动作,仅对传入的聚合根进行解析放入不同的存储介质,你想放入 Redis、数据库还是 ES,由 Converter 来完成聚合根的转换解析。同样,从不同的存储介质中查询得到的数据,交给 Converter 来组装。
6.2.3 CQRS
CQRS,英文全称 Command Query Responsibility Segregation,翻译过来就是命令查询职责分离。它在 DDD 中的理论体现可总结为如下流程图:
7. 事件驱动模型
与聚合核心逻辑有关的,走应用服务编排;与核心逻辑无关的,走事件驱动模型,采用独立事务模式
8. 如何落地 CQRS?
9. 防腐层
不同限界上下文之间如果需要进行业务往来,它们并不会直接通过 RPC 等方式进行通信,而是通过一个中间方或者一个约定好的规则来进行互相通信。上下文映射图中的开放主机服务、发布语言以及防腐层就是防腐思想的体现。
9.1 外部系统的防腐
但是如果我们在应用服务编排业务逻辑需要外部服务所提供的能力呢?
上游服务接口逻辑变动,破坏了规约;
本系统过分信任上游服务接口,将接口调用逻辑散落在系统各处。
那么我们是不是可以把这个调用方规整到一起呢?找一个中间方去调用上游系统接口,系统内依赖中间方就好了。如果上游接口变化或者提供能力的服务变化,我们也只需要修改中间方就好了。
在本系统中这种做文章的方式我们称为防腐层
。
我们认为一切外部服务所提供的能力或者模型都是不可靠的,我们如果要去使用这个能力,应该依赖本系统内的适配能力接口,由接口实现类去依赖外部服务与模型。
这样我们的供求关系就从别人有什么我们就用什么
变成了我们用什么别人提供什么
,如下图所示:
9.2 落地防腐层
防腐层又称适配层,用于转义内部上下文依赖的外部上下文。
最后更新于