【问题标题】:Solving cyclic dependency graphs in a DI Container解决 DI 容器中的循环依赖图
【发布时间】:2019-10-14 14:59:15
【问题描述】:

我需要解决如何管理在我创建的 DI 容器中注册的相互依赖的实例。

我通过构造函数注入创建了一个简单的 DI 容器,它适用于简单的任务。但是存在一个更大的问题,即当我注册两个实例是相互依赖的(例如,一个类 A 需要一个 B 实例,但 B 需要一个 A 实例),落入 stackoverflow。

public class DIContainer : IContainer
    {
        private readonly Dictionary<Type, Func<object>> _registeredTypes = new Dictionary<Type, Func<object>>();

        public object GetInstance(Type type)
        {
            if (_registeredTypes.ContainsKey(type))
            {
                return _registeredTypes[type]();
            }
            else
            {
                return null;
            }
        }

        private object CreateInstance(Type type)
        {
            var constructor = type.GetConstructors()
                .OrderByDescending(c => c.GetParameters().Length)
                .First();

            var args = constructor.GetParameters().Select(p => GetInstance(p.ParameterType)).Where(a => a != null).ToArray();

            return Activator.CreateInstance(type, args);
        }

        public T Get<T>()
        {
            return (T)GetInstance(typeof(T));
        }

        public void Register<I, C>()
        {
            Register(typeof(I), typeof(C));
        }

        public void RegisterSinglenton<I, C>()
            where C : I
        {
            var instance = CreateInstance(typeof(C));
            RegisterSinglenton<I>((C)instance);
        }

        public void RegisterSinglenton<T>(T obj)
        {
            _registeredTypes.Add(typeof(T), () => obj);
        }

        public void Register(Type service, Type implementation)
        {
            _registeredTypes.Add(service, () => CreateInstance(implementation));
        }
    }

我知道大多数人会建议使用已经实现的容器(如 ninject 或 autofac),但这是我需要为个人项目实施的解决方案,并且会感谢您可以给我的所有建议.

【问题讨论】:

  • 在大多数现代 DI 容器中,循环依赖也会导致抛出异常(当然不是 StackOverflow,但仍然如此)。另一种方法是使用属性注入或注入某种“代理”对象,将未来的调用委托给实际实例。
  • 没有可以解决循环依赖的容器,你需要修复你的代码,因为这是不可能的。
  • 这不是DI问题,这是代码结构的问题。改一下,注册到 Di 容器的实现不会相互依赖。

标签: c# dependency-injection ioc-container


【解决方案1】:

但存在一个更大的问题,即当我注册两个实例时相互依赖(例如,A 类需要 B 实例,但 B 需要 A 实例)

这是Code Smell,表示某些东西(很可能)设计不佳。通常最佳选择是重新设计/重构需求以消除这种类型的循环依赖。循环依赖的最大问题之一是每个依赖调用另一个的能力,导致每个依赖以一种递归的方式调用另一个方法,直到系统失败并出现堆栈溢出。

我需要为个人项目实施,并会感谢您能给我的所有建议。

话虽如此,如果确实需要,则几乎没有办法做到这一点。在我个人看来,下一个最佳选择(在重新设计/重构之后)是使用延迟/委托构造函数注入。这可能看起来像:

 // "Func Factory"
 public class A
 {
   private Func<B> _bFactory;
   private B b
   {
     if (_b == null)
     {
      _b = _bFactory();
     }
     return _b;
   }

   public A(Func<B> bFactory)
   {
     _bFactory = bFactory;
   }

   public void SomeMethod()
   {
     b.DoSomething();
   }
 }

或更清洁的解决方案(恕我直言)

 // "Lazy Factory"
 public class A
 {
   private Lazy<B> _b;

   public A(Lazy<B> b)
   {
     _b = b;
   }

   public void SomeMethod()
   {
     b.Value.DoSomething();
   }
 }

在前面的两个示例中,B 的构造都被推迟到实际需要时。 (Autofac 支持这两个开箱即用,无需任何额外注册;Dynamic Instantiation (Func)Delayed Instantiation (Lazy)

另一种选择(我不是粉丝)是使用属性注入。

public class A
{
  // One way is to create an attribute for signaling a property to inject
  [MyDIFrameworkAttributeForPropertyInjection]
  public B B1 { get; set; }

  // Another way is to use reflection to loop through all properties
  // and if a Type is found in the container, inject it after instantiation
  public B B2 { get; set; }

  public A()
  {
    // WARNING, B1 AND B2 WILL ALWAYS BE NULL
    // IN THE CONSTRUCTOR AND ANY METHOD THE CONSTRUCTOR CALLS
    // BECAUSE IT CANNOT BE ASSIGNED UNTIL THE CLASS IS INSTANTIATED
  }
}

虽然这确实有效并且看起来很干净,但对于其他程序员来说,他们何时可以使用 Injected Property 来完成工作并不总是显而易见的(也根本不是它被注入 (B2))。

【讨论】:

    【解决方案2】:

    大多数现代 DI 容器不允许通过构造函数注入实现循环依赖。但是,有一些策略可以克服这些限制。例如,假设您有类 A 依赖于类 B 并且类 B 依赖于类 A

    1. 最好的方法是重构客户端的代码以摆脱循环依赖。这也将使客户的代码整体更好。在这种情况下,您的代码不需要更改。
    2. 在客户代码中使用Lazy 或其他一些持有者,例如,Castle Windsor 支持Lazy - https://github.com/castleproject/Windsor/blob/master/docs/whats-new-3.0.md#added-support-for-lazyt-components
    3. 使用属性注入。因为不需要在构造时初始化对象属性(它们将使用默认值初始化),我们可以创建类AB 的两个实例。当我们拥有两个实例时,我们可以设置它们的属性A.b = instanceOfBB.a = instanceOfA。这是推荐的方式,例如 Autofac,参见 https://docs.autofac.org/en/latest/advanced/circular-dependencies.html
    4. 还有更复杂的方法允许客户端使用仍然使用构造函数注入 - 使用一些“代理”对象进行延迟初始化。在创建B 而不是注入A 的实例时,我们注入某种没有任何依赖关系的FakeA 实例。与构造A 相同。现在我们同时拥有AB 实例,我们只需要将它们链接到它们的Fakes。 Fake 应该继承 A 用于 FakeAB 用于 FakeB 并具有将设置为“真实”对象引用的属性。所以实际上我们使用了属性注入,但这对客户端代码是不可见的。这种Fakes 可以通过一些反射魔法来创建,例如,参见https://www.castleproject.org/projects/dynamicproxy/

    第三个和第四个选项都有限制,您的代码和客户都必须考虑这一点。例如,使用属性注入时,当客户端代码在容器将其设置为对象实例之前尝试访问属性时,可能会出现错误。使用第四个选项,问题就像解决方案本身一样复杂。例如,无法从密封类继承,因此无法创建代理。

    【讨论】:

    • 谢谢!非常感谢。我有时间为此寻找解决方案,并且我尝试过使用 Property injection 进行一些操作,但它看起来很丑,以至于我不得不进行咨询。这很有帮助。
    猜你喜欢
    • 2023-01-30
    • 2020-11-12
    • 1970-01-01
    • 2013-02-04
    • 2010-10-27
    • 1970-01-01
    相关资源
    最近更新 更多