架构案例-依赖倒置循环依赖解耦

Posted amongdata

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了架构案例-依赖倒置循环依赖解耦相关的知识,希望对你有一定的参考价值。

一、循环依赖有哪些

首先我们要讲清楚什么是循环依赖,以及循环依赖的在程序设计层面、软件产品设计层面、顶层架构设计层面上可能出现的场景。从概念模型上讲,只要两个或多个元素产生相互依赖关系,就可以看成产生了循环依赖:

上图是两个依赖关系正确的示例:A元素正常工作依赖于B元素的正常工作,或者A元素的正常工作依赖于B、C、D元素的正常工作。这里的A、B、C、D四个元素可以指代四段代码,也可以指代一个业务系统中四个功能模块,还可以指代顶层架构设计中的4个独立工作的业务系统。

循环依赖在逻辑层面上是一个有向循环图。上图中展示了两个错误的依赖关系实例:A元素正常工作依赖于B元素正常工作的同时,B元素正常工作又依赖于A元素的正常工作。那么究竟哪个元素能够首先正常工作起来呢(先有鸡还是先有蛋)?右侧图展示了三个元素的循环依赖,A元素依赖于C元素,C元素依赖于D元素,D元素又依赖于A元素。那么A、C、D三个元素究竟哪一个元素才是底层的基础元素呢?

  • 代码层面的循环依赖:

代码层面的循环依赖是开发人员最容易出现的编码错误,不过有的时候也不能全怪开发人员,毕竟业务设计者从需求理解阶段可能就出现了问题。以下示例了代码层面的循环依赖:

/**
 * 此类依赖于BusinessB
 * @author yinwenjie
 */
public class BusinessA 
    private BusinessB bb;
    public BusinessA(BusinessB bb) 
        this.bb = bb;
    

    ......


/**
 * 此类依赖于BusinessC
 * @author yinwenjie
 */
public class BusinessB 
    private BusinessC bc;
    public BusinessB(BusinessC bc) 
        this.bc = bc;
    

    ......


/**
 * 此类依赖于BusinessA
 * @author yinwenjie
 */
public class BusinessC 
    private BusinessA ac;
    public BusinessC(BusinessA ac) 
        this.ac = ac;
    

    ......  

    // 接下来我们试图实例化BusinessA...
    public static void main(String[] args) 
        // 怎么实例化BusinessA呢?
        // new BusinessA(new BusinessB(new BusinessC(new BusinessA(!程序员已经发疯!))))
    

 

实际上按照这样的引用结构和构造函数要求,实例化BusinessA这件事情是永远无法完成的

PS:比如spring bean初始化的循环依赖,需要有一套机制解决

  • 功能层面的循环依赖:

业务系统的功能间也可能出现循环依赖。相对于代码层面的循环依赖,功能模块层面的循环依赖更能够影响一款业务系统的设计质量。笔者曾参与的一款软件,客户方曾经提出过这样的一个业务需求:

货运系统中在创建新的“发车单”时,必须选择空闲的司机和空闲的货车(当然货车类型是要判断的)。空闲的司机和空闲货车缺少任何一样都不能完成“发车单”的创建。但同时为了记录某辆货车上一次对应的“发车单”,客户要求只能在创建新的发车单后,货车才能解除之前的“发车单”绑定关系,变成“空闲货车”。

那么问题来了,如果只有完成新的“发车单”创建后,货车才能解除和之前“发车单”的绑定关系,那么新创建“发车单”时,“空闲的货车”从哪里来呢?实际上客户方不懂技术,是我们在需求调研阶段遇到的算一个问题的问题,但关键看需求人员从哪个方面着手向用户解释引导用户对需求逻辑进行分析。不一定用技术语言直接告诉用户,他的需求在技术层面上不符合逻辑。

  • 架构层面的循环依赖:

多个业务系统在进行集成时,他们也可能会出现循环依赖。特别是参与集成的业务系统越多,这种循环依赖的情况就越容易出现:

在系统数量还没有达到一定数量时(通常来说这个阀值为4),系统间的循环依赖最可能是由业务人员/技术人员无意造成。这时,系统间的依赖关系还处于一个可控级别,即使出现系统间循环依赖的情况,技术团队/业务团队也可以快速进行纠正。但是,当参与集成的业务系统数量超过可控制的阀值数量后,这个检查和纠正工作就不再是人工可及的范围了。如下图所示的5个系统进行集成时,很容易出现系统间循环依赖的情况:

二、避免循环依赖

  • 依赖倒置原则预防循环依赖

依赖倒置原则可以帮助预防代码层面和功能模块层面的循环依赖。顶层架构层面的循环依赖问题,也可以遵循这个原则进行设计来避免。依赖倒置原则在很多书本、网络资料上都有很详细的介绍。基本上这个原则是在说两个点:高层次模块不应该依赖于低层次模块,模块的实现都应该依赖于抽象(接口),而抽象(接口)能够屏蔽功能设计上的细节。

  • 对循环依赖的自动检测

如果您负责的产品是遗留产品。在经过多个设计人员更替后,产品内部设计或多或少出现了一些循环依赖问题,这时该怎么办?您要做的首先是检查产品的哪些模块出现了循环依赖,再思考修改方法。目前市面上有很多这样的工具,可以帮助您检查代码层面和系统模块层面的依赖关系是否良好,这里笔者推荐SonarQube。以下截图是笔者经历过的一个项目中,某个子模块通过SonarQube进行检测的结果:

呵呵,V0.0.9版本,看来还有若干个类的复杂度偏高,说明模块中使用的设计模式还有改进空间。好吧,这个子系统只是开了一个头,目前已经发展到V0.5.3的版本号,代码行数也快接近5万行了。不过作为这个子模块的作者之一,本人自认为包耦合度一直控制得很好,直到现在也没有出现包循环依赖的情况。

  • 抽离底层能引导业务人员优化依赖结构

在上一小节“功能层面的循环依赖”中我们举了一个司机-货车-发车单三个元素在业务功能层面的循环依赖问题。现在我们接着这个问题继续讨论。客户之所以要在新的“发车单”创建后,才解除货车和历史“发车单”的绑定关系,是因为用户担心软件失去对历史“发车单”的跟踪能力,最后无法统计车辆使用率或者司机绩效情况。但实际上,客户完全无需担心出现这样的情况,了解客户的真实想法后,需求人员就能引导客户开辟一个新的日志模块,这个日志模块处于整个业务系统设计的更底层,专门跟踪各种历史行为,车辆的、人员的、财务的、货品的,都行:

我们可以用这种剥离循环依赖元素中底层能力的方式,解决循环依赖问题。这种解决思路不但适用于业务系统功能间的解耦,同样适用于代码级别或者系统顶层架构级别的解耦

以上是关于架构案例-依赖倒置循环依赖解耦的主要内容,如果未能解决你的问题,请参考以下文章

架构案例-依赖倒置循环依赖解耦

架构案例-依赖倒置循环依赖解耦

架构案例-依赖倒置循环依赖解耦

我曾想深入了解的:依赖倒置控制反转依赖注入

设计模式-- 依赖倒置DIP(例解-从编码到服务级解耦)

设计模式-- 依赖倒置DIP(例解-从编码到服务级解耦)