首页 > 其他分享 >解放代码:识别与消除循环依赖的实战指南

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

时间:2024-06-18 14:28:24浏览次数:27  
标签:指南 实战 依赖 public HealthTask Integer 识别 HealthRecord initialHealthPoint

目录

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

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

(二)无环依赖的原则

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

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

使用 Maven 集成 JDepend

分析报告识别循环依赖

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

提取中介者

转移业务逻辑

采用回调接口

三、案件实战分析

(一)具体案列介绍

HealthRecord 类

HealthTask 类

循环依赖的产生

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

提取中介者的实现

简化的 HealthTask 类

测试用例示例

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

转移业务逻辑的实现

改造后的 HealthRecord 类

改造后的 HealthTask 类

测试用例示例

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

使用回调接口的实现

HealthLevelHandler 接口的定义

改造后的 HealthTask 类

改造后的 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 插件:

<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 的方法。这个类还包含一个获取健康等级的方法,该方法根据任务数量来判断用户的健康等级。

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 中的健康等级信息。

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 的交互,并提供了计算任务健康积分的方法。

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

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 的功能,确保它能正确计算每个任务的健康积分。

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 的依赖,从而消除了循环依赖。

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 的创建过程,并提供了获取健康任务列表的方法。

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 对象作为参数,并利用其计算任务的健康积分。

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 的功能。现在,系统中不存在任何循环依赖,且代码更易于理解和维护。

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 接口的定义
public interface HealthLevelHandler {
    Integer getHealthLevel();
}
改造后的 HealthTask

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

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 的构造函数。

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 的功能。现在,系统中不存在任何循环依赖,且代码更易于理解和维护。

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博客

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

标签:指南,实战,依赖,public,HealthTask,Integer,识别,HealthRecord,initialHealthPoint
From: https://blog.csdn.net/xiaofeng10330111/article/details/139756774

相关文章

  • 从零开始学SQL注入(sql十大注入类型):技术解析与实战演练
    从零开始学SQL注入(sql十大注入类型):技术解析与实战演练环境工具:burpsuite靶场:sqli服务器:centos7数据库:mysql5.7什么是Sql注入?SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的BUG来实现攻击,而是针对程序员编写时的疏忽,通过SQL语句,实现无账号登录,甚至篡......
  • 公有云--web集群部署实战
    公有云--web集群部署实战今日目标:-华为云网站部署实战-华为云负载均衡ELB-ELASTICSEARCH安装部署Web集群架构图Web集群规划:-NFS共享存储-web-0001、web-0002、web-0003、-ELB负载均衡-跳板机(管理主机)部署Web集群1.购买云主机主机名称IP地址nfs192.168.1.10we......
  • 购物车实战学习
    <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0"><title>购物车实战</title>&l......
  • 使用 Apache JMeter 调度器的详细指南
    前言ApacheJMeter是一个用于性能测试的开源工具,广泛用于模拟各种场景下的负载测试。调度器(Scheduler)是JMeter的一个重要功能,可以帮助测试人员在指定的时间段内执行测试计划。本指南将详细介绍如何配置和使用JMeter的调度器。启动JMeter在命令行中导航到JMeter的bin......
  • 使用 Apache JMeter Flexible File Writer 插件的详细指南
    简介ApacheJMeter是一个强大的开源工具,广泛用于性能测试和负载测试。为了更好地记录和分析测试结果,JMeter提供了多个监听器(Listener)来收集数据。FlexibleFileWriter是一个非常有用的插件,它允许用户以自定义格式将测试结果写入文件中。本指南将详细介绍如何安装、配置和使用......
  • 使用 Apache JMeter 事务控制器的详细指南
    简介ApacheJMeter是一个用于负载测试和性能测试的强大开源工具。逻辑控制器(LogicControllers)是JMeter的重要组成部分,帮助用户定义请求的执行逻辑。事务控制器(TransactionController)是一种常用的逻辑控制器,用于将多个请求组合在一起,并将它们作为一个单独的事务进行计时。本......
  • 使用 Apache JMeter 吞吐量控制器的详细指南
    简介ApacheJMeter是一个用于负载测试和性能测试的强大开源工具。逻辑控制器(LogicControllers)是JMeter的重要组成部分,帮助用户定义请求的执行逻辑。吞吐量控制器(ThroughputController)是其中一种,用于控制采样器执行的频率,以实现特定的吞吐量目标。本指南将详细介绍如何配置和......
  • 多模态大模型:识别和处理图片与视频的技术详解
    随着人工智能和深度学习技术的快速发展,多模态大模型在识别和处理图片与视频方面展现出了强大的能力。多模态大模型能够处理多种形式的数据,包括文本、图像、视频、音频等,从而实现更智能、更全面的理解与应用。本文将详细介绍多模态大模型是如何识别和处理图片与视频的。1.......
  • 解决 JMeter 返回内容中文乱码问题的详细指南
    前言在使用ApacheJMeter进行性能测试时,处理中文字符可能会遇到乱码问题。这不仅影响测试结果的正确性,还会导致测试报告难以理解。本文将详细介绍如何解决JMeter返回内容中的中文乱码问题,从配置文件设置到编码转换,帮助测试工程师顺利进行性能测试。常见的中文乱码问题在JM......
  • 网络调试利器:Chrome Network工具的详细指南
    前言作为测试工程师,熟练使用Chrome开发者工具中的Network工具可以极大地提升我们调试和分析Web应用的能力。Network工具用于监视网络活动,包括HTTP请求、响应、资源加载时间和数据传输量等。本文将详细介绍如何使用这个强大的工具来进行网络分析和调试。打开Network工具打开Ch......