计算机系统应用教程网站

网站首页 > 技术文章 正文

Java中使用 依赖倒置 原则解决循环依赖问题

btikc 2024-10-18 04:39:51 技术文章 18 ℃ 0 评论

#记录我的9月生活#

最近,在工作中我遇到了一个有趣的挑战。在审查一个 pull request 时,我发现代码结构在两个包之间引入了一个循环依赖。在这篇博客中,我想分享我们是如何解决这个问题的。

首先,我用一个示例来描述这个问题。我们有一个庞大的单体 Spring Boot 应用程序,它已经演变成了一团乱麻。我们正在尝试将其转变为一个模块化单体应用程序,但这是另一个博客的主题。

在这个单体应用程序中,我们引入了 Spring Modulith 来将代码组织成模块。这些模块是顶级包(与 @SpringBootApplication 注解类在同一级别)。结构如下所示:

com.example/
├── applicationlifecycle/
│   └── ...
├── application/
│   └── ...
└── SpringBootApplication.java

我们来看看两个模块 —— applicationlifecycleapplication。第一个模块负责应用程序的启动和停止,第二个模块负责创建、更新和删除应用程序。

这两个模块中类的依赖关系是直观的 —— applicationlifecycleapplication。显然,如果 application 模块中的任何类尝试访问 applicationlifecycle 模块中的类,就会导致循环依赖。如果这种代码没有被及时发现并解决,随着时间的推移,代码库会变得杂乱无章,导致在进行修改时出现意想不到的错误,并且维护起来非常困难。

引发循环依赖的新需求

我在审查的 pull request 实现了一个新需求:当用户请求删除一个应用程序时,如果该应用程序正在运行,则拒绝该请求。只有停止的应用程序才能被删除。

这看起来应该很简单。删除应用程序的代码已经存在,我们只需要添加一个检查,如果应用程序正在运行则阻止删除操作。我们有一个 ApplicationLifecycleService 类可以查询应用程序的状态。

package com.example.applicationlifecycle;

class ApplicationLifecycleService {

  boolean isRunning(Application application) {
    // ...
  }
}

我们从 applicationlifecycle 模块中注入 ApplicationLifecycleServiceapplication 模块的 ApplicationService 类中,以检查应用程序的状态。

package com.example.application;

import com.example.applicationlifecycle.ApplicationLifecycleService; // 循环依赖!

class ApplicationService {
  
  private final ApplicationRepository apps;
  private final ApplicationLifecycleService applicationLifecycle;

  void delete(Application application) {

    if (!applicationLifecycle.isRunning(application)) {
       throw new ApplicationNotRunningException();
    }

    apps.delete(application);
  }
}

乍一看,这样的实现似乎没问题。但实际上,我们无意中引入了一个循环依赖。虽然今天可能容易忽视,但几年后这个错误的代价将会显现。

我们可以使用 Spring Modulith 来可视化这个循环依赖。我们也可以添加一个测试,如果检测到这样的循环依赖就会失败。但是,如果应用程序已经有很多循环依赖,需要几个月的时间来修复,那这个方法可能就不可行了。

那么我们该如何解决它呢?

打破循环依赖

打破这个循环依赖的一种方法是将导致循环依赖的需求隔离到它自己的模块中。新需求通常会带来对问题领域的新见解,并有助于改进代码库的结构。但是,取决于需求,这可能会有点过头。如果需求很小,是否值得为它创建一个单独的模块?太多的小模块会增加认知负担。

新的用例可以揭示我们应用程序中隐藏的模块。

让我们看看如何将这个解决方案应用于我们的问题。我们将应用程序删除的用例移动到它自己的模块中 —— applicationdeletion。所有与删除应用程序相关的类(如控制器、服务等)将移到新模块中。它将依赖于 applicationlifecycleapplication 两个模块。可视化如下所示:

你可能会认为,应用程序删除用例不值得拥有自己的模块。我同意。那么有没有其他方法可以在不创建新模块的情况下打破循环依赖?

依赖倒置原则

我们可以应用 依赖倒置原则(DIP) 来打破循环依赖。仅从名字来看,这似乎很有希望,对吧?倒置依赖关系符合 applicationlifecycleapplication 的直观依赖关系。那么我们如何倒置它呢?

DIP 的目的是通过依赖抽象而不是底层模块中的具体实现来实现高层模块的可重用性。举个例子,考虑一个 Copy 函数。它需要一个 Reader 和一个 Writer。理想情况下,它应该依赖于一个通用的 Reader 和 Writer 以允许 Copy 函数在许多场景中重复使用,而不是特定版本如 KeyboardReaderPrinterWriter

但我们将使用 DIP 来根据我们的需要控制依赖的方向。我们通过在低层包(application)中引入一个接口,并让高层包(applicationlifecycle)依赖于它。虽然这与原本的重用性目的相违背,但我们并不真正关心 applicationlifecycle 模块的重用性,因为它有一个特定的用途(管理应用程序的生命周期),并且不打算作为通用抽象被许多其他模块使用。下图展示了 DIP 的操作。

让我们将其应用于我们的问题。

注意,application 模块对 ApplicationLifecycleService 并不感兴趣,而只对 isRunning() 函数的行为感兴趣。我们引入一个名为 ApplicationLifecycleEnquiry 的新接口,它只有一个函数 isRunning()

package com.example.application;

public interface ApplicationLifecycleEnquiry {

  public boolean isRunning(Application application);
}

然后我们让 ApplicationLifecycleService 实现上述接口。注意,此类中不需要进行其他更改。

package com.example.applicationlifecycle;

class ApplicationLifecycleService implements ApplicationLifecycleEnquiry {

  @Override
  boolean isRunning(Application application) {
    // ...
  }
}

最后,ApplicationService 类使用该接口来检查应用程序的运行状态。

package com.example.application;

// 不需要从 applicationlifecycle 包导入任何内容!
import com.example.application.ApplicationLifecycleEnquiry;

class ApplicationService {
  
  private final ApplicationRepository apps;
  private final ApplicationLifecycleEnquiry applicationLifecycle;

  void delete(Application application) {

    if (!applicationLifecycle.isRunning(application)) {
       throw new ApplicationNotRunningException();
    }

    apps.delete(application);
  }
}

ApplicationService 不再依赖于 applicationlifecycle 包中的任何类。没有循环依赖!

依赖注入使依赖关系倒置成为可能。

现在还有一个问题需要回答。ApplicationLifecycleEnquiry 的实现如何在运行时可用于 ApplicationService?答案是 Spring 框架及其依赖注入功能。Spring 会找到 ApplicationLifecycleEnquiry 的实现类 —— 即 ApplicationLifecycleService,并在运行时将其注入到 ApplicationService 中。实际上,我们已经将依赖从编译时移动到了运行时。这非常好,因为循环依赖问题只在编译时才会出现。

结论

循环依赖必须被避免。依赖倒置原则允许我们通过接口倒置依赖,从而解决循环依赖问题。

pull request 中的循环依赖引发了关于解决方案的讨论和研究。我们选择了基于 DIP 的解决方案,因为它满足了我们的需求,并且不需要创建一个新模块。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表