如何解决代码循环依赖问题?

今天跟大家一起探讨在日常开发过程中经常会碰到的一个问题这个问题跟代码的维护工作有很大关系我们知道任何系统在开发了一段时间之后,随着业务功能和代码数量的不断增加,代码之间的调用和被调用场景也会逐渐变的越来越复杂。各个类或组件之间可能会存在出乎开发人员想象的复杂关系。


代码之间复杂关系的一种常见形式就是类与类之间存在循环依赖关系。所谓循环依赖,简单来说就是一个类A会引用类B中的方法,而反过来类B同样也会引用类A中的方法,从而导致类A和类B之间存在相互引用,从而形成一种循环依赖。


合理的系统架构以及持续的重构优化工作能够减轻这种复杂关系,但如何有效识别系统中存在的循环依赖仍然是开发人员面临的一个很大的挑战。主要原因在于类之间的依赖关系存在传递性。如果系统中只存在类A和类B,那么它们之间的依赖关系就非常容易识别。但如果再引入一个类C,那么这三个类之间关系的组合就有很多种情况。可以想象一下,如果一个系统中存在几十个类,那么它们之间的依赖关系就很难通过简单的关系图进行一一列举。而一般的系统中的类的个数显然不止几十个。


更宽泛的讲,类之间的这种循环依赖关系同样也可以扩展到组件级别。组件之间的循环依赖关系的产生原因在于组件1中的类与组件2中的类之间存在依赖关系,从而导致组件与组件之间产生一种循环依赖。


在软件设计领域存在一条公认的设计原则这就是无环依赖原则。无环依赖原则认为在组件之间不应该存在循环依赖关系。通过将系统划分为不同的可发布组件,对某一个组件的修改所产生的影响可以不扩展到其他组件。

<<<

ADP(The Acyclic Dependencies Principle,无环依赖原则)

The dependencies between packages must not form cycles.

在包的依赖关系图中不允许存在环。

The dependency structure between packages must be a directed acyclic graph (DAG). That is, there must be no cycles in the dependency structure.

包之间的依赖结构必须是一个直接的无环图形(DAG)。也就是说,在依赖结构中不允许出现环(循环依赖)。

>>>

下面,我们将通过一个具体的代码示例来介绍组件之间循环依赖的产生过程。这个代码示例描述了医疗健康类系统中的一个常见场景,每个用户都有一份健康档案,存储着代表用户当前健康状况的健康等级,以及一系列的健康任务。用户每天可以通过完成医生所指定的任务来获取一定的健康积分,而这个积分的计算过程取决于该用户当前的等级。也就是说,不同的等级下同一个任务所能获取的积分也是不一样的。反过来,等级的计算也取决于该用户当前需要完成的任务数量,任务越多说明越不健康,等级就越低


针对这个场景,我们可以抽象出两个类,一个是代表健康档案的HealthRecord类,一个是代表健康任务的HealthTask类。我们先来看HealthRecord类,这个类里面包含着一个HealthTask列表以及添加HealthTask的方法,同样也包含一个获取等级的方法,这个方法根据任务数量来判断等级

public class HealthRecord {

private List<HealthTask> tasks = new ArrayList<HealthTask>();

public Integer getHealthLevel() {

//根据健康任务数量来判断健康等级

//任务越多说明越不健康,健康等级就越低

if(tasks.size() > 5) {

return 1;

}

if(tasks.size() < 2) {

return 3;

}

return 2;

}

public void addTask(String taskName, Integer initialHealthPoint) {

HealthTask task = new HealthTask(this, taskName, initialHealthPoint);

tasks.add(task);

}

public List<HealthTask> getTasks() {

return tasks;

}

}

对应的HealthTask中显然应该包含对HealthRecord的引用,同时也实现了一个方法来计算该任务所能获取的积分这个就需要使用到HealthRecord中的等级信息。

public class HealthTask {

private HealthRecord record;

private String taskName;

private Integer initialHealthPoint;

public HealthTask(HealthRecord record, String taskName, Integer initialHealthPoint) {

this.record = record;

this.taskName = taskName;

this.initialHealthPoint = initialHealthPoint;

}

public Integer calculateHealthPointForTask() {

//计算该任务所能获取的积分需要等级信息

//等级越低积分越高,以鼓励多做任务

Integer healthPointFromHealthLevel = 12 / record.getHealthLevel();

//最终积分为初始积分加上与等级相关的积分

return initialHealthPoint + healthPointFromHealthLevel;

}

public String getTaskName() {

return taskName;

}

public int getInitialHealthPoint() {

return initialHealthPoint;

}

}

