依赖倒置(搭建柔性框架)

精英怪
广告

本文基于我之前写的两篇文章「从架构层面看设计模式 」和「Service层需要接口吗? 」,建议阅读此文之前先阅读这两篇文章。

依赖倒置(搭建柔性框架)

在「Service层需要接口吗? 」这篇文章的评论里,有朋友评论使用DDD设计的时候就需要使用接口。不过那篇文章就不是讨论DDD开发框架的,实际对应的是事务脚本开发框架。本文会在何处何时使用接口做进一步阐述。

为什么选择事务脚本开发框架呢?主要是鉴于在个人所接触的大大小小几十个项目里,使用DDD的项目寥寥无几。而在使用了DDD的项目中,有些项目就理解错了DDD;有些设计挺丰满,但落地很骨感。也就是说,从设计到代码层面时,都多多少少做了妥协。

个人认为主要原因是DDD的概念过于复杂,在进行DDD时,需要先理解很多的概念,比如:领域、子域、核心域、支撑域、通用域、实体、值对象、聚合、聚合根、领域事件等等等等。

部分概念可以参考我之前写的几篇文章:

《领域设计:Entity与VO》

《领域设计:聚合与聚合根》

《领域设计:领域事件》

这些概念不是自己理解就ok了,是需要整个团队去理解,理解了这些概念之后,才能进行设计和开发。也就是说,DDD对整个团队的要求高了几个level。

所以,DDD与国内短平快的开发模式不是很契合。DDD重设计,但大部分的企业实际是重结果,所以很多短平快的项目都使用的事务脚本的方式来进行开发。

事务脚本的方式早期开发很快,但是后期维护会比较麻烦,具体原因可以参考「从架构层面看设计模式 」,我会在下文进行一个对比。

所以我希望能搭建一个兼容事务脚本和领域设计的开发框架,能在开发效率和可维护性之间找到一个平衡。这就是我写此文的初衷。

if-else与事务脚本的关系

在「从架构层面看设计模式 」中,我将if-else与设计模式进行了对比。指出了if-else代码与使用设计模式的代码之间的6个区别:

边界差异解耦独立进化对象聚集依赖倒置逻辑显化

实际上,事务脚本的结构和if-else的结构很类似,如下图所示:

一般我们基于事务脚本的框架分三层,Controller、Service、和Dao层。其中,

Controller实际就对应if-else的调用层Service对应了if-else的逻辑层if-else不涉及数据持久化,所以也就没有Dao层

不过整体结构是一致的,类似的结构,也就导致了类似的问题!我们通过一个例子,来具体看一下。

举个例子

假设我们有下面这个用例:

如果,我们使用事务脚本的方式来进行开发的话,那我们的代码看起来像这样:

你能一眼就看出来代码的逻辑吗?你是不是需要完整的读完代码才能知道具体的逻辑是什么?想象一下,在后期维护的时候,你是不是每次都要看一遍这部分代码,才能回忆起来逻辑,然后才能进行修改,这个成本是不是有点太大了?

这段代码的结构如下图所示:

这段代码总体来说,有如下这些问题:

类职责不单一,包含了:业务逻辑数据库操作发送短信Service层职责不单一:AppApplyService是业务服务,SmsService是公共服务依赖有问题:业务逻辑层依赖了持久层逻辑未显化:不完整读完代码无法确定逻辑

对于这些问题,我们该怎么来解决呢?

独立业务逻辑

首先,我们想到的就是「独立业务逻辑」。业务代码最核心的就是业务逻辑,我们不希望我们的业务代码淹没在一堆非业务代码中。

那我们如何独立呢?这就涉及到了我们常说的「领域建模」(注意:此处的领域建模和DDD中的领域模型是有差异的,在这里你可以把它理解为包含了业务逻辑的充血模型)。即将业务逻辑抽离到对应的对象中,针对领域模型,我们可以独立出一层,这里我们称为「业务逻辑层」。而原来的业务逻辑层我们称为「业务调用层」。如下图所示:

