Решение циклических графов зависимостей в DI-контейнере - PullRequest
1 голос
/ 14 октября 2019

Мне нужно решить, как управлять взаимозависимыми экземплярами, зарегистрированными в созданном мною контейнере 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));
        }
    }

IЯ знаю, что большинство людей посоветуют использовать уже внедренный контейнер (например, ninject или autofac), но это решение, которое мне нужно реализовать для личного проекта, и будет благодарно за все советы, которые вы можете дать мне.

Ответы [ 2 ]

1 голос
/ 14 октября 2019

Но существует более серьезная проблема, которая заключается в том, что, когда я регистрирую два экземпляра, они взаимозависимы (например, класс 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 поддерживает обе эти функции без дополнительной регистрации; Динамическое создание (Func) и Отложенное создание (Ленивое) )

Другой вариант(Я не фанат) это использовать Property Injection.

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 голосов
/ 14 октября 2019

Большинство современных 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. Используйте свойство инъекции. Поскольку нет необходимости инициализировать свойства объекта при построении (они будут инициализированы со значениями по умолчанию), мы можем создать оба экземпляра классов A и B. Когда у нас есть оба экземпляра, мы можем установить их свойства A.b = instanceOfB и B.a = instanceOfA. Это способ, рекомендованный, например, Autofac, см. https://docs.autofac.org/en/latest/advanced/circular-dependencies.html.
  4. . Существует также более изощренный способ, который позволит клиенту по-прежнему использовать инъекцию конструктора - ленивая инициализация с некоторыми «прокси» -объектами. При создании B вместо внедрения экземпляра A мы внедряем какой-то экземпляр FakeA, который не имеет каких-либо зависимостей. То же самое с конструкцией A. Теперь у нас есть экземпляры A и B, и все, что нам нужно, это связать их с их Fake. Fake должен наследовать A для FakeA и B для FakeB и иметь свойство, которое будет установлено на ссылку на "реальный" объект. Так что на самом деле мы используем внедрение свойств, но это не видно клиенту. Такие Fake могут быть созданы с помощью некоторой магии отражения, см., Например, https://www.castleproject.org/projects/dynamicproxy/,.

Как третий, так и четвертый варианты будут иметь ограничения, которые должны учитывать как ваш код, так и клиент,Например, при внедрении свойств могут возникнуть ошибки, когда клиентский код попытается получить доступ к свойству, прежде чем ваш контейнер установит его для экземпляра объекта. С четвертым вариантом проблемы более сложны, как и само решение. Например, нет способа унаследовать от запечатанного класса, поэтому нет способа создать прокси.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...