Как избежать безумия конструктора Dependency Injection?

300

Я обнаружил, что мои конструкторы начинают выглядеть так:

public MyClass(Container con, SomeClass1 obj1, SomeClass2, obj2.... )

с постоянно увеличивающимся списком параметров. Поскольку «Контейнер» является моим контейнером внедрения зависимостей, почему я не могу просто сделать это:

public MyClass(Container con)

для каждого класса? Каковы недостатки? Если я делаю это, я чувствую, что использую прославленную статику. Пожалуйста, поделитесь своими мыслями о безумии IoC и Dependency Injection.

JP Richardson
источник
64
почему вы передаете контейнер? Я думаю, что вы можете неправильно понять МОК
Пол Creasey
33
Если ваши конструкторы требуют больше или больше параметров, вы можете делать слишком много в этих классах.
Остин Салонен
38
Это не так, как вы делаете инъекцию конструктора. Объекты вообще не знают о контейнере IoC, и не должны.
duffymo
Вы можете просто создать пустой конструктор, в котором вы вызываете DI, напрямую спрашивая, что вам нужно. Это удалит конструктор madnes, но вам нужно убедиться, что вы используете интерфейс DI ... на случай, если вы измените свою систему DI на полпути вниз разработки. Честно говоря .. никто не вернется к этому, даже если это то, что DI делает для внедрения в ваш конструктор. дох
Петр Кула

Ответы:

410

Вы правы в том, что если вы используете контейнер в качестве локатора службы, это более или менее прославленная статическая фабрика. По многим причинам я считаю это анти-паттерном .

Одним из замечательных преимуществ использования Constructor Injection является то, что он делает нарушения принципа единой ответственности совершенно очевидными.

Когда это произойдет, пришло время провести рефакторинг в Facade Services . Короче говоря, создайте новый, более грубый интерфейс, который скрывает взаимодействие между некоторыми или всеми мелкозернистыми зависимостями, которые вам требуются в настоящее время.

Марк Симанн
источник
8
+1 за количественную оценку усилий по рефакторингу в единую концепцию; Круто :)
Райан Эмерл
46
серьезно? Вы только что создали косвенное обращение, чтобы переместить эти параметры в другой класс, но они все еще там! просто сложнее с ними бороться.
неопровержимый
23
@irreputable: в вырожденном случае, когда мы переносим все зависимости в агрегированную службу, я согласен, что это просто еще один уровень косвенности, который не приносит никакой пользы, поэтому мой выбор слов был немного не в порядке. Однако дело в том, что мы перемещаем только некоторые из детализированных зависимостей в агрегированную службу. Это ограничивает число перестановок зависимостей как в новой Агрегированной службе, так и для оставленных зависимостей. Это упрощает работу с обоими.
Марк Симанн
92
Лучшее замечание: «Одним из замечательных преимуществ использования Constructor Injection является то, что он делает нарушения принципа единой ответственности совершенно очевидными».
Игорь Попов
2
@DonBox В этом случае вы можете написать реализацию нулевого объекта, чтобы остановить рекурсию. Не то, что вам нужно, но дело в том, что Constructor Injection не предотвращает циклы - оно только дает понять, что они есть.
Марк
66

Я не думаю, что ваши конструкторы классов должны иметь ссылку на ваш период контейнера IOC. Это представляет ненужную зависимость между вашим классом и контейнером (тип зависимости, которую IOC пытается избежать!).

вывод
источник
+1 В зависимости от контейнера IoC сложно изменить этот контейнер позже, не меняя кучу кода во всех других классах
Tseng
1
Как бы вы реализовали IOC, не имея параметров интерфейса на конструкторах? Я неправильно читаю ваш пост?
J Hunt
@J Охота, я не понимаю ваш комментарий. Для меня параметры интерфейса означают параметры, которые являются интерфейсами для зависимостей, то есть, если ваш контейнер внедрения зависимостей инициализируется MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(параметры интерфейса). Это не связано с постом @ производного, который, как я понимаю, говорит, что контейнер внедрения зависимостей не должен внедрять себя в свои объекты, т. MyClass myClass = new MyClass(this)
Джон Доу
25

Трудность передачи параметров не является проблемой. Проблема в том, что ваш класс делает слишком много, и его нужно больше разбивать.

Внедрение зависимостей может служить ранним предупреждением для классов, становящихся слишком большими, особенно из-за усиливающейся боли во всех зависимостях.

kyoryu
источник
44
Поправьте меня, если я ошибаюсь, но в какой-то момент вы должны «склеить все вместе», и, таким образом, вы должны получить больше, чем несколько зависимостей для этого. Например, в слое View, при создании шаблонов и данных для них, вы должны получить все данные из различных зависимостей (например, «сервисы»), а затем поместить все эти данные в шаблон и на экран. Если на моей веб-странице есть 10 разных «блоков» информации, мне нужно 10 разных классов, чтобы предоставить мне эти данные. Итак, мне нужно 10 зависимостей в мой класс View / Template?
Андрей
4

