使用构造函数依赖注入打破循环依赖

Break circular dependency with constructor dependency injection

我的代码库大量使用 interfaces/abstractions、工厂、存储库、依赖项注入和其他设计模式,以尝试编写良好、可维护的代码。我的 DI 容器是 SimpleInjector。然而,我遇到了一个间接的循环依赖问题,我正在努力寻找如何在不违反良好设计(以及我自己的原则!)的情况下解决这个问题。

下面的半伪代码代表了问题的简化(注意:为了简洁起见,我没有在这里详细介绍接口,但是从class中可以推断出实现它们,我也没有显示所有琐碎的东西,比如 c'tor 的设置支持字段——这是暗示的)。另请注意,我始终使用构造函数注入。

class A : IA
{
  A(int id, IBRepository br);
  IEnumerable<IB> GetBs(); // uses _br and _id to find all "child" B's.

  int _id;
  IBRepository _br;
}

class B : IB
{
  B(int id, int aId, IARepository ar);
  IA GetA(); // uses _ar and _aId to find "parent" A.

  int _id;
  int _aId;
  IARepository _ar;
}

class ARepository : IARepository
{
  ARepository(IAFactory af);
  IA FindById(int id); // uses _af to create A if Id found.

  IAFactory _af;
}

class BRepository : IBRepository
{
  BRepository(IBFactory bf);
  IB FindById(int id); // uses _bf to create B if Id found.

  IBFactory _bf;
}

class AFactory : IAFactory
{
  AFactory(IBRepository br);
  IA Create(); // _br fed in to A c'tor

  IBRepository _br;
}

class BFactory : IBFactory
{
  BFactory(IARepository ar);
  IB Create(); // _ar fed in to B c'tor

  IARepository _ar;
}

下面是一些示例使用代码:

// Sample usage code
IARepository ar = Container.GetInstance<IARepository>();
IA a = ar.FindById(123);
IEnumerable<IB> bs = a.GetBs(); // get children.
IB b = bs.First();
IA a2 = b.GetA() // get parent.
Debug.Assert(a.Id == a2.Id);

容器通过注册具体的 classes 到它们各自的接口(在 singleton/non-transient 生命周期内使用 RegisterSingle<T>)来连接(未显示)。

问题是我引入了一个间接的循环依赖,如此处所示(实际上,这个问题表现为应用程序引导时的异常——SimpleInjector DI 容器很好地告诉了你这个问题!) :

// Circular dependency chain
AFactory
  BRepository
    BFactory
      ARepository
        AFactory // <-- circular dependency!

如您所见,我已经在 AB 上使用 Id,试图缓解因在每个 [=67= 上存储直接对象引用而可能引起的循环依赖问题](我之前就被它咬过)

我考虑过完全打破域模型的 parent/child 关系(A.GetBs()B.GetA()),并将该功能推入某种单独的查找服务,但这似乎对我来说就像代码的味道,因为域模型应该封装它们自己的关系。此外,存储库已经用于此类查找功能的目的(AB 已经具备)。

虽然最重要的是,这会使 AB 的客户端代码更加复杂(消费者习惯于能够无缝地 "dot" 通过对象图)。此外,这会损害性能,而不是在 AB 上缓存 parent/child 对象引用,客户端代码调用一些单独的服务来进行查找将需要不断访问支持数据存储(假设没有存储库缓存)并加入 ID。

俗话说,大多数软件问题都可以通过另一个层次的抽象或间接来解决,我相信这里会是这样,但我还没有弄明白。

我查看了以下内容,虽然我认为我理解了他们告诉我的内容,但我正在努力将其与我的问题集联系起来:

总而言之,我如何在仍然使用构造函数 DI 的同时打破循环依赖问题,坚持并利用既定的设计模式和最佳实践,并理想地为 [=14] 的消费者维护良好的 API =] 和 B 到 "dot" 他们如何处理关系?如有任何帮助,我们将不胜感激。

Class B 不需要 A 工厂或存储库。如果 B 是给定 A 的子代,则只需将 A(或 IA)的实例传递给 B 的构造函数。

您问题中的示例用法将完美无缺。

你将不得不改变一些东西,因为你的依赖图是循环的。您必须决定哪些对象真正相互依赖,哪些不相互依赖。要求 B 只能在父 A 的可用实例存在时才能创建是一种方法。

我不太明白你们的工厂和仓库是做什么的。对我来说,两者似乎都太多了。但是让两个存储库都依赖于两个工厂是否可行?然后每个工厂都没有任何构造函数依赖。

如果需要,工厂 Create 方法随后可以接收存储库实例作为方法参数。

长评论:

您可以将导致圆圈的物体的分辨率延迟 "adding a level of inderectoin"。由于通常您不需要构造函数中的工厂,因此您可以使用 Func<T> 而不是 T 本身将实际分辨率推迟到您需要的程度,其中函数类似于 ()=> container.Resolve(typeof(T)):

class AFactory : IAFactory
{
  AFactory(Func<IBRepository> br);
  IA Create()
  {   
      var _br = _lazyBr();
      // _br fed in to A c'tor
      ...
  }

  Func<IBRepository> _lazyBr;
}

请注意,Unity DI 会自动注册 Func<T>T,我不确定如何在您使用的容器中实现它,但它应该类似于:

 container.Register<Func<IBRepository>>( 
       () => container.Resolve<IBRepository>());

在尝试了@CoderDennis 和@AlexeiLevenkov 的非常有用的建议后,最后我 re-evaluated 我的域模型设计并通过不再提供 child [=17] 解决了循环依赖问题=] a 属性 访问他们的 parent。所以我只是避免了我最初遇到的问题,而不是真正解决它,但设计的改变最终对我很有效。

感谢两位帮助我解决问题并帮助我意识到也许最好的想法是改变我解决问题的方法。