DDD之跨层调用的思考

一、背景

最近通过COLA构建篮球运营管理平台演示源码的时候对跨层调用做了一些深度思考,在跨层调用中有些调用并不是严格按规范或者相对固定的分层模式去走的,这就出现了一些疑问,比如不按规范来我怎么控制代码质量,我怎么知道最佳实践是什么?
另外一方面的问题是目前实践DDD的代码和案例确实不是很多,深度集成各种中间件和调用案例的工程也不是很多,大多情况下都是理论+摸着石头过河。本文结合最近的代码实践和跟COLA作者沟通的一些点来阐述在跨层调用下的场景。

二、对规范的思考

跨层调用在架构风格或者架构思想上目前看不是一个好少年。由于跨层调用很容易带来工程的不稳定性,比如dto的转换,跨层调用的耦合到后期无法维护。但是在实践中跨层调用就像幽灵一样,随时出现。我们通过之前的架构风格等博文可以知道,在整洁架构,洋葱架构,严格的分层架构中只有外层依赖内层,并不会出现跨层调用。在调用链路上每层都有自己的职责。
很多同学比较执着于规范,这无可厚非,但是理想化的工程模型在现实中很难全部实现,因此严格的跨层调用规范并不适用于所有场景。下面我们通过一些case来阐述一些打破跨层调用规范的场景。

三、打破规范的场景

3.1 阿里巴巴Java开发规范

DDD之跨层调用的思考_第1张图片

如上图是阿里巴巴Java代码开发规范-嵩山版,第六章第一节的应用分层的分层架构示意图。很明显上图是一种松散分层的模型,在这个松散分层中并没有严格强调上层只依赖下层,而是上层与相对下层的松散组合。当然这张图也没有从DDD的方面考虑。
下面我们看一下DDD的应用分层架构,如下图是松散的分层架构模式
DDD之跨层调用的思考_第2张图片

当我们把跨层调用的依赖箭头去掉,就像一层一层垒积木一样的,跨层之间没有关系。根据DDD群的讨论,这两种架构风格都害人不浅,尤其是松散的分层架构。

3.2 CQRS调用

在CQRS架构示意图中,Query层很明显是比较灵活的,通常来说单纯的应用CQRS或者在DDD中应用CQRS都将面临查询是否经过业务层或者领域层。比较复杂的查询场景有分页查询,导出。多表聚合查询,多数据源聚合查询,单数据源搜索引擎。相对Command而言这些查询更重,一旦实现也不容易调整,但是有一点好处就是相对稳定,迭代上面如果没有太多业务逻辑的话比较好维护。另外一点就是这些查询可以更接近更上层,如应用层或者适配层。为了兼容性能指标,查询通常不受DO,BO的转换约束。所以在松散的分层架构中融合CQRS的query是相对容易的。
另外再多说一句,当这种跨层调用出现的时候,我们的查询结果模型应该怎么定或者怎么管理?
这里建议底层依然走DO模型,但是可以绕过BO,在应用层用聚合DTO来承接。当然另一种方式就是DAO.mapper的返回走的是DTO模型。不过这样的话在整个层里面就显得有点突出了。

3.3 消息回调

在消息回调的场景中,这个跨层调用就会显得很突出。比如在COLA中基础设施层已经集成了MQ相关的中间件,那么收发消息肯定都是经过基础设施层的服务的。领域服务工程发送消息很明显是遵守层间调用依赖的,但是消费消息就显得有点另类了,因为消息肯定是从基础设施层出来的。那么谁去消费这个消息,如果有多业务线的话这个消息是不是先要到应用层处理。另外的问题是应用层不会包含MQ中间件的依赖,这样的话消息的监听基本就在基础设施层了。
在cola实践中CQRS模式在应用层集成之后应用层可以依赖基础设施层服务。但是消费消息的话只能在基础设施层发起,因此一种可能的解决方案就是在基础设施层引用应用层,这就形成了一种相互依赖的关系。但是这应该不是最好的解决方法。
下面我们探索另外一种可能的方案。由于中间存在领域层,而应用层也肯定会依赖领域包。在之前博客帖子领域的代码实战中我简单模拟了下整个领域分包的实践,在基础设施层的mq领域调用了app层的调用,说白了还是在同一个模块里,所以冲击并不大。我现在负责的交易核心中台项目也没有单独将领域层分出来。所以一种可能的方案就是将app层+领域层+基础设施层融合在一起,通过约定的分包策略进行代码组织开发。当然,领域层模型可以单独分模块出来这样的话也会好一点。
那假如我非要在分包策略的基础上分模块,根据cola的分包模型去搞呢。那消息消费的问题该怎么解决?
这两天我也进行了一定的思考,结合公司业务场景和交易核心项目工程的代码案例,消息消费目前细分起来可以分为应用层消息和领域层消息。下面说明一下这两个的区别。
领域层消息:领域层消息消费的场景是跟自己的核心模型有关的消息。举个例子,在交易中心的核心领域模型中有个支付单对接支付中心的业务,那么支付中心返回的支付结果消息就算是交易中心的领域层消息。
应用层消息:应用层消息消费的场景是跟自己的核心模型没有多大关系的消息。举个例子,在交易中心的核心领域模型并没有某某业务模型或者并没有这个场景,但是需要监听其他系统发出的消息来调整自己核心模型的数据和状态。那么这里的应用场景可能就有很多,比如中台类项目可能就会经常遇到需要对接各种其他业务线的平台系统,每个平台系统都会有自己的业务模型来对接中台的核心领域模型。那么在中台类项目的角度来看这些业务线平台类系统的特定场景就属于中台的应用层,这些应用层业务会跟核心领域模型做数据模型的对接。
大概了解这两类消息的情况下,消息回调在跨层调用和多模块分包情况下就很好处理了。针对领域层消息消费可以通过领域服务接口来定义消费逻辑,如(gataway)。
针对应用层消息消费目前有两种方案:

  1. 在领域层内单独通过如ablility包作为消息消费的入口。优点:避免跨层调用,缺点:容易污染领域模型,将特定业务场景混合核心领域模型导致代码混乱。
  2. 通过spring的应用事件发布消费机制来实现

