【问题标题】:How to remove circular dependency between two classes in a same package?如何消除同一个包中两个类之间的循环依赖?
【发布时间】:2019-11-04 10:19:14
【问题描述】:

我们正在做一些重构,我们在这里碰壁了。 有两个服务类 AService 和 BService 做不同的工作有一个循环依赖。 早些时候它们在不同的库中,因此不存在循环依赖。但是现在在重构之后,我们将它们移到了一个单独的库中到一个单独的包中。 AService 的工作是将一些数据持久化到 NoSQL 数据库中以用于单独的用例。 BService 作业与 AService 作业完全不同。 AService 需要 BService 来获取一些数据。 BService 需要 AService 向 NoSQL Database 写入一些数据并回读。

如何解决循环依赖问题,使它们相互依赖。有针对此类问题的设计模式吗?

class AService {
@Autowired
private BService bService;

}

class BService {
@Autowired
private AService aService;

}

【问题讨论】:

  • 也许您可以将需要 BService 的 AService 代码移动到 BService 并将 AService 作为写入数据存储的服务。
  • 可能你的服务太大了,做的太多了?考虑拥有更多但更小、更简单的服务。

标签: java design-patterns circular-dependency


【解决方案1】:

解决方案 1(推荐): 类职责的重新设计。秉承单一职责原则,根据您透露的类细节,我们可以通过提取至少一个新类来修复您的类设计:DatabaseConnector。该类封装了所有与数据库相关的操作(CRUD),因此解除了服务类的循环依赖(不改变类的原始概念)。

// This is just a raw template to express the intention
class DatabaseConnector {
  void createInDatabase(Object data) {
  }

  Object readFromDatabase(Object args) {
  }

  void updateInDatabase(Object data) {
  }

  void deleteFromDatabase(Object data) {
  }
}

class AService {
  @Autowired
  private DatabaseConnector dbConnector;    
}

class BService {
  @Autowired
  private DatabaseConnector dbConnector;    
}

您可以在DatabaseConnector 中添加更多专门的方法以满足特殊要求(例如readName()readId() 等)。

由于将来可能需要更多类访问数据库,因此您今天已经解决或阻止了新的循环依赖关系。封装解决了潜在的即将出现的问题。

方案二:依赖倒置

interface IAService {    
}

interface BService {
}

class AService implements IAService {
  @Autowired
  private IBService bService;    
}

class BService implements IBService {
  @Autowired
  private IAService aService;    
}

循环依赖始终是不良类设计的指标。在大多数情况下,起源是违反单一责任原则(SOLID 中的 S)。错误的构图概念也可能导致这种设计错误。总是有帮助但不能解决类职责的概念缺陷的是引入接口来反转所有依赖项(SOLID 中的 D)。认真对待 SOLID 原则可以节省大量时间和工作,并且总是会产生更好的代码(尽管您引入了更高的代码复杂性)。

中介者模式还可以通过封装两个或多个对象的双向交互来帮助解除循环依赖。

您当前代码的缺点是(除了循环依赖之外),每当 A 类发生变化并且数据持久性也发生变化时,您必须触及和修改 B 类。这种更改可能会破坏使用相同持久性操作的 B 类.对于一个类与另一个类共享责任的所有情况都是如此。如果没有共享代码,两个类根本就不会互相认识。在您的特殊情况下,依赖是循环的,您也将此缺陷添加到另一个依赖方向:每当 B 需要调整或扩展如何读取数据时,您必须修改 A 类,这可能会破坏 A。如果您是使用单元测试,那么您也必须重构 both 类的测试。 A 和 B 的这种紧密(和循环)耦合将导致错误或错误。扩展代码变得很危险。但好消息是循环依赖永远不会编译(因为依赖解析会导致无限递归)。

【讨论】:

    【解决方案2】:

    最简单的方法是将服务 B 使用的方法从服务 A 移到服务 B 中,或者移到一个完全不同的类中。

    设计表明您可能在服务 A 中有太多方法,服务 A 并没有实际使用这些方法,但这些方法是真正应该是静态的并且完全在另一个类中的公共方法。

    【讨论】:

    • 将共享代码从一个类移动到另一个类可以解决问题,但不会改进设计。无缘无故不属于一类的操作。添加它也会增加显然不是有意的责任。它还可以将循环问题推迟到代码被扩展并且新的类也需要这个功能。如果你坚持单一职责原则,那么在引入新课程时你总是安全的。
    • 所以答案不是“使代码静态”而是“封装共享代码(或共享责任)”。封装需要引入一个新的类来处理共享责任。
    • 没错,我想你也可以创建一个由Service A和Service B实现的抽象类,并将常用方法移到抽象类中。
    • 让两个服务都继承这个抽象基类?这会引入新的问题以及服务之间的不自然关系:服务类型 数据库连接器。将其设计成这样会更自然:服务具有使用数据库连接器。组合优于继承。这样,即使在运行时,您也可以更改数据库连接器的具体实现。继承需要重写或更改基类以交换不同的连接器。然后,您必须调整代码中引用旧基类的每个位置。
    • 我明白你在说什么。我想我误解了服务 A 和服务 B 都是类似的类,它们都涉及 SQL 数据库并且具有重叠的方法,但你基本上是说应该有一个名为“SQLDatabaseConnector”(或类似的东西)的类,其中服务 A 和服务 B 都使用,如有必要,服务 A 和服务 B 可以覆盖。
    【解决方案3】:

    真正的问题是为什么你的概念让 A 和 B 相互依赖?

    • 可能 A 和 B 代表相同的概念,并且可以合并。
    • 也可能是您的服务 A 或 B 的一部分应提取到新服务 C 中。

    在最后一种情况下,您需要知道哪种方法取决于哪个服务,以及可以在新服务中提取哪个方法。如果没有上下文和方法列表,就很难提出解决方案。 在间接递归方法的情况下,您可能还需要削减一些方法。

    旁注:不要试图找到解决方法来使其与循环依赖项一起工作,这表明概念问题,您必须进行重构。

    【讨论】:

    • AService 和 BService 用于单独的用例。它们不应该放在一个库中,但是由于网络调用的其他一些问题,我们必须合并它们以便它们可以相互调用。所以我们不能合并它们,因为概念不同。提取没有意义,因为两者代表不同的概念。
    • @AvinashKharche 您必须了解,多个类相互依赖这一事实表明它们共享共同的职责(它们相互补充)或者它们依赖于共享的信息(操作)。你必须使用单一职责原则来评估你的班级设计。你将不得不重写你的课程并引入新的课程。查看中介者模式也可以帮助您。
    • @AvinashKharche 你说得对,合并不是解决方案(单一责任原则)。但是提取公共代码确实有意义并且不会改变概念。在您的特殊情况下,您所要做的就是将数据库访问提取到一个新类:DatabaseConnector。该类负责读取和写入数据库并实现所有 CRUD 操作。现在两个服务不再相互依赖,而是依赖DatabaseConnector?
    • @AvinashKharche 如果你仍然拒绝接受你的设计缺陷,你可以引入接口来解除循环依赖。寻找依赖倒置原则。
    • @AvinashKharche 查看我的答案。
    【解决方案4】:

    尝试将 autowired 放在 setter 上:

    @Autowired
    public void setServiceB(ServiceB service){
     this.serviceB = service;
     }
    

    【讨论】:

    • 这可能会修复依赖注入容器 (Spring) 的错误,因为现在在构造实例之后 满足了 setter 依赖。但这并不能解决依赖图本身的设计缺陷。因此,这不是对原始问题的解决方案,而是一种欺骗 IoC 框架的技巧。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多