从代码中,我们不难看出HealthRecord和HealthTask之间存在明显的相互依赖关系。那么有没有工具能够自动化的帮助我们来识别它们之间存在的这种循环依赖关系呢答案是肯定的业界存在一款优秀的开源工具专门用于量化代码的各种度量标准其中就包括了代码循环依赖分析这款工具就是JDepend。我们可以使用JDepend来对包含HealthRecord和HealthTask类的包结构进行分析得到系统中存在循环依赖代码的提示。


对于循环依赖,JDepend给出了四个子页面,分别是所选中的包、存在循环依赖关系的包、所依赖的包和被依赖的包。我们可以使用JDepend来自动分析各个开源框架中存在的依赖关系。例如,在我们熟悉的Dubbo框架dubbo-rpc-api代码工程中,实际上也存在循环依赖的代码。


现在,我们已经解决了如何有效识别代码中存在的循环依赖这个重要问题。接下来,我们就将讨论如何消除代码中的这些循环依赖。软件行业有一句很经典的话,即当我们碰到问题无从下手时,不妨考虑一下是否可以通过“加一层”的方法进行解决。消除循环依赖的基本思路也是一样,我们有三种常见的方法,分别是提取中介者、转移业务逻辑和采用回调接口。

我们先来看第一种方法提取中介者。提取中介者的核心思想是把两个相互依赖的组件中的交互部分抽象出来形成一个新的组件,而新组件同时包含着原有两个组件的引用,这样就把循环依赖关系剥离出来并提取到一个专门的中介者组件中。


这个中介者组件的实现也非常简单通过提供一个计算积分的方法来对循环依赖进行了剥离,该方法同时依赖于HealthRecord和HealthTask对象,并实现了原有HealthTask中根据HealthRecord的等级信息进行积分计算的业务逻辑。

public class HealthPointMediator {

private HealthRecord record;

public HealthPointMediator(HealthRecord record) {

this.record = record;

}

public Integer calculateHealthPointForTask(HealthTask task) {

Integer healthLevel = record.getHealthLevel();

Integer initialHealthPoint = task.getInitialHealthPoint();

Integer healthPoint = 12 / healthLevel + initialHealthPoint;

return healthPoint;

}

}

这个时候HealthTask就变得非常简单,已经不包含任何有关HealthRecord的依赖信息

public class HealthTask {

private String taskName;

private Integer initialHealthPoint;

public HealthTask(String taskName, Integer initialHealthPoint) {

this.taskName = taskName;

this.initialHealthPoint = initialHealthPoint;

}

public String getTaskName() {

return taskName;

}

public Integer getInitialHealthPoint() {

return initialHealthPoint;

}

}

然后,我们来针对“提取中介者”这种消除循环依赖的实现方法来编写测试用例。我们在HealthRecord中创建了6个HealthTask并赋予不同的积分初始值,然后通过HealthPointMediator这个中介者来分别对每个Task计算积分

public class HealthPointTest {

public static void main(String[] args) {

HealthRecord record = new HealthRecord();

record.addTask("忌烟酒", 5);

record.addTask("一周慢跑三次", 4);

record.addTask("一天喝两升水", 2);

record.addTask("坐1小时起来活动5分钟", 2);

record.addTask("晚上10点按时睡觉", 3);

record.addTask("晚上8点之后不再饮食", 1);

HealthPointMediator mediator = new HealthPointMediator(record);

List<HealthTask> tasks = record.getTasks();

for(HealthTask task : tasks) {

Integer healthPoint = mediator.calculateHealthPointForTask(task);

System.out.print(healthPoint);

}

}

}

最后,我们同样可以再次运行JDepend来获取当前的代码依赖关系。这次,我们发现代码中已经不存在任何依赖环了。


我们继续介绍第二种消息循环依赖的方法,这就是转移业务逻辑。这种方法的实现思路在于提取一个专门的业务组件来完成对等级的计算过程。这样,HealthTask原有的对HealthRecord的依赖就转移到了对这个业务组件的依赖,而这个业务组件本身不需要依赖任何对象。


HealthLevelHandler这个业务组件的实现过程同样非常简单,包含了对等级的计算过程。

public class HealthLevelHandler {

private Integer taskCount;

public HealthLevelHandler(Integer taskCount) {

this.taskCount = taskCount;

}

public Integer getHealthLevel() {

if(taskCount > 5) {

return 1;

}

if(taskCount < 2) {

return 3;

}

return 2;

}

}

随着业务组件的提取,HealthRecord需要做相应的改造,这里封装了对HealthLevelHandler的创建过程

