解放代码:识别与消除循环依赖的实战指南

目录

一、对循环依赖的基本认识

(一)代码中形成循环依赖的说明

(二)无环依赖的原则

二、识别和消除循环依赖的方法

(一)使用JDepend识别循环依赖

[使用 Maven 集成 JDepend](#使用 Maven 集成 JDepend)

分析报告识别循环依赖

(二)消除循环依赖的三大方法思考

提取中介者

转移业务逻辑

采用回调接口

三、案件实战分析

(一)具体案列介绍

[HealthRecord 类](#HealthRecord 类)

[HealthTask 类](#HealthTask 类)

循环依赖的产生

(二)具体解决方案一:提取中介者

提取中介者的实现

[简化的 HealthTask 类](#简化的 HealthTask 类)

测试用例示例

(三)具体解决方案二:转移业务逻辑

转移业务逻辑的实现

[改造后的 HealthRecord 类](#改造后的 HealthRecord 类)

[改造后的 HealthTask 类](#改造后的 HealthTask 类)

测试用例示例

(四)具体解决方案三:采用回调接口

使用回调接口的实现

[HealthLevelHandler 接口的定义](#HealthLevelHandler 接口的定义)

[改造后的 HealthTask 类](#改造后的 HealthTask 类)

[改造后的 HealthRecord 类](#改造后的 HealthRecord 类)

测试用例示例

参考书籍、文献和链接等


本文讨论软件开发中常见的循环依赖问题及其解决方法。首先介绍了循环依赖在代码中的形成原因,并提出了避免循环依赖的基本原则。其次,详细介绍了使用工具如JDepend来识别项目中的循环依赖,并通过具体案例分析了三种消除循环依赖的方法:提取中介者、转移业务逻辑和采用回调接口。每种方法都结合了实际的代码改造示例和测试用例,帮助读者理解和应用这些技术以优化自己的软件架构和设计。主要思想的编排思路来自极客时间《如何有效识别和解决代码中存在的循环依赖问题?》,当然也有其他的参考和自身的一些思考和优化。

一、对循环依赖的基本认识

循环依赖是指两个或多个模块或对象彼此之间相互依赖形成闭环,如 A 依赖 B,B 又依赖 A。这就像是两个人互相拉着对方,无法确定谁先开始走。

循环依赖最大的问题是,在运行时,可能会导致无限递归、栈溢出或者对象创建循环等问题,因为每个类的构造函数都试图创建依赖的对象,形成了一个死锁。所以一般情况下我们都希望在编译时,编译器就可能会报告循环依赖错误或无法解析的问题。

(一)代码中形成循环依赖的说明

循环依赖是指两个或多个模块、类、或组件之间相互依赖形成闭环。这种情况可能导致编译错误、运行时错误,甚至导致系统崩溃或无法正常工作。随着系统复杂性的增加,循环依赖问题可能会变得更加严重和难以检测。

简单来说,假设当前只有存在两个类A和类B,这个时候的识别还是较简单的,但如果此时在增加C,那么这个时候其可能产生依赖的情况就会很多了,比如如下的三种情况:

显然我们平时的系统中不会只是三个类这么简单,在多类情况下这种依赖问题就会不断放大(随着业务发展只会不断扩大),比如简单的依赖会变为如下依赖:

现实中,随着类和模块数量的增加,系统中的依赖关系会变得复杂,容易形成循环依赖,不仅增加了系统的复杂性和维护难度,还可能导致运行时错误和系统不稳定。因此,在设计和实现系统时,应尽量避免循环依赖,并采取合适的解决策略来管理依赖关系。

(二)无环依赖的原则

无环依赖原则(ADP)规定,在系统的依赖关系图中,不应该存在任何依赖环。换句话说,一个模块或组件的依赖链中不应出现环形依赖结构。这样,任何一个模块的变更不会导致循环依赖的问题,从而避免了系统的复杂性和难以维护性。

也就是基本原则要求如下:

  • 在软件设计中,模块之间的依赖关系可以用图来表示,节点表示模块,边表示依赖关系。无环依赖原则要求这个图必须是一个有向无环图(DAG)。
  • 通过确保依赖关系是无环的,可以防止系统中出现依赖的闭环,避免模块之间的强耦合。这样,修改一个模块不会引发对其依赖模块的连锁反应。

二、识别和消除循环依赖的方法

识别和消除循环依赖是软件开发中的一个重要任务,可以通过重构代码、引入中介者模式、使用接口和依赖注入,以及遵循设计原则等方法来实现。借助 Maven Enforcer Plugin、Madge、Deptrac、JDepend、PyDepGraph 和 SonarQube 等开源工具,可以更高效地检测和管理项目中的依赖关系,从而提高代码的可维护性和可扩展性。

(一)使用JDepend识别循环依赖

JDepend 是一个用于分析 Java 包之间依赖关系的工具。它能够生成报告,帮助开发者理解项目的架构和依赖关系,并识别可能的设计问题,如循环依赖。JDepend 提供了关于包的耦合度、内聚性、抽象度和稳定性等方面的信息,从而帮助改进代码质量和系统结构。可以从 JDepend 的官方网站 下载 JDepend,并将 JDepend 集成到构建工具中,例如 Maven 或 Gradle,以便在构建过程中自动分析依赖关系。

使用 Maven 集成 JDepend

在Maven 项目的 pom.xml 文件中添加 JDepend 插件:

java 复制代码
<project>
  <!-- 其他配置 -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>jdepend-maven-plugin</artifactId>
        <version>2.0-beta-2</version>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

然后,运行以下命令来执行 JDepend 分析:

mvn jdepend:generate

分析报告识别循环依赖

JDepend 生成的报告包括以下几个方面:

  • 包的稳定性:度量包的变化频率,越不稳定的包越容易发生变化。
  • 包的抽象度:度量包的抽象程度,抽象包通常包含更多的接口和抽象类。
  • 包的耦合度:度量包之间的依赖关系。
  • 循环依赖:检测包之间是否存在循环依赖。

一旦生成 JDepend 报告就可以在报告中查找是否存在循环依赖,循环依赖通常会被标记为 "Cycles" 或 "Dependency Cycles":

(二)消除循环依赖的三大方法思考

提取中介者

通过引入一个独立的中介者类来管理和协调其他类之间的交互,从而避免直接的循环依赖。

适用于需要集中管理复杂依赖关系的场景,可以有效地降低类之间的耦合度,增强系统的可维护性和灵活性。

转移业务逻辑

将涉及循环依赖的业务逻辑转移到一个独立的类或模块中,从而解除类之间的直接依赖关系。

适用于可以重构业务逻辑的场景,通过模块化设计提高系统的可理解性和可维护性。

采用回调接口

通过定义接口来抽象依赖关系,使类之间的依赖通过接口进行交互,从而避免直接的循环依赖。

适用于需要解耦具体实现的场景,可以增强系统的扩展性和灵活性。

三、案件实战分析

(一)具体案列介绍

在医疗健康类系统中,每个用户都有一份健康档案,记录着他们当前的健康状况(以健康等级表示)和一系列健康任务。用户每天可以通过完成医生指定的任务来获得健康积分。健康积分的计算取决于用户的当前等级,不同等级下完成同一个任务获得的积分不同。同时,用户的健康等级也取决于他们当前需要完成的任务数量,任务越多说明越不健康,等级也就越低。

针对这个场景,我们可以抽象出两个类:HealthRecordHealthTask

HealthRecord

HealthRecord 类代表健康档案,包含一个 HealthTask 列表以及添加 HealthTask 的方法。这个类还包含一个获取健康等级的方法,该方法根据任务数量来判断用户的健康等级。

java 复制代码
import java.util.ArrayList;
import java.util.List;

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

HealthTask 类代表健康任务,它包含对 HealthRecord 的引用,并实现了一个计算任务所能获得积分的方法,这个方法需要使用 HealthRecord 中的健康等级信息。

java 复制代码
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;
    }
}

循环依赖的产生

从代码中可以看出,HealthRecordHealthTask 之间存在循环依赖:

  • HealthRecord 需要知道 HealthTask 列表来确定健康等级。
  • HealthTask 需要引用 HealthRecord 来计算任务的健康积分。

这种循环依赖在系统扩展和维护时会带来很多问题,例如增加新的功能可能导致意想不到的耦合和复杂度。

(二)具体解决方案一:提取中介者

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

在医疗健康类系统中,我们面对 HealthRecordHealthTask 两个类之间的循环依赖问题:HealthRecord 类负责管理健康档案和健康任务列表,同时根据任务数量确定用户的健康等级。而 HealthTask 类则依赖于 HealthRecord 的健康等级信息来计算任务的健康积分,这导致了双向的依赖关系。

提取中介者的实现

为消除 HealthRecordHealthTask 的直接依赖,引入中介者类 HealthPointMediator。该中介者类负责协调 HealthRecordHealthTask 的交互,并提供了计算任务健康积分的方法。

java 复制代码
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

为了减少 HealthTaskHealthRecord 的依赖,可简化 HealthTask 类的实现。现在它专注于任务的描述和初始化积分,不再直接引用 HealthRecord

java 复制代码
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;
    }
}

测试用例示例

编写了一个简单的测试用例来验证 HealthPointMediator 的功能,确保它能正确计算每个任务的健康积分。

java 复制代码
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.println("任务:" + task.getTaskName() + ",积分:" + healthPoint);
        }
    }
}

通过提取中介者的方法,我们成功地消除了 HealthRecordHealthTask 之间的循环依赖关系。中介者模式使得系统更加灵活和可扩展,同时降低了类之间的耦合度,提高了代码的可维护性和可读性。这种设计方式不仅符合面向对象设计的原则,也使得系统更易于理解和维护。

(三)具体解决方案二:转移业务逻辑

这种方法的实现思路在于提取一个专门的业务组件来完成对等级的计算过程。这样,HealthTask原有的对HealthRecord的依赖就转移到了对这个业务组件的依赖,而这个业务组件本身不需要依赖任何对象。

转移业务逻辑的实现

提取一个专门的业务组件 HealthLevelHandler,负责计算健康等级而不依赖于任何其他对象。这样一来,HealthTask 类的原有对 HealthRecord 的依赖转移到了对 HealthLevelHandler 的依赖,从而消除了循环依赖。

java 复制代码
public class HealthLevelHandler {
    private Integer taskCount;

    public HealthLevelHandler(Integer taskCount) {
        this.taskCount = taskCount;
    }

    public Integer getHealthLevel() {
        if (taskCount > 5) {
            return 1;
        } else if (taskCount < 2) {
            return 3;
        } else {
            return 2;
        }
    }
}

改造后的 HealthRecord

HealthRecord 类现在封装了对 HealthLevelHandler 的创建过程,并提供了获取健康任务列表的方法。

java 复制代码
public class HealthRecord {
    private List<HealthTask> tasks = new ArrayList<>();

    public void addTask(String taskName, Integer initialHealthPoint) {
        HealthTask task = new HealthTask(taskName, initialHealthPoint);
        tasks.add(task);
    }

    public HealthLevelHandler getHealthPointHandler() {
        return new HealthLevelHandler(tasks.size());
    }

    public List<HealthTask> getTasks() {
        return tasks;
    }
}

改造后的 HealthTask

HealthTask 类改为接受 HealthLevelHandler 对象作为参数,并利用其计算任务的健康积分。

java 复制代码
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;
    }
}

