类型A和B之间的循环依赖

时间:2017-08-06 07:41:34

标签: java oop orm relational-database

我有两个班级:

class A {

    final B b;

    A(final B b) {
    this.b = b;
    }

    void doIt() {
    b.doSomething();
    }

    void doSomething() {
    // TODO Auto-generated method stub
    }
}

class B {

    final A a;

    B(final A a) {
    this.a = a;
    }

    void doIt() {
    a.doSomething();
    }

    void doSomething() {
    // TODO Auto-generated method stub
    }
}

没有办法实例化其中任何一个。

我可以使用setter,例如:

class B {

    A a;

    B() {

    }

    void setA(final A a) {
    this.a = a;
    }

    void doIt() {
    a.doSomething();
    }

    void doSomething() {
    // TODO Auto-generated method stub
    }
}

但是,我需要在a中检查null doIt()并处理此案例。这不是世界末日,但也许

有更聪明的方法吗?

或许这甚至可能是一种反模式,而且首先是架构出了问题?

问题的根源

我有一个数据库,我可以从中加载实体。我加载后缓存它们并使用此缓存在类型之间建立双向关系。这是必要的,因为当我加载A的多个实例时,它们应该(在这种情况下)具有B的所有相同实例。因此,B需要在之前进行实例化并重新使用。但是B也与所有这些a有关系,因此也就是循环依赖。

所以更简短,循环依赖是由数据库中的双向关系引起的。我尽可能地避免使用它们,但除了这里描述的麻烦之外,没有真实的世界'双向关系的问题,实际上这是非常自然的事情。

所以也许问题应该是如何正确地将关系数据库中的双向关系映射到oop世界?

更具体的例子:

为了建立双向关系,我从缓存加载实例,以便在实体ID相同时我有相同的实例:

interface Cache<T>

interface CacheA extends Cache<A>

interface CacheB extends Cache<B>

class CacheManager {

    final CacheA cacheA;

    final CacheB cacheB;

    CacheManager(final DatabaseAccess databaseAccess) {
    cachA = new CacheA();
    cachB = new CacheB();
    cacheA.setEntityLoader(id -> new SimpleAttachedA(id, databaseAccess, cacheB));
    cacheB.setEntityLoader(id -> new SimpleAttachedB(id, databaseAccess, cacheA));

    }

}

如果访问A的关系,B的实例将访问此缓存。反之亦然。

我知道最好,如果CacheACacheB是同一个对象,那么我可以创建缓存并将其传递给A的所有实例和B

CacheACacheB曾经是相同的,即Cache。我选择将它们分开,所以我可以使用泛型类并删除大量重复的代码。

CacheManager无法实现CacheACacheB,如果它们都扩展相同的通用接口Cache<T>,但使用不同类型的T

因此CacheManager使用合成而不是继承。所以我最终得到了两个缓存,它们需要相互访问才能实现AB的双向关系。

4 个答案:

答案 0 :(得分:2)

循环依赖通常是糟糕设计的标志。在某些情况下,您无法阻止循环依赖,但您应该始终考虑另一种解决方案。当你有循环依赖时,你可能会更改其中一个类,并且还必须更改另一个类。当你的循环包含两个以上的类时,这可能需要做很多工作。还有像你提到的那样的问题,“我需要另一个类来实例化其中一个”。

由于你的班级似乎是虚拟班级,我不能给出一个非常好的建议,但仍然有一些一般的观点。

类应具有高内聚低耦合。意思是,类应尽可能少地依赖于另一个类(低耦合),而每个类应该执行一个功能,以及所有功能(高内聚)。

当你有两个彼此依赖的类时,这通常是低内聚的标志,因为功能的一部分在类A中,另一部分在类B中。在这种情况下,您应该考虑在一个类中合并两个类。

另一方面,当您尝试查找该合并类的类名并提出ThisAndThatDoer之类的内容时,您应该将它们分成两个类ThisDoerThatDoer,因为这是低凝聚力的标志。当您再次将原始类相互依赖时,您可以创建一个连接两个类的新类Executor。但这很快就会变成神级,这也是一种反模式。所以你应该小心这一点。

总而言之,我建议考虑你的课程设计,并找到一种方法,至少在一个方向上消除依赖性。我希望这有助于你的问题。

答案 1 :(得分:1)

这确实被认为是一种反模式,通常有一种更好的方法。如果A需要B且B需要A,则表示

  • 在概念上属于一起的功能被分为两个部分(A和B)。因为每个组件只实现部分功能,所以需要另一个组件来完成缺少的功能。 解决方案:识别属于一起的A和B的部分并将它们移动到新的组件C.使A和B的左边依赖于C并删除A和B之间的依赖关系。
  • A包含两个独立的功能A1和A2,可分为两个独立的组件。如果A1需要B提供的功能,而B需要A1提供的功能,则会产生A和B之间的循环依赖关系。 解决方案:将A拆分为两个独立的组件A1和A2,使A1依赖于B,使B依赖于A2。

如前所述,有些情况下循环依赖性是正常的,如果避免,会导致更加模糊的设计。但是,通常情况并非如此,您应该尝试重新构建功能以避免循环依赖。

如果您提供有关特定情况的更多信息(即A和B在做什么以及相互依赖的部分是什么),可以给出更多提示。

答案 2 :(得分:1)

  

或许这甚至可能是一种反模式,也就是说   首先是架构错了吗?

1)如果两个班级都不需要访问另一个班级的所有成员,则应尽可能避免两个具体班级之间的双向耦合。
使它通常产生高于所需的耦合。

2)如果需要,双向耦合不是必需的问题,但在对象的构造中存在循环依赖性。
这是一种难闻的气味,因为如果不破坏它就无法解决。

使用setter的解决方案确实是一种解决问题的方法 如果你真的想要避免它并在两个类中保持构造函数的方式,那么如果你改变你的设计,你会有一些解决方案。

例如,您可以用包含所有数据的类替换参数(A或B类)中的一个,以创建AB实例。

假设您要抽象B

的创建

<强> A

public A(ContextForB contextForB) {
    // init A data
      ...
    // create B from context and set the B dependency
    b = new B(contextForB, this);
}

<强>乙

public B(ContextForB contextForB, A a) {
   //.. create B from the context
      ...
   // set the A dependency
   this.a = a;
}

ContextForB可以是任何东西 ResultSetQueryIteratorSupplier等...

您可以通过以下方式使用它们:

// instantiate both from A
A a = new A(new ContextForB(...));  

// OR instantiate both from B
ContextForB contextForB = new ContextForB(...);
B b = new B(contextForB, new A(contextForB));

答案 3 :(得分:0)

创建界面

public interface Doable {
      void doSomething();
}

然后在A和B中使用它:

class A implements Doable {

    final Doable b;

    A(final Doable b) {
    this.b = b;
    }

    void doIt() {
    b.doSomething();
    }

    @Override
    void doSomething() {
    // TODO Auto-generated method stub
    }
}

class B implements Doable {

    Doable a;

    B(final Doable a) {
      this.a = a;
    }

    void doIt() {
    a.doSomething();
    }
    @Override
    void doSomething() {
    // TODO Auto-generated method stub
    }
}

这里你将避免循环依赖。