public class HealthRecord {

private List<HealthTask> tasks = new ArrayList<HealthTask>();

public void addTask(String taskName, Integer initialHealthPoint) {

HealthTask task = new HealthTask(taskName, initialHealthPoint);

tasks.add(task);

}

public HealthLevelHandler getHealthPointHandler() {

return new HealthLevelHandler(new Integer(tasks.size()));

}

public List<HealthTask> getTasks() {

return tasks;

}

}

同样,对应的HealthTask也需要进行改造,添加了对HealthLevelHandler的使用过程。

public class HealthTask {

private String taskName;

private Integer initialHealthPoint;

public HealthTask(String taskName, Integer initialHealthPoint) {

this.taskName = taskName;

this.initialHealthPoint = initialHealthPoint;

}

public Integer calculateHealthPointForTask(HealthLevelHandler handler) {

Integer healthPointFromHealthLevel = 12 / handler.getHealthLevel();

return initialHealthPoint + healthPointFromHealthLevel;

}

public String getTaskName() {

return taskName;

}

}

最后,我们需要完成对测试类的改造。现在,HealthTask和HealthRecord都已经只剩下对HealthLevelHandler的依赖。如果我们执行JDepend,同样也会发现系统中不存在任何循环依赖。

public class HealthPointTest {

public static void main(String[] args) {

HealthRecord record = new HealthRecord();

record.addTask("忌烟酒", 5);

record.addTask("一周慢跑三次", 4);

record.addTask("一天喝两升水", 2);

record.addTask("坐1小时起来活动5分钟", 2);

record.addTask("晚上10点按时睡觉", 3);

record.addTask("晚上8点之后不再饮食", 1);

HealthLevelHandler handler = record.getHealthPointHandler();

List<HealthTask> tasks = record.getTasks();

for(HealthTask task : tasks) {

Integer healthPoint = task.calculateHealthPointForTask(handler);

System.out.print(healthPoint);

}

}

}

介绍完了提取中介者和转移业务逻辑之后,我们来看最后一种消除循环依赖的方法,这种方法会采用回调接口。所谓回调本质上就是一种双向调用模式,也就是说,被调用方在被调用的同时也会调用对方。在实现上,我们可以提取一个用于计算等级的业务接口,然后让HealthRecord去实现这个接口。这样,HealthTask在计算积分时只需要依赖这个业务接口,而不需要关心这个接口的具体实现类。


我们同样将这个接口命名为HealthLevelHandler,包含一个计算等级的方法定义。

public interface HealthLevelHandler {

Integer getHealthLevel();

}

有了这个接口,HealthTask中就不再存在对HealthRecord的任何依赖而是在构造函数中注入这个Handler接口。在计算积分时我们也只会使用这个接口所提供的方法

public class HealthTask {

private String taskName;

private Integer initialHealthPoint;

private HealthLevelHandler handler;

public HealthTask(String taskName, Integer initialHealthPoint, HealthLevelHandler handler) {

this.taskName = taskName;

this.initialHealthPoint = initialHealthPoint;

this.handler = handler;

}

public Integer calculateHealthPointForTask() {

Integer healthPointFromHealthLevel = 12 / handler.getHealthLevel();

return initialHealthPoint + healthPointFromHealthLevel;

}

public String getTaskName() {

return taskName;

}

}

而现在的HealthRecord就需要实现该接口,并提供计算等级的具体业务逻辑。同时在创建HealthTask时HealthRecord需要把自己作为一个参数传入到HealthTask的构造函数中

public class HealthRecord implements HealthLevelHandler {

private List<HealthTask> tasks = new ArrayList<HealthTask>();

@Override

public Integer getHealthLevel() {

if(tasks.size() > 5) {

return 1;

}

if(tasks.size() < 2) {

return 3;

}

return 2;

}

public void addTask(String taskName, Integer initialHealthPoint) {

HealthTask task = new HealthTask(taskName, initialHealthPoint, this);

tasks.add(task);

}

public List<HealthTask> getTasks() {

return tasks;

}

}

就这样,我们通过回调方法完成了对系统的改造。采用这样方法,测试用例的代码也会变得更加简洁,我们没有发现除HealthRecord和HealthTask之外的任何第三方对象。我们同样可以通过使用JDepend来验证当前系统中是否还存在循环依赖关系

