Як уникнути божевілля конструктора залежної інжекції?


300

Я вважаю, що мої конструктори починають виглядати так:

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

з постійно зростаючим списком параметрів. Оскільки "Контейнер" - це мій контейнер для ін'єкцій залежності, чому я не можу просто зробити це:

public MyClass(Container con)

для кожного класу? Які недоліки? Якщо я це роблю, то відчувається, що я використовую прославлений статик. Будь ласка, поділіться своїми думками щодо безумства IoC та Dependency Injection.


64
чому ти передаєш контейнер? Я думаю, ви можете неправильно зрозуміти МОК
Пол Крісі

33
Якщо ваші конструктори вимагають більше або більше параметрів, ви можете робити занадто багато в цих класах.
Остін Салонен

38
Це не так, як ви робите впорскування конструктора. Об'єкти взагалі не знають про контейнер IoC, а також не повинні.
duffymo

Ви можете просто створити порожній конструктор, за допомогою якого ви телефонуєте DI безпосередньо, запитуючи потрібні речі. Це видалить конструктори Madnes, але вам потрібно переконатися, що ви використовуєте інтерфейс DI .. у випадку, якщо ви зміните свою систему DI на півдорозі. Чесно кажучи .. ніхто не повернеться робити це таким чином, хоча це те, що DI робить для того, щоб ввести його у ваш конструктор. doh
Пьотр Кула

Відповіді:


409

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

Однією з чудових переваг конструкторської інжекції є те, що вона робить порушення Принципу єдиної відповідальності яскраво очевидними.

Коли це станеться, настав час повернутися до служби фасадів . Коротше кажучи, створіть новий більш грубозернистий інтерфейс, який приховує взаємодію між деякими або всіма дрібнозернистими залежностями, які вам потрібні на даний момент.


8
+1 для кількісного визначення зусиль рефакторингу в єдину концепцію; приголомшливо :)
Ryan Emerle

46
насправді? Ви просто створили непрямий спосіб перемістити ці параметри до іншого класу, але вони все ще є! просто складніше боротися з ними.
незаперечний

23
@irreputable: У виродженому випадку, коли ми переносимо всі залежності в Агрегатну службу, я погоджуюся, що це просто інший рівень непрямості, який не приносить користі, тому мій вибір слів був трохи відключений. Однак справа в тому, що ми переносимо лише деякі тонкодисперсні залежності в Агрегатну службу. Це обмежує кількість перестановок залежностей як у новій Агрегатній службі, так і на залишення залежностей. Це робить обидва простішими справи.
Марк Семанн

92
Найкраще зауваження: "Однією з чудових переваг конструкторської інжекції є те, що вона робить порушення Принципу єдиної відповідальності яскраво очевидними".
Ігор Попов

2
@DonBox У цьому випадку ви можете написати нульові реалізації об'єктів, щоб зупинити рекурсію. Не те, що потрібно, але справа в тому, що впорскування конструктора не перешкоджає циклам - це лише дає зрозуміти, що вони є.
Марк Семанн

66

Я не думаю, що ваші конструктори класів повинні мати посилання на ваш контейнерний період IOC. Це являє собою непотрібну залежність між вашим класом та контейнером (тип залежності, яку МОК намагається уникнути!).


+1 Залежно від контейнера IoC важко змінити цей контейнер пізніше, не змінюючи купу коду у всіх інших класах
Tseng

1
Як би ви реалізували IOC, не маючи параметрів інтерфейсу на конструкторах? Я неправильно читаю ваш пост?
J Hunt

@J Hunt Я не розумію твого коментаря. Для мене параметри інтерфейсу означають параметри, які є інтерфейсами для залежностей, тобто, якщо ваш контейнер для введення залежностей ініціалізується MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(параметри інтерфейсу). Це не пов’язано з публікацією @ деривації, яку я трактую як кажу, що контейнер для ін'єкцій залежності не повинен вводити себе в свої об'єкти, тобтоMyClass myClass = new MyClass(this)
Джон Доу

25

Складність передачі параметрів не є проблемою. Проблема полягає в тому, що ваш клас робить занадто багато, і його слід розбивати більше.

Ін'єкційна залежність може виступати як попередження для занадто великих занять, особливо через посилення болю при проходженні всіх залежностей.


43
Виправте мене, якщо я помиляюся, але в якийсь момент ви повинні "склеїти все разом", і, таким чином, вам доведеться отримати більше ніж кілька залежностей для цього. Наприклад, у шарі «Перегляд», будуючи шаблони та дані для них, потрібно захопити всі дані з різних залежностей (наприклад, «послуги»), а потім поставити всі ці дані у шаблон і на екран. Якщо на моїй веб-сторінці є 10 різних «блоків» інформації, то для отримання цих даних мені потрібно 10 різних класів. Тож мені потрібно 10 залежностей у моєму класі View / Template?
Андрій

4

Я зіткнувся з аналогічним питанням щодо інжекцій на основі конструктора та наскільки складним воно було пройти у всіх залежностях.

Один із підходів, який я використовував у минулому, - це використовувати шаблон фасаду додатків за допомогою сервісного рівня. Це матиме грубу API. Якщо ця послуга залежить від сховищ, вона використовує інжектор сетевих приватних властивостей. Це вимагає створення абстрактної фабрики та переміщення логіки створення сховищ у заводські.

Детальний код з поясненням можна знайти тут

Кращі практики для IoC у складному сервісному рівні


3

Я читав цю цілу нитку двічі, і думаю, що люди відповідають на те, що знають, а не на те, що запитують.

Оригінальне запитання JP виглядає так, що він створює об'єкти, надсилаючи розв’язувач, а потім купу класів, але ми припускаємо, що ці класи / об'єкти самі є сервісами, дозрілими для ін'єкцій. Що робити, якщо їх немає?

JP, якщо ви хочете використовувати DI і бажаєте слави змішування ін'єкцій з контекстними даними, жоден із цих шаблонів (або передбачуваних "анти-шаблонів") конкретно не вирішує цього питання. Це насправді зводиться до використання пакету, який підтримає вас у такому починанні.

Container.GetSevice<MyClass>(someObject1, someObject2)

... цей формат рідко підтримується. Я вважаю, що складність програмування такої підтримки, додана до жалюгідної продуктивності, яка буде пов'язана з реалізацією, робить її непривабливою для розробників відкритих джерел.

Але це слід зробити, тому що я повинен мати можливість створити і зареєструвати завод для MyClass'es, і цей завод повинен мати можливість приймати дані / введення, які не підштовхуються до того, щоб бути "послугою" просто заради передачі дані. Якщо "антидіаграма" стосується негативних наслідків, то примушення існування штучних типів обслуговування для передачі даних / моделей, безумовно, негативно (нарівні з вашим почуттям щодо загортання класів у контейнер. Застосовується той же інстинкт).

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

Створення екземпляра за допомогою Ninject з додатковими параметрами в конструкторі

Це для .NET, є популярним, і все ще ніде не є таким чистим, як це має бути, але я впевнений, що на будь-якій мові ви не хочете використовувати.


3

Впорскування контейнера - це ярлик, про який ви врешті пошкодуєте.

Переваги ін'єкцій не є проблемою, це, як правило, симптом інших структурних недоліків, зокрема, розділення проблем. Це не одна проблема, але може бути багато джерел, і те, що робить це настільки складно виправити, це те, що вам доведеться мати справу з усіма ними, іноді одночасно (подумайте про розплутування спагеті).

Ось неповний перелік речей, на які слід звернути увагу

Поганий дизайн домену (сукупний корінь… тощо)

Погана розбіжність проблем (склад служби, команди, запити) Див. CQRS та пошук подій.

АБО Mappers (будьте обережні, ці речі можуть призвести до неприємностей)

Перегляньте Моделі та інші DTO (Ніколи не використовуйте їх повторно, і намагайтеся звести їх до мінімального !!!!)


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 Container
  • Клас є описовим (з точки зору його залежностей). За умовою, якщо ви хочете знати, від якого класу Aзалежить, ця інформація накопичується вA.Dependency
  • Підпис конструктора стає простим

Чому ні

  • потрібно створити додатковий клас
  • реєстрація послуги стає складною (потрібно реєструвати кожен X.Dependencyокремо)
  • Концептуально те саме, що проходити IoC Container
  • ..

Рішення 2 - лише необґрунтований факт, якщо проти нього є твердий аргумент, то описовий коментар буде вдячний


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


-8

Які рамки введення залежності ви використовуєте? Чи намагалися ви замість цього застосовувати ін’єкцію на основі сетера?

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

За допомогою Spring ви можете передавати необхідні значення замість сетерів, а ви можете використовувати @required примітки, щоб переконатися, що вони введені. Мінус полягає в тому, що вам потрібно перенести код ініціалізації з конструктора на інший метод і провести Spring call, що після того, як всі залежності будуть введені, позначивши його за допомогою @PostConstruct. Я не впевнений в інших структурах, але я припускаю, що вони роблять щось подібне.

Обидва способи роботи, це питання переваги.


21
Причина введення конструкторів - зробити залежність очевидною, а не тому, що вона виглядає більш природно для розробників Java.
L-Four

8
Пізній коментар, але ця відповідь змусила мене сміятися :)
Фредерік Прийк

1
+1 для ін’єкцій на основі сетера. Якщо у мене в класі є сервіси та сховища, це досить чортово очевидно, що це залежності. Мені не потрібно писати масивні конструктори VB6, а також дурно призначати код у конструкторі. Цілком очевидно, що залежність від необхідних полів.
Пьотр Кула

Станом на 2018 рік, Spring офіційно рекомендує не використовувати інжектор сетерів, за винятком залежностей, які мають розумне значення за замовчуванням. Як і у випадку, якщо залежність є обов'язковою для класу, рекомендується вводити конструктор. Дивіться дискусію щодо сеттера проти ctor DI
John Doe
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.