可以看到,独立了领域模型后,原来「业务逻辑层」对「持久层」的依赖关系也被倒置了,现在变成了「持久层」依赖于「业务逻辑层」。

为什么要依赖倒置呢?因为「业务逻辑层」使我们系统的核心,我们不希望核心逻辑依赖于非核心逻辑,这会导致核心逻辑的不稳定。

独立公共服务

接下来,我们继续进行功能拆解。将短信发送服务抽离到公共服务中。如下图所示:

这里可以看到,调整后,「业务调用层」和「业务逻辑层」都依赖了「基础层」。不过因为基础层主要提供的是基础设施,是相对比较稳定的,所以「业务逻辑层」对其依赖没有什么问题。

且「基础层」可以供多个服务使用,可以以库的形式提供出来。

隔离持久逻辑

接着,我们看一个问题。在用例中,有一个校验逻辑:需要确保「申请的应用数量<=5个」。这个逻辑我们应该放在哪里?

第一种方案是逻辑还在原来的Service中。不错此方案还是导致了Service中的代码逻辑不够单一,不推荐。第二种方案是将逻辑独立到「业务逻辑层」,作为独立的逻辑类来处理。比如提供一个AppApplyCheckService,对应到DDD中的领域服务。然后在原Service中调用。不过这里我不推荐这么做。因为这里的校验逻辑和其后的创建逻辑构成了一个完整的业务逻辑,推荐放在一起。也就是下面说的第三个方案。第三个方案,在领域模型中实现对应校验逻辑

顺便一提,上面的Service也是不需要基于接口来实现的,因为这里可以通过Spring注入,还是「业务调用层」依赖「业务逻辑层」

无论是第二种方案还是第三种方案,都会引出一个新问题:「如何查询出当前用户已经申请的应用数量?」也就是说,业务逻辑层中的对象如何调用持久层来查询数据呢?

上面说了,「业务逻辑层」不能依赖于「持久层」,所以我们这里需要做依赖倒置。引入Repository仓储接口,在持久层对其进行实现。结构图如下:

代码实现

调整后的代码如下:

这里的代码基本都是委托性代码,构建了完整的执行流程。通过对方法的友好命名,对应用例的逻辑可以在这里友好的显示出来(这里的vo.create()可能叫vo.checkAndCreate()更好一点)。

对应的核心业务逻辑代码在AppApply领域对象中:

其中的checkNum就是业务员校验逻辑:

脱离容器的测试

如此调整,所带来的一个好处,就是可以进行脱离容器的单元测试。虽然Spring提供了一些单元测试的辅助类,但是基于容器的测试由于需要启动容器,速度还是比较慢。独立了业务逻辑后,可以针对业务逻辑做单元测试。

这里需要针对Repository进行Mock,代码并不复杂。

测试在2秒内就可以完成。

柔性框架

「柔性」这个词,第一次是在DDD那本书里看到的,书中说的是「柔性设计」:「为了使项目能够随着开发工作的进行加速前进,而不会由于它自己的老化停滞不前,设计必须要让人们乐于使用,而且易于作出修改!」

就「柔性框架」而言,就是希望框架能够随着项目的推进而能够不断的演化,且能让人们乐于使用。

回到上面的框架结构:

在项目早期,可以使用事务脚本的方式快速进行开发。此时不需要关注「业务逻辑层」项目中后期,可以引入「业务逻辑层」进行领域建模对于复杂业务逻辑,可以在「业务逻辑层」进行领域驱动设计

总结

本文基于「从架构层面看设计模式 」对if-else的分析,对事务脚本代码进行重构,推导出一个适应项目全期的「柔性框架」。

发表评论

快捷回复: 表情:
AddoilApplauseBadlaughBombCoffeeFabulousFacepalmFecesFrownHeyhaInsidiousKeepFightingNoProbPigHeadShockedSinistersmileSlapSocialSweatTolaughWatermelonWittyWowYeahYellowdog
评论列表 (暂无评论,274人围观)

还没有评论,来说两句吧...