public class HealthPointTest {

public static void main(String[] args) {

HealthRecord record = new HealthRecord();

record.addTask("忌烟酒", 5);

record.addTask("一周慢跑三次", 4);

record.addTask("一天喝两升水", 2);

record.addTask("坐1小时起来活动5分钟", 2);

record.addTask("晚上10点按时睡觉", 3);

record.addTask("晚上8点之后不再饮食", 1);

List<HealthTask> tasks = record.getTasks();

for(HealthTask task : tasks) {

Integer healthPoint = task.calculateHealthPointForTask();

System.out.print(healthPoint);

}

}

}

在日常开发过程中,前面介绍的三种消除循环依赖的方法都可以根据具体场景进行灵活应用。作为总结,我们来梳理一下与循环依赖相关的各个知识点。我们介绍了循环依赖的定义以及无环依赖设计原则。然后我们给出了如何自动识别系统中存在的循环依赖的方法和工具。最后,我们基于代码示例详细阐述了消除循环依赖的三种方法。

  • 38
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 循环依赖指两个或多个模块相互依赖,且彼此之间形成了一个闭环的依赖关系。在编译、构建或运行程序时,循环依赖可能会导致无法正确加载、编译或执行代码解决循环依赖的方法包括: 1. 重构代码结构:将依赖关系打破,使得各个模块之间不再相互依赖。 2. 引入中间件:引入一个新的模块或组件,作为两个或多个模块之间的中间层,从而避免直接相互依赖。 3. 延迟加载:延迟加载某些模块,直到真正需要使用它们时再进行加载,以避免循环依赖。 4. 依赖注入:通过依赖注入框架,将依赖的对象注入到需要使用它们的模块中,从而避免循环依赖。 需要注意的是,解决循环依赖问题并不是一件简单的事情,需要结合实际情况进行具体分析和处理。 ### 回答2: 循环依赖是指在一个系统或者模块间存在相互依赖的情况,其中一个模块依赖于另一个模块,而该模块又依赖于第一个模块,形成了一个循环的依赖关系。 循环依赖会导致以下问题:首先,造成死锁,因为两个模块都在等待对方完成;其次,增加了开发和维护的复杂性,因为难以准确地确定模块间的先后顺序;最后,降低了系统的灵活性,因为任何一个模块的变更都可能会影响到其他所有依赖于它的模块。 要解决循环依赖问题,可以采取以下几种方法: 1. 重构代码结构:重新组织代码结构,通过拆分或合并模块来消除循环依赖。这个过程中,需要仔细分析模块之间的依赖关系,找出冗余的依赖,减少它们之间的耦合度。 2. 引入中间层:在循环依赖的模块之间引入中间层,将循环依赖改为单向依赖。这样,就能够确保模块间的依赖关系按照正确的顺序进行。 3. 使用回调函数:将模块之间的相互调用改为通过回调函数来实现。这样,模块的调用顺序不再依赖于相互之间的依赖关系,可以更加灵活地控制程序的执行。 4. 引入观察者模式:将循环依赖的模块之间引入观察者模式,通过事件的发布和订阅机制来解耦模块间的依赖关系。 总之,解决循环依赖问题需要深入分析模块之间的依赖关系,并采取适当的措施来降低模块间的耦合度,从而达到消除循环依赖的目的。 ### 回答3: 循环依赖是指两个或多个对象之间互相依赖,形成了一个闭环,导致程序无法正常执行或产生错误的情况。 循环依赖通常出现在对象之间的相互引用上。举例来说,对象A引用了对象B,而对象B又引用了对象A,它们之间形成了一个循环依赖。在这种情况下,当我们在创建或使用这些对象时,程序可能会陷入无限循环,或者无法正确地处理对象间的依赖关系。 要解决循环依赖问题,可以考虑以下几种方法: 1. 重构代码结构:分析循环依赖的原因,重新设计代码结构,将相互依赖的部分提取到一个单独的模块或类中,减少或避免循环依赖的发生。 2. 使用接口或抽象类:通过引入接口或抽象类,将具体实现类抽象出来,降低了依赖的耦合度,从而可以在不产生循环依赖的情况下正常使用对象。 3. 引入中间层或事件驱动机制:通过引入中间层或事件驱动机制,将对象之间的直接依赖转变为间接依赖,避免了循环依赖的发生。例如,使用消息队列或事件总线来解耦对象之间的依赖关系。 4. 使用依赖注入框架:依赖注入框架可以帮助管理对象之间的依赖关系,通过外部容器来管理对象的创建和注入,从而减少了手动管理依赖的复杂性,避免了循环依赖问题。 总之,解决循环依赖问题需要对代码结构进行合理设计,降低依赖的耦合度,引入合适的设计模式或框架,从而实现代码的灵活性和可维护性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值