【问题标题】:Break circular dependency with constructor dependency injection使用构造函数依赖注入打破循环依赖
【发布时间】:2015-02-15 03:01:14
【问题描述】:

我的代码库大量使用接口/抽象、工厂、存储库、依赖注入和其他设计模式,以尝试编写好的、可维护的代码。我的 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);

通过将具体类注册到它们各自的接口(使用RegisterSingle&lt;T&gt; 用于单例/非瞬态生命周期)来连接容器(未显示)。

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

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

如您所见,我已经在 AB 上使用了 ID,以试图减轻在每个类上存储直接对象引用可能引起的循环依赖问题(我之前被这个问题困扰过) )。

我考虑完全打破域模型的父/子关系(A.GetBs()B.GetA())并将该功能推入某种单独的查找服务,但这对我来说似乎是一种代码味道领域模型应该封装它们自己的关系。此外,存储库已经用于这种查找功能的目的(AB 已经拥有)。

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

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

我查看了以下内容,虽然我认为我掌握了他们告诉我的内容,但我很难将其与我的问题集联系起来:

总而言之,我如何在仍然使用构造函数 DI 的同时打破循环依赖问题,坚持并利用已建立的设计模式和最佳实践,并在理想情况下为 AB 的消费者维护一个不错的 API 到“点”他们通过关系的方式?任何帮助将不胜感激。

【问题讨论】:

  • 为什么有工厂和仓库?在不知道它们的作用的情况下,很难提出解决方案。您有依赖于您的存储库的工厂和依赖于您的工厂的存储库。
  • 尝试使用 Func&lt;IBFactory&gt;Func&lt;IAFactory&gt; 而不是 IBFactory/IAFactory - 这样您就可以消除一个额外重定向价格的循环依赖(Unity 同时注册 Func类/接口,可能你的容器需要一些额外的代码)
  • @CoderDennis 实际上,AB 需要额外的依赖项(之前未显示/讨论过),因此是工厂。因此,工厂将这些额外的依赖项作为它们自己的依赖项,并在每次调用Create() 时将它们提供给AB 的c'tors。存储库从 XML 源加载数据并提供域对象,因此存储库依赖于工厂。例如。 GetById() 询问 XML 源并使用工厂将查询结果转换为 IAIB 实例等。感谢您的帮助。
  • @AlexeiLevenkov 抱歉,我不太明白...我到底应该在哪里使用Func&lt;IAFactory&gt;/Func&lt;IBFactory&gt;
  • @prlc - 我添加了带有代码的注释作为“答案”(因为没有办法将体面的格式化程序代码放在注释中)。

标签: c# dependency-injection


【解决方案1】:

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

您问题中的示例用法会完美运行。

您将不得不更改某些内容,因为您的依赖关系图是循环的。您必须决定哪些对象真正相互依赖,哪些不依赖。要求 Bs 只有在存在父 A 的可用实例时才能创建是一种方法。

我不太了解您的工厂和存储库是做什么的。对我来说,拥有两者似乎太多了。但是让两个存储库都依赖于两个工厂是否可行?那么每个工厂就没有任何构造函数依赖了。

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

【讨论】:

  • 如果你可以假设你总是有一个A(或IA)实例来创建B,那是完全合理的。但是,客户端代码通常通过调用 IBRepository 上的方法来获取 IB 实例,例如IBRepository.FindById(),因此不负责实例化 B 实例,因此无法注入 IA
  • 如果您查看我原始帖子中的依赖关系图,您会发现这就是为什么BRepository 间接依赖于ARespository(以及随后的AFactory);因此,如果用户在B 的实现内部调用IB.GetA(),我们调用_ar.FindById(_aId) 方法来获取使用AFactory 构造的IA 实例(或A 具体实例)。
【解决方案2】:

长评:

您可以通过“添加一定程度的非直连”来延迟解决导致圆圈的对象。由于通常您不需要构造函数中的工厂,因此您可以使用 Func&lt;T&gt; 而不是 T 本身将实际分辨率推迟到您需要的点,其中函数类似于 ()=&gt; 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&lt;T&gt;T,我不确定如何在您使用的容器中实现它,但它应该类似于:

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

【讨论】:

  • 啊,现在说得通了。您的代码示例帮助很大,谢谢。我会进一步探索。
【解决方案3】:

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

感谢两位帮助我解决问题并帮助我认识到最好的办法就是改变解决问题的方法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-08-30
    • 1970-01-01
    • 1970-01-01
    • 2011-02-02
    • 1970-01-01
    • 2019-04-20
    • 1970-01-01
    • 2010-11-29
    相关资源
    最近更新 更多