Я натолкнулся на похожий вопрос о внедрении зависимостей на основе конструктора и о том, насколько сложным было передать все зависимости.

Один из подходов, который я использовал в прошлом, заключается в использовании шаблона фасада приложения с использованием сервисного уровня. Это будет иметь грубый API. Если этот сервис зависит от репозиториев, он будет использовать метод установки частных свойств. Это требует создания абстрактной фабрики и перемещения логики создания репозиториев в фабрику.

Подробный код с объяснением можно найти здесь

Лучшие практики для IoC на уровне сложных сервисов

samsur
источник
3

Я прочитал всю эту ветку дважды, и я думаю, что люди отвечают тем, что они знают, а не тем, что спрашивают.

Исходный вопрос JP выглядит так, как будто он строит объекты, отправляя распознаватель, а затем группу классов, но мы предполагаем, что эти классы / объекты сами по себе являются сервисами, готовыми для внедрения. Что, если они не?

JP, если вы хотите использовать DI и хотите , чтобы у вас была возможность смешивать инъекции с контекстными данными, ни один из этих паттернов (или предполагаемых «анти-паттернов») специально не решает эту проблему. Это на самом деле сводится к использованию пакета, который будет поддерживать вас в таких усилиях.

Container.GetSevice<MyClass>(someObject1, someObject2)

... этот формат редко поддерживается. Я считаю, что сложность программирования такой поддержки, добавляемая к жалкой производительности, которая может быть связана с реализацией, делает ее непривлекательной для разработчиков с открытым исходным кодом.

Но это должно быть сделано, потому что я должен иметь возможность создавать и регистрировать фабрику для MyClass'а, и эта фабрика должна иметь возможность получать данные / ввод, которые не превращаются в «службу» только для передачи данные. Если «анти-паттерн» связан с негативными последствиями, то форсирование существования искусственных типов сервисов для передачи данных / моделей, безусловно, является негативным (наравне с вашим чувством оборачивания ваших классов в контейнер. Применяется тот же инстинкт).

Однако существуют рамки, которые могут помочь, даже если они выглядят немного некрасиво. Например, Ninject:

Создание экземпляра с использованием Ninject с дополнительными параметрами в конструкторе

Это для .NET, популярно и еще нигде не так чисто, как должно быть, но я уверен, что есть что-то на любом языке, который вы выберете.

Крейг Брунетти
источник
3

Внедрение контейнера - это быстрый путь, о котором вы в конечном итоге пожалеете.

Чрезмерная инъекция не является проблемой, это, как правило, является признаком других структурных недостатков, в первую очередь разделения проблем. Это не одна проблема, но она может иметь много источников, и что делает это так трудно исправить, это то, что вам придется иметь дело со всеми из них, иногда в одно и то же время (подумайте о спагетти без сплетений).

Вот неполный список вещей, которые нужно посмотреть

Плохой дизайн домена (агрегированные корни… и т. Д.)

Плохое разделение интересов (состав службы, команды, запросы). См. CQRS и Sourcing Event.

ИЛИ Mappers (будьте осторожны, эти вещи могут привести к неприятностям)

Просмотр моделей и других DTO (никогда не используйте повторно и старайтесь свести их к минимуму !!!!)

Marius
источник
2

Проблема:

1) Конструктор с постоянно увеличивающимся списком параметров.

2) Если класс наследуется (например:), RepositoryBaseто изменение сигнатуры конструктора вызывает изменения в производных классах.

Решение 1

Перейти IoC Containerк конструктору

Зачем

  • Больше не нужно постоянно увеличивать список параметров
  • Подпись конструктора становится простой

Почему нет

  • Делает вас класс тесно связан с контейнером IoC. (Это вызывает проблемы, когда 1. вы хотите использовать этот класс в других проектах, где вы используете другой контейнер IoC. 2. вы решили изменить контейнер IoC)
  • Делает ваш класс менее описательным. (Вы не можете реально взглянуть на конструктор класса и сказать, что ему нужно для работы.)
  • Класс может получить доступ ко всем услугам.

Решение 2

Создайте класс, который группирует весь сервис и передайте его конструктору

 public abstract class EFRepositoryBase 
 {
    public class Dependency
    {
        public DbContext DbContext { get; }
        public IAuditFactory AuditFactory { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
        }
    }

    protected readonly DbContext DbContext;        
    protected readonly IJobariaAuditFactory auditFactory;

    protected EFRepositoryBase(Dependency dependency)
    {
        DbContext = dependency.DbContext;
        auditFactory= dependency.JobariaAuditFactory;
    }
  }