优点:消息消费逻辑和触发逻辑分开,避免跨层调用。
缺点:消费逻辑代码不方便统一管理。产生了二次消息消费和订阅,代码不容易理解。
那么针对第二种方案的可行性我们分析一下:
假如采用rocketmq作为消息中间件的话,那么消息消费的监听代码(XXListener)应该是在基础设施层,然后通过spring application event的发布订阅机制将消息发布出去。在应用层的spring 事件监听器会监听到消息,通过应用层再次触发业务逻辑代码。由于整个项目肯定已经跟spring框架耦合了,所以可以借助这种机制将消息再次转发出去。消息体的话可以通过map返回也可以在XXListener转成领域层的消息模型到应用层,应用层不用再次进行转换。
总体上来说,消息回调的跨层调用实际上受到很多因素影响,毕竟针对消费的消息去做区分也会带来理解上的门槛。目前看消息回调的跨层调用还是尽量避免为好。

3.4 跟随者(遵奉者)模式

之前在学DDD的40+模式的时候有个模式令人印象深刻,虽然不是很出名,但是在服务间调用的时候有一定用处。下面来回顾一下跟随者模式的定义:
通过严格遵从上游团队的模型,可以消除在BOUNDED CONTEXT之间进行转换的复杂性。尽管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,但选择CONFORMITY模式可以极大地简化集成。此外,这样还可以与供应商团队共享UBIQUITOUS LANGUAGE。供应商处于统治地位,因此最好使沟通变容易。他们从利他主义的角度出发,会与你分享信息。
eric的书上大多描述了跟随者模式在团队间和上下游合作的一些问题,以及大概的解决办法。当然应对到代码层面可能不太好理解。举个例子,业务订单系统跟交易系统和支付系统的单据转换以及上下游的调用就会出现这种协作问题。通常业务订单系统会按交易系统的模型去做数据转换,和适配。交易系统会按支付系统的模型做转换和适配,中间可能会加入防腐层来防治核心领域模型和业务被污染。
上面说的是比较常见的情况,下面我们说一下其他两种场景

  1. 上游的数据我都要,上游的数据模型我直接复制处理
  2. 上游的数据我根据下游需要而要,但是我不存,而是调用下游直接给下游。

那么上面这几种情况都算是跟随者模式的例子,在代码上我可以不需要将上游的模型拿到我自己的领域层去识别。而只需要在DTO层面上做适配就可以了,同理我需要调用下游的时候也不需要经过领域层,而是直接通过防腐层或者适配层调用。在cola工程模块中就是应用层(app)或者适配层(adapter)可以直接调用基础设施层(infrast)的外部服务接口。
当然这种跨层调用也是很好处理的,具体实践起来也没有多大难度。

3.5 跨层调用的返回值处理

上面说了一些跨层调用的场景,这里特别说明一下跨层调用的返回值如何处理。一般情况下上游调用下游接口是下游包装自己业务领域的data,code,msg。但是有时候上游需要知道下下的返回信息,这样的话就需要特殊处理一下了。

四、总结

在上述第三节中为什么说DDD的松散分层架构害人不浅呢,我想有以下几点原因:

  1. 人云亦云,复制粘贴
  2. 潜意识的松散分层架构很灵活且更容易实践
  3. 由于跨层调用存在,实践者可以没有条件和约束的应用这种策略

最终经过跨层调用实践的代码很多都难以维护,且在多模块的复杂系统中跨层调用重构起来更困难。
在架构实践中,合适的才是最好的。但是并不是随意的应用跨层调用,也不是严格的遵守上下层依赖调用,而是有选择的,谨慎的根据需求和技术场景等考虑是否进行跨层调用,通常来说90%的场景都不需要跨层调用。

你可能感兴趣的:(DDD领域驱动设计与实战,软件工程专题,DDD,跨层调用,分层架构设计)