测试用例示例

编写一个简单的测试用例来验证改造后的 HealthRecordHealthTask 的功能。现在,系统中不存在任何循环依赖,且代码更易于理解和维护。

java 复制代码
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.println("任务:" + task.getTaskName() + ",积分:" + healthPoint);
        }
    }
}

通过转移业务逻辑的方法,我们成功消除了 HealthRecordHealthTask 之间的循环依赖。通过引入 HealthLevelHandler 业务组件,我们将等级计算的责任集中在一个地方,简化了系统的设计并提高了其灵活性和可维护性。这种设计方式符合单一职责原则,使得各个类的功能更加清晰和独立。

(四)具体解决方案三:采用回调接口

所谓回调本质上就是一种双向调用模式,也就是说,被调用方在被调用的同时也会调用对方。在实现上,我们可以提取一个用于计算等级的业务接口,然后让HealthRecord去实现这个接口。这样,HealthTask在计算积分时只需要依赖这个业务接口,而不需要关心这个接口的具体实现类。

使用回调接口的实现

首先定义了一个名为 HealthLevelHandler 的接口,该接口包含了计算健康等级的方法声明,作为回调接口使用。接着,HealthRecord 类实现了这个接口,并提供了具体的等级计算逻辑。在创建 HealthTask 对象时,我们将 HealthRecord 对象作为参数传入 HealthTask 的构造函数,并在 HealthTask 中使用 HealthLevelHandler 接口来获取健康等级信息,从而消除了对 HealthRecord 的直接依赖。