Производный класс

  public class ApplicationEfRepository : EFRepositoryBase      
  {
     public new class Dependency : EFRepositoryBase.Dependency
     {
         public IConcreteDependency ConcreteDependency { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory,
            IConcreteDependency concreteDependency)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
            ConcreteDependency = concreteDependency;
        }
     }

      IConcreteDependency _concreteDependency;

      public ApplicationEfRepository(
          Dependency dependency)
          : base(dependency)
      { 
        _concreteDependency = dependency.ConcreteDependency;
      }
   }

Зачем

  • Добавление новой зависимости к классу не влияет на производные классы
  • Класс агностик IoC Контейнер
  • Класс описательный (в аспекте его зависимостей). По соглашению, если вы хотите знать, от чего Aзависит класс , эта информация накапливается вA.Dependency
  • Подпись конструктора становится простой

Почему нет

  • нужно создать дополнительный класс
  • Услуга регистрации становится сложной (нужно регистрироваться каждый X.Dependencyотдельно)
  • Концептуально так же, как прохождение IoC Container
  • ..

Решение 2 просто необработанное, хотя, если есть веские аргументы против него, то желательно иметь описательный комментарий

tchelidze
источник
1

Это подход, который я использую

public class Hero
{

    [Inject]
    private IInventory Inventory { get; set; }

    [Inject]
    private IArmour Armour { get; set; }

    [Inject]
    protected IWeapon Weapon { get; set; }

    [Inject]
    private IAction Jump { get; set; }

    [Inject]
    private IInstanceProvider InstanceProvider { get; set; }


}

Вот грубый подход, как выполнить инъекции и запустить конструктор после введения значений. Это полностью функциональная программа.

public class InjectAttribute : Attribute
{

}


public class TestClass
{
    [Inject]
    private SomeDependency sd { get; set; }

    public TestClass()
    {
        Console.WriteLine("ctor");
        Console.WriteLine(sd);
    }
}

public class SomeDependency
{

}


class Program
{
    static void Main(string[] args)
    {
        object tc = FormatterServices.GetUninitializedObject(typeof(TestClass));

        // Get all properties with inject tag
        List<PropertyInfo> pi = typeof(TestClass)
            .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
            .Where(info => info.GetCustomAttributes(typeof(InjectAttribute), false).Length > 0).ToList();

        // We now happen to know there's only one dependency so we take a shortcut just for the sake of this example and just set value to it without inspecting it
        pi[0].SetValue(tc, new SomeDependency(), null);


        // Find the right constructor and Invoke it. 
        ConstructorInfo ci = typeof(TestClass).GetConstructors()[0];
        ci.Invoke(tc, null);

    }
}

В настоящее время я работаю над проектом хобби, который работает следующим образом: https://github.com/Jokine/ToolProject/tree/Core

Ronijo
источник
-8

Какую инфраструктуру внедрения зависимостей вы используете? Вы пробовали использовать инъекцию на основе сеттера?

Преимущество внедрения на основе конструктора состоит в том, что это выглядит естественным для Java-программистов, которые не используют DI-инфраструктуры. Вам нужно 5 вещей для инициализации класса, тогда у вас есть 5 аргументов для вашего конструктора. Недостатком является то, что вы заметили, это становится громоздким, когда у вас много зависимостей.

В Spring вы можете вместо этого передавать требуемые значения с помощью сеттеров и использовать аннотации @required, чтобы обеспечить их внедрение. Недостатком является то, что вам нужно переместить код инициализации из конструктора в другой метод и иметь вызов Spring, который после внедрения всех зависимостей помечает его как @PostConstruct. Я не уверен насчет других фреймворков, но я предполагаю, что они делают нечто подобное.

Оба способа работают, это вопрос предпочтений.

Дэвид У Крук
источник
21
Причина внедрения конструктора состоит в том, чтобы сделать зависимости очевидными, а не потому, что это выглядит более естественным для разработчиков Java.
L-Four
8
Поздний комментарий, но этот ответ заставил меня рассмеяться :)
Фредерик Прийк,
1
+1 для инъекций на основе сеттера. Если у меня есть службы и репозитории, определенные в моем классе, то чертовски очевидно, что они являются зависимостями. Мне не нужно писать массивные конструкторы, выглядящие на VB6, и глупо присваивать код в конструкторе. Довольно очевидно, каковы зависимости от требуемых полей.
Петр Кула
По состоянию на 2018 г. Spring официально рекомендует не использовать внедрение метода установки, за исключением зависимостей, которые имеют разумное значение по умолчанию. Например, если зависимость является обязательной для класса, рекомендуется внедрение в конструктор. Смотрите обсуждение о сеттере против ctor DI
Джон Доу