2.3.3 消除循环依赖案例分析
在本节中,我们将基于日常开发需求,通过一个具体的案例来介绍组件之间循环依赖的产生过程以及解决方案。
这个案例描述了医疗健康类系统中的一个常见场景,每个用户都有一份健康档案,存储着代表用户当前健康状况的健康等级以及一系列的健康任务。用户每天可以通过完成医生所指定的任务来获取一定的健康积分,而这个积分的计算过程取决于该用户当前的健康等级。也就是说,不同的健康等级下完成同一个任务所能获取的积分也是不一样的。反过来,等级的计算也取决于该用户当前需要完成的任务数量,任务越多说明用户越不健康,其健康等级也就越低。健康档案和健康任务之间的关联关系如图2-4所示。
图2-4 健康档案和健康任务之间的关联关系
针对这个场景,我们可以抽象出两个类,一个是代表健康档案的HealthRecord类,一个是代表健康任务的HealthTask类。我们先来看HealthRecord类,这个类包含一个HealthTask列表以及添加HealthTask的方法,同样也包含一个获取健康等级的方法,这个方法根据任务数量来判断健康等级,如代码清单2-22所示。
代码清单2-22 HealthRecord类实现代码
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中的等级信息,如代码清单2-23所示。
代码清单2-23 HealthTask类实现代码
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之间存在明显的相互依赖关系。
那么,如何消除循环依赖?软件行业有一句很经典的话,即当我们碰到问题无从下手时,不妨考虑一下是否可以通过“加一层”的方法进行解决。消除循环依赖的基本思路也是这样,就是通过在两个相互循环依赖的组件之间添加中间层,变循环依赖为间接依赖。有三种方法可以做到这一点,分别是提取中介者、转移业务逻辑和引入回调。
1. 提取中介者
我们先来看第一种方法:提取中介者。提取中介者的核心思想是把两个相互依赖的组件中的交互部分抽象出来形成一个新的组件,而新组件同时包含着对原有两个组件的引用,这样就把循环依赖关系剥离出来并提取到一个专门的中介者组件中,如图2-5所示。
图2-5 提取中介者之后的类图
这个中介者组件的实现也非常简单,通过提供一个计算积分的方法来对循环依赖进行了剥离,该方法同时依赖于HealthRecord和HealthTask对象,并实现了原有HealthTask中根据HealthRecord的等级信息进行积分计算的业务逻辑。中介者HealthPointMediator类的实现代码如代码清单2-24所示。
代码清单2-24 HealthPointMediator类实现代码
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; } }
2. 转移业务逻辑
我们继续介绍第二种消除循环依赖的方法,这就是转移业务逻辑。这种方法的实现思路在于提取一个专门的业务组件来完成对等级的计算过程。这样,HealthTask原本对HealthRecord的依赖就转移到了对这个业务组件的依赖,而这个业务组件本身不需要依赖任何对象,如图2-6所示。
图2-6 转移业务逻辑之后的类图
图2-6中的专门负责处理业务逻辑的HealthLevelHandler类的实现代码也很简单,如代码清单2-25所示。
代码清单2-25 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; } }
3. 引入回调
介绍完了提取中介者和转移业务逻辑这两种方法之后,我们来看最后一种消除循环依赖的方法,这种方法会采用回调接口。所谓回调,本质上就是一种双向调用模式,也就是说,被调用方在被调用的同时也会调用对方。在实现上,我们可以提取一个用于计算健康等级的业务接口,然后让HealthRecord去实现这个接口。我们同样将这个接口命名为HealthLevelHandler,其中包含一个计算健康等级的方法定义。这样,HealthTask在计算积分时只需要依赖这个业务接口,而不需要关心这个接口的具体实现类,如图2-7所示。
图2-7 引入回调之后的类图
由于篇幅有限,完整的消除循环依赖案例代码可以在以下GitHub地址下载:https://github.com/tianminzheng/acyclic-relationships-demo。