HealthLevelHandler 接口的定义
java 复制代码
public interface HealthLevelHandler {
    Integer getHealthLevel();
}
改造后的 HealthTask

HealthTask 类不再直接依赖 HealthRecord,而是依赖 HealthLevelHandler 接口,并通过该接口获取健康等级信息来计算任务的健康积分。

java 复制代码
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

HealthRecord 类实现了 HealthLevelHandler 接口,并提供了具体的健康等级计算逻辑。在创建 HealthTask 对象时,将 this 作为 HealthLevelHandler 的实例传入 HealthTask 的构造函数。

java 复制代码
public class HealthRecord implements HealthLevelHandler {
    private List<HealthTask> tasks = new ArrayList<>();

    @Override
    public Integer getHealthLevel() {
        if (tasks.size() > 5) {
            return 1;
        } else if (tasks.size() < 2) {
            return 3;
        } else {
            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;
    }
}

测试用例示例

编写一个简单的测试用例来验证改造后的 HealthRecordHealthTask 的功能。现在,系统中不存在任何循环依赖,且代码更易于理解和维护。

java 复制代码
public class HealthRecord implements HealthLevelHandler {
    private List<HealthTask> tasks = new ArrayList<>();

    @Override
    public Integer getHealthLevel() {
        if (tasks.size() > 5) {
            return 1;
        } else if (tasks.size() < 2) {
            return 3;
        } else {
            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;
    }
}

通过引入 HealthLevelHandler 接口,HealthTask 类不再直接依赖于 HealthRecord,而是依赖于一个通用的接口,提高了系统的灵活性和可维护性。这种设计方式符合面向接口编程的思想,使得各个组件之间的耦合度降低,同时也使得代码更具扩展性和测试性。

参考书籍、文献和链接等

如何有效识别和解决代码中存在的循环依赖问题?-极客时间

JDepend 的官方网站

GitHub - clarkware/jdepend: A Java package dependency analyzer that generates design quality metrics.

JDepend Task

Managing Your Dependencies with JDepend -- Craftsmanship

六、如何解决循环依赖_循环依赖解决方式-CSDN博客

https://jiapan.me/2020/circular-dependence/

https://blog.51cto.com/u_16038001/6159153

中介者模式解决Spring循环依赖_哔哩哔哩_bilibili

Spring Boot 系统学习第四天:Spring循环依赖案例分析_springboot 循环以来例子-CSDN博客

深入理解循环依赖:如何避免和解决

相关推荐
morris1314 分钟前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
龙哥说跨境21 分钟前
如何利用指纹浏览器爬虫绕过Cloudflare的防护?
服务器·网络·python·网络爬虫
七星静香29 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员30 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU30 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie634 分钟前
在IDEA中使用Git
java·git
Elaine2023911 小时前
06 网络编程基础
java·网络
G丶AEOM1 小时前
分布式——BASE理论
java·分布式·八股
落落鱼20131 小时前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀1 小时前
LRU缓存算法
java·算法·缓存