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

问题描述 投票:1回答:2

我需要解决如何管理在我创建的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),但这是我需要针对个人项目实现的解决方案,并且会赞赏您能给我的所有建议。

c# dependency-injection ioc-container
2个回答
1
投票

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

Code Smell这是(很可能)设计不当的内容。通常,最佳选项是重新设计/重构要求,以消除这种循环依赖类型。循环依赖关系的最大问题之一是每个依赖关系以某种方式调用另一个依赖关系的能力,这种方式导致每个依赖关系以某种recursive方式在另一个依赖关系上调用方法,直到系统出现堆栈溢出故障为止。

我需要为一个个人项目实施,并将感谢您可以给我的所有建议。

话虽这么说,但真正需要的方法很少。我个人认为,下一个最佳选择(在重新设计/重构之后)是使用延迟/委托构造函数注入。可能看起来像:

 // "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();
   }
 }

或更清洁的解决方案(IMHO)

 // "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属性进行工作时也不总是很明显(也不是完全被注入(B2)。]


0
投票

[大多数现代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实例,我们所需要做的就是将它们链接到它们的FakeFake应该继承AFakeABFakeB,并具有将设置为对“真实”对象的引用的属性。因此,实际上我们使用属性注入,但是这对于客户端代码是不可见的。可以使用一些反射魔术来创建此类Fake,例如,请参见https://www.castleproject.org/projects/dynamicproxy/

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

© www.soinside.com 2019 - 2024. All rights reserved.