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

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


代码之间复杂关系的一种常见形式就是类与类之间存在循环依赖关系。所谓循环依赖,简单来说就是一个类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);

}

}

}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/627999.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

有趣的css - 卡片翻转效果

大家好&#xff0c;我是 Just&#xff0c;这里是「设计师工作日常」&#xff0c;今天分享的是利用 css3 属性 backface-visibility 让卡片翻转的过渡动画效果。 《有趣的css》系列最新实例通过公众号「设计师工作日常」发布。 目录 整体效果核心代码html 代码css 部分代码 完整…

ICode国际青少年编程竞赛- Python-5级训练场-函数练习2

ICode国际青少年编程竞赛- Python-5级训练场-函数练习2 1、 def get_item(a):Spaceship.step(1)Dev.step(a)Dev.turnLeft()Dev.step(1)Spaceship.step(1)Dev.turnRight()Dev.step(-a)Spaceship.step(1) get_item(3) get_item(2) get_item(3) get_item(1) get_item(5)2、 de…

PDF批量编辑:PDF转HTML批量操作技巧,提升文档格式转换效率

在数字化办公日益普及的今天&#xff0c;PDF&#xff08;Portable Document Format&#xff09;作为一种跨平台的文件格式&#xff0c;广泛应用于各种文档的存储和传输。然而&#xff0c;PDF文件的不可编辑性使得在某些情况下&#xff0c;我们需要将其转换为HTML格式以便更好地…

性价比王者HUSB237,极简PD Sink的“瘦身秘籍”

在小型化、高集成的要求下&#xff0c;慧能泰取电芯片进行技术升级后“瘦身成功”&#xff0c;推出最新一代极具性价比的最简PD Sink取电芯片——HUSB237。 图1&#xff1a;HUSB237 demo及封装图 HUSB237 是一款极具性价比的最简PD Sink取电芯片&#xff0c;支持PD3.1协议包含…

无人售货奶柜:掘金新零售蓝海,

无人售货奶柜&#xff1a;掘金新零售蓝海&#xff0c; 在日新月异的商业浪潮中&#xff0c;无人奶柜犹如一股清新的创业飓风&#xff0c;正以不可阻挡之势吸引着众多创业者的目光。这股新兴力量以其独到之处和庞大的市场蓝海&#xff0c;预示着一场关于健康、便捷消费方式的深…

网络故障快速定位的秘诀 - 基于 AnaTraf 全流量回溯分析

网络故障是每个 IT 从业者都深有体会的头疼问题。当网络出现异常时,如何快速定位故障原因,恢复网络正常运行,是考验运维能力的关键所在。借助 AnaTraf 网络流量分析仪的全流量回溯分析功能,您可以轻松应对各种复杂的网络问题,实现快速故障定位。 1. 网络故障分析的痛点 网络故…

【跟着例子学MySQL】生成统计报告 --分组聚合

文章目录 前言生成报告DISTINCT 关键字GROUP BY 子句GROUP BY 聚合函数HAVING 子句WITH ROLLUP 子句未完待续 前言 举例子&#xff0c;是最简单有效的学习方法。本系列文章以一个贯穿始终的场景&#xff0c;结合多个实例讲解MySQL的基本用法。 ❔ 为什么要写这个系列&#xff…

前端铺子后台管理系统:基于Vue3与Ant Design Vue的轻量级解决方案

一、引言 随着前端技术的飞速发展&#xff0c;构建高效、轻量且易于维护的后台管理系统成为了企业信息化建设的重要一环。前端铺子后台管理系统&#xff0c;作为一款基于Vue的前端框架&#xff0c;结合Ant Design Vue的UI组件库&#xff0c;为企业提供了一个高效、灵活的后台管…

铁路机辆作业移动智能终端的特点是什么?

在铁路机辆作业的现代化进程中&#xff0c;移动智能终端以其独特的优势成为了不可或缺的装备。这些终端以其高度的便携性&#xff0c;使得工作人员能够随时随地处理各种作业任务&#xff0c;极大地提升了工作效率。它们具备出色的抗干扰性和高防护性&#xff0c;能够在复杂多变…

Docker部署MaxKB详细步骤(window系统)

上面章节已经实现了ollama李现部署llama3&#xff0c;并实现了一些简单的问答&#xff0c;但是问答的界面是在命令提示符中&#xff0c;交互很不友好&#xff0c;也不方便局域网其他用户访问&#xff0c;所以这节用docker部署MaxKB实现网页访问llama3&#xff0c;首先电脑上需要…

26版SPSS操作教程(高级教程第二十二章)

目录 前言 粉丝及官方意见说明 第二十二章一些学习笔记 第二十二章一些操作方法 联合分析 假设数据 具体操作 结果解释 联合分析的数据建模 CONJOINT过程语法 汽车偏好研究 具体操作 结果解释 高精统计图 市场占有率模拟 结果解释 结束语 前言 #一路向光而…

STL—string类(1)

一、string类 1、为什么要学习string&#xff1f; C语言中&#xff0c;字符串是以\0结尾的一些字符的集合&#xff0c;为了操作方便&#xff0c;C标准库中提供了一些str系列的库函数&#xff0c;但是这些库函数与字符串是分离开的&#xff0c;不太符合OOP&#xff08;面向对象…

【Fastadmin】自定义404页面

1.修改config.php // 文件路径&#xff1a;application/config.php// 自定义错误码模板http_exception_template > [// 定义404错误的模板渲染404 > APP_PATH . common/view/404/404.html,], 2.需要把debug关闭才能生效 在.env文件中把debug true&#xff0c;改为…

阿里云OSS如果指定某个文件夹给子账户

** 第一步创建子账号 ** 创建完用户不要给任何权限&#xff01; 当前页面切换到认证管理获取AccessKey即可 第二步目录授权 找到对应桶文件目录 上面授权按钮操作 选择添加的子账号账号保存即可&#xff01;

Ardupilot Rpanion 4GLTE 网络性能测试 - 国内中转

Ardupilot Rpanion 4GLTE 网络性能测试 - 国内中转 1. 源由2. 视频效果2.1 整体刷新率不高2.2 网络延迟可接受2.3 带宽增加丢包明显2.4 实测效果流畅 3. 总结 1. 源由 上一次&#xff0c;由于ZeroTier使用了国外服务器&#xff0c;延迟~ 569 ms&#xff0c;花屏、卡顿。 本着…

[自动化]pyautogui的使用

目录 环境 包的版本 前置知识 鼠标控制函数 屏幕与鼠标位置 size() position() OnScreen() 鼠标移动 moveTo() move() 鼠标拖动 dragTo() drag() mouseDown()按下鼠标 mouseUp()松开鼠标 鼠标滚动 scroll() 键盘控制函数 write() press() keyDown()和keyU…

数据分析(二)——导入外部数据,导入Excel数据,CSV文件,txt文件,HTML网页,数据抽取,DataFrame对象的loc属性与iloc属性

一.导入外部数据 1.导入.xIs或.xIsx文件 pd.read_ excel(io,sheet_ name,header) 1.1常用参数说明 ●io:表示.xIs或.xIsx文件路径或类文件对象 ●sheet name:表示工作表&#xff0c;取值如下表所示 ●header:默认值为0&#xff0c;取第一行的值为列名&#xff0c;数据为除列…

深度剖析MyBatis的一级缓存

概述 MyBatis 的一级缓存是什么时候开启的&#xff1f; 在 MyBatis 中&#xff0c; 一级缓存是默认开启的 。 参考&#xff1a;MyBatis缓存的概念 通过场景来理解: 场景一 1、在一个 SqlSession 中&#xff0c;对 User 表进行两次根据 ID 的查询&#xff0c;查看发出 sql …

一步一步带你做网络工程

网络工程怎么做 一、网络设备交换机的应用&#xff1a; 要求&#xff1a;在此接入交换机S3700&#xff0c;上划分两个vlan&#xff0c;vlan10和vlan20分别有两个PC&#xff0c;按拓扑图完成要求&#xff1a; 划分vlan添加端口 sys [Huawei]sys S1 [S1]undo in e [S1]undo t…

2024年重庆等保测评公司有哪些?分别位于哪里?

2024年重庆等保测评公司有哪些&#xff1f;分别位于哪里&#xff1f; 【回答】&#xff1a;目前2024年重庆等保测评公司有四家&#xff0c;具体公司名称以及地址如下&#xff1a; 1、重庆信安网络安全等级测评有限公司&#xff0c;重庆市两江新区黄山大道中段55号附2号麒麟D座…