领域驱动机理-依赖倒置
2021.05.25
干货直营
作者:王尧喜(微信332069183)
前美团资深专家、每日优鲜研发总监
大家可以加我微信入群
欠大家一篇领域驱动方法中项目分层设计的说明,今天补上。主讲核心领域中的依赖倒置/六边形防腐设计原因和落地细节。
【1】原因
在多方协同的系统结构中存在大量的指令和数据交换,假设设计不当,会出现一种现象:本系统的逻辑实现与上下游耦合严重,出现非必要时候的代码修改依赖,即上下游系统升级导致自己的核心逻辑联动修改的情况。
为了对抗这种耦合情况的发生,产生了跨系统设计中思考领域边界和交互治理的管理需求。落实到领域驱动方法论中,一是宏观战略设计中的领域边界划分和联动关系,一是战术设计中的依赖倒置以及核心领域模型与生命周期的管理。
以自己的server为例,反向绘制DSL交换链路,那么基本拓扑如下:
对应的,设计要关注的命题也就如下图所示:
1、上游指令和协议的隔离处理。
2、核心领域的建模与扩展性设计。
3、下游指令和协议的隔离处理。
(本次我们讲1和3)
【2】期望的拓扑结构
首先看下期望的逻辑分层机构,然后探讨如何实现:
输入指令层:处理上游下发或者自己下发给自己的指令(不考虑指令类型和技术选型的差异,所以RPC/MQ等统一在此位置)。这一层要考虑是指令的非标和差异协议直接渗透到核心领域中。
一方面尽可能设计标准API和弹性API,一方面设计协议处理层,消化参数不对齐问题。
协议映射层:向上接收和处理指令层的入口参数(request paratemers)以及拼接和转换口参数(response results)。向下理解核心领域的模型定义和领域能力,按领域接口和语义要求进行交互。这一层依旧需要帮助系统隔离上游的参数侵入。
核心领域层:系统核心模型和能力弹性的承载层,是系统扩展性张力来源。后续再讲,本次不涉及。
数据访问层:数据存储方案和组件选型层。这一层的挑战是存储选型的变化、以及存储文件schema与领域模型无法一致的问题。也是讨论比较多的焦点之一,本篇会做出这部分的设计说明。
输出指令层:完成业务逻辑语义所必须协同的下游服务,比如各类RPC、webservice、MQ等的RMI调用。一种情况是逻辑中间需要调用下游。一种逻辑是逻辑结束后对下游的周知。
【3】输入指令层和协议映射层:
围绕这一层的职能,本质是要稳定协议语义,避免修改接口,即便修改也控制修改的范围和规模,以及不要把上游的知识和领域概念过多的传递到核心领域中(会引起代码中大量引用上游类库或者概念,如果上游变更,则产生非必要联动修改),所以大概有几点方案:
输入层尽力做薄:输入层在定位上类似协议传输和交换,不做逻辑,逻辑下沉到聚合层或者领域层实现(比如不在Controller内做逻辑、不在MQ listener内做逻辑)。
提供弹性API:比如搜索接口的通用设计。
参数颗粒细化:控制变更的语义范围,避免一个大实体类声明所有参数。
上游参数映射:尽力处理上游系统的领域内容,防止上游定义变更,带来的大批量修改灾难。
部分举例说明:
弹性API:(比如Grid、容器、KV等都是弹性结构,我们在设计中可以借鉴)
假设有个订单搜索的接口,可以根据多参数检索订单。直观实现方法是写个Class SearchParam,里面声明各种String、Date、Integer等参数表示。这种写法的问题是对上游的契约语义弹性不足,因为追加参数的时候每次都改API协议。
由此需要设计一个模型或者结构来做扩展性设计:那么简单示例如下
通过List<Conditon>实现参数规模的弹性,通过不断丰富Condition模型来支持更多的参数结构和含义,比如范围搜索、模糊搜索、并联或者互斥条件含义等,都是可以在这一层实现的。
OrderSearchImpl 通过调用核心领域实现搜索结果。
参数颗粒细化:比如生成订单的参数,需要拆成很多模型。不同模型的逻辑和数据处理,在传递到领域中的不同Service中处理,来避免大参数依赖问题。
上游参数映射:比如优惠券系统有很多模式和玩法,接口数据结构也比较丰富,但是订单逻辑中不一定需要,那么就要自己设计一层来处理对方的数据参数。避免因为对方拆模型、改定义、改数值等带来的影响。
【4】输入指令层和协议映射层:聚合
在如上的设计理念之后,大家经常会有几个疑惑:
问题1:领域ServiceA需要依赖领域ServiceB其他服务的话,在哪里调用?
答:直接调用就可以,领域知识和内核可以共享。
问题2:对2个领域Serivce(A+B)的聚合怎么做?
答1:如果聚合无法成为领域能力,那么在上层聚合,也就是传说的application层。
答2:如果聚合也是领域能力,那么在该领域实体与能力中聚合。
【5】数据访问层:
围绕这一层的职能,本质是数据选型方案以及数据模型设计不稳定和易变情况下屏蔽核心领域模型的影响。所以设计的基本思路是:
依赖反转:以领域模型为核心和语义基础,不以数据存储层的schema为基础。究其原因:是ER关系的局限性导致无法表达和反推实际的物理模型关系。
独立仓储:仓储层独立设计。内含持久化存储、缓存存储、检索存储等各类方案。
模式建模:假设有多数据介质间的交互要求,那么设置类DCL的模态指令。如下图的QueryMode。
QueryMode:mysqlOnly(只读mysql数据源)、cacheOnly(只读缓存)、mysqlThenFreshCache(读mysql且刷缓存)等等。
同理比如UpdateMode:mysqlBothRedis,mysqlMust 等等。
交互指令:核心领域层根据要求,调用仓储层实现数据方案以及数据同步模式要求。
组件实现:组件实现可以使用策略模式、也可以使用组合模式,看场景复杂度。如果能长期保持多个组件的接口定义一样,用策略和工厂。如果比如mysql的读写接口非常复杂,远远多余缓存接口,那么可以使用组合和聚合方案。
【5.1】数据访问层:
如上所述:repository层的package(橙色是目录)结构基本如下:
以OrderQueryIF为例:
OrderQueryIF:接口,定义数据模型的增删改查。
OrderQueryImpl:实现层,根据Mode要求,调用不同comp实现。
Mode:对增删改查的数据层刷盘和同步语义进行说明。
comp:选型的不同组件,都在这一个层级。
所以谨记:对cache的访问,是repository内部的选型问题,而不是Service能力,不是在Service内引用redis API。
【6】输出指令层:
对于这一层的职能来说,和防止上游渗透核心领域一样,防止下游渗透核心领域是一个意思,外部材料比较多,就不细致展开。
简单说明方案:
设立独立module:设立一个独立的模块,处理API等协议调用问题,并且共整体领域各个Serivice共享。
防腐处理:处理参数拼接、API引用、结果解析等(和clean、六边形等架构中的防腐概念是一致的)。
选型和依赖的区别:不是一件事,经常见到大家混淆。
选型:是指的比如采用CQRS/Event/RPC还是什么方式实现和下游的交互。
依赖:是指的业务概念和领域知识上,双方的紧合程度(是松散依赖还是强耦合依赖)。
综上:切记不要把技术选型和领域模型耦合关系混到一起。
欢迎随时交流。