解决 DI 容器中的循环依赖图

Solving cyclic dependency graphs in a DI Container

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

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

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),但这是我需要为个人项目实现的解决方案,我会感谢所有你可以给我的建议。

But exists an bigger issue that is when I register two instances are co-dependant (e.g. An class A that needs a B instance, but the B needs an A instance)

这是一个 Code Smell 某些东西(很可能)设计得很糟糕。通常最好的选择是redesign/refactor要求去除这种循环依赖。循环依赖的最大问题之一是每个依赖调用另一个依赖的能力导致每个依赖以一种递归的方式调用另一个的方法,直到系统因堆栈溢出而失败。

I need to implement for a personal project and will apreciate all the advices you can give me.

话虽这么说,但如果确实需要,几乎没有什么方法可以做到这一点。在我个人看来,下一个最佳选择(在 redesign/refactor 之后)是使用 deferred/delegate 构造函数注入。这可能看起来像:

 // "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) and 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 属性 来工作时并不总是很明显(也根本没有注入(B2))。

大多数现代 DI 容器不允许通过构造函数注入进行循环依赖。然而,有一些策略可以克服这些限制。例如,假设您有 class A 依赖于 class B 并且 class B 依赖于 class 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. 使用属性注入。因为不需要在构造时初始化对象属性(它们将使用默认值初始化),我们可以创建 classes AB 的两个实例。当我们有两个实例时,我们可以设置它们的属性,A.b = instanceOfBB.a = instanceOfA。这是推荐的方式,例如Autofac,见https://docs.autofac.org/en/latest/advanced/circular-dependencies.html.
  4. 还有更复杂的方法,允许客户端使用仍然使用构造函数注入 - 使用一些 "proxy" 对象进行延迟初始化。在创建 B 而不是注入 A 的实例时,我们注入了某种没有任何依赖关系的 FakeA 实例。与构造 A 相同。现在我们有 AB 个实例,我们只需要将它们 link 到它们的 Fake 即可。 Fake 应该继承 FakeAAFakeBB 并且有一个 属性 将被设置为 [=51= 的引用] 目的。所以实际上我们使用了 属性 注入,但这对客户端代码是不可见的。这样的 Fake 可以用一些反射魔法来创建,例如参见 [​​=35=]。

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