提问者:小点点

通过构造器依赖注入打破循环依赖


我的代码库大量使用接口/抽象、工厂、存储库、依赖注入和其他设计模式,试图写出好的、可维护的代码。我的DI容器是SimpleInjector。然而,我遇到了一个间接的循环依赖问题,我很难看到如何在不违反良好设计(和我自己的原则!)的情况下打破这个问题。

下面的半伪代码代表了问题的简化(注意:为了简洁起见,我在这里不详细介绍接口,但它们可以从实现它们的类中推断出来,我也没有显示所有琐碎的东西,比如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);

容器通过将具体类注册到它们各自的接口(使用注册单)来连接(未显示)

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

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

如您所见,我已经在AB上使用Id,试图减轻循环依赖问题,这些问题可能因在每个类上存储直接对象引用而产生(我以前就被它咬过)。

我考虑过将父/子关系(A. GetBs()B.GetA())完全从域模型中分离出来,并将该功能推送到某种单独的查找服务中,但对我来说这似乎是一种代码味道,因为域模型应该封装自己的关系。此外,存储库已经服务于这种查找功能的目的(AB已经拥有)。

最重要的是,这会使AB的客户端代码更加复杂(消费者习惯于能够无缝地“点点”他们在对象图中的方式)。此外,这会损害性能,因为与在AB上缓存父/子对象引用不同,调用一些单独的服务进行查找的客户端代码需要不断访问后备数据存储(假设没有存储库缓存)并在Id上连接。

俗话说,大多数软件问题都可以通过另一个抽象或间接级别来解决,我相信这里也是如此,但我还没能弄清楚。

我已经复习了以下内容,虽然我认为我理解了他们告诉我的内容,但我很难将其与我的问题集联系起来:

  • https://softwareengineering.stackexchange.com/questions/253646/how-to-handle-circular-dependency-in-dependency-injection
  • http://misko.hevery.com/2008/08/01/circular-dependency-in-constructors-and-dependency-injection/
  • 如何避免这里的循环依赖

总而言之,我如何打破循环依赖问题,同时仍然使用构造函数DI,坚持和利用既定的设计模式和最佳实践,并理想地为AB的消费者保持一个良好的API来“点缀”他们的关系?任何帮助都将不胜感激。


共3个答案

匿名用户

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

您问题中的示例用法将非常有效。

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

我真的不明白你的工厂和存储库是做什么的。对我来说,拥有两者似乎太多层了。但是让两个存储库都依赖于两个工厂会有效吗?那么每个工厂都没有任何构造函数依赖。

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

匿名用户

长评论:

您可以通过“添加一个级别的嵌入”来延迟引起圆圈的对象的分辨率。由于通常您不需要构造函数中的工厂,因此您可以使用Func将实际分辨率推迟到您需要的点

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

  Func<IBRepository> _lazyBr;
}

请注意,UnityDI自动注册Func

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

匿名用户

在尝试了@CoderDennis和@AlexeiLevenkov非常有用的建议后,最后我重新评估了我的领域模型设计,并通过不再给子类一个属性来访问它们的父类来打破循环依赖问题。所以我只是避免了我最初遇到的问题,而不是真正解决它,但是设计的改变最终对我来说很有效。

感谢这两个人帮助我解决问题,帮助我意识到也许最好的主意是改变我解决问题的方法。