Який "правильний" спосіб впровадження DI в .NET?


22

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

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

Чи буде правильним підходом впроваджувати ін'єкційну залежність у мою програму чи я повинен реалізовувати її за якимсь іншим принципом?


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

Важко відповісти, не знаючи, який саме проект .net ви будуєте. Наприклад, хороша відповідь для WPF може бути поганою відповіддю для MVC.
JMK

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

Відповіді:


30

Ще не думайте про інструмент, який ви збираєтеся використовувати. Ви можете робити DI без контейнера IoC.

Перший момент: У Марка Семана є дуже хороша книга про DI в .Net

Друга: корінь складу. Переконайтесь, що вся настройка виконана у точці входу проекту. Решта вашого коду повинні знати про ін'єкції, а не про будь-який інструмент, який використовується.

По-третє: інжекція конструктора - це найбільш вірогідний шлях (є випадки, коли ви цього не хотіли б, але не так багато).

По-четверте: вивчіть використання лямбда-фабрик та інші подібні функції, щоб уникнути створення непотрібних інтерфейсів / класів з єдиною метою ін'єкції.


5
Всі відмінні поради; особливо перша частина: навчіться робити чисті DI, а потім почніть дивитись на контейнери IoC, які можуть зменшити кількість кодового шаблону, необхідного для цього підходу.
Девід Арно

6
... або пропускати контейнери IoC разом, щоб зберегти всі переваги статичної перевірки.
День

Мені подобається, що ця порада є гарною відправною точкою, перш ніж потрапити в DI, і у мене є книга Марка Семана.
Снуп

Я можу другий відповісти. Ми успішно використовували бедний mans-DI (рукописний завантажувач) у досить великому застосуванні, розбивши частини логіки завантажувача в модулі, що складали додаток.
перук

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

13

У вашому питанні є дві частини - як правильно виконати DI, і як переробити великий додаток для використання DI.

Перша частина добре відповідає @Miyamoto Akira (особливо рекомендація прочитати книгу Марка Семана "Ін'єкція залежності в .net". Блог Marks також є хорошим безкоштовним ресурсом.

Друга частина - це набагато складніше.

Хорошим першим кроком було б просто перемістити всю інстанцію до конструкторів класів - не вводити залежності, просто переконавшись, що ви лише зателефонуєте newв конструктор.

Це виділить усі ваші порушення SRP, які ви робили, тож ви можете почати розбивати клас на менших співпрацівників.

Наступним номером, який ви знайдете, будуть класи, які спираються на параметри виконання для будівництва. Зазвичай це можна виправити, створивши прості фабрики, часто з Func<param,type>, ініціалізуючи їх у конструкторі та викликаючи їх у методах.

Наступним кроком буде створення інтерфейсів для ваших залежностей та додавання до ваших класів другого конструктора, крім цих інтерфейсів. Ваш конструктор без параметрів створив би конкретні екземпляри і передав їх новому конструктору. Це зазвичай називають "B * stard Injection" або "Poor mans DI".

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

Можна, звичайно, піти далі. Якщо ви маєте намір використовувати контейнер IOC, наступним кроком може бути заміна всіх прямих викликів newу ваших безпараметричних конструкторах статичними викликами до контейнера IOC, по суті (ab) використанням його як локатора сервісу.

Це призведе до виникнення більшої кількості параметрів конструктора виконання, для вирішення яких було раніше.

Після цього ви можете приступити до видалення конструкторів без параметрів та рефактора до чистого DI.

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


3
Дякую за дуже детальну відповідь. Ви дали мені кілька ідей, як підійти до проблеми, з яким я стикаюся. Вся архітектура програми вже побудована на увазі IoC. Основна причина, за яку я хочу використовувати DI, - це навіть не тестування одиниць, а бонус, а можливість заміни різних реалізацій на різні інтерфейси, визначені в ядрі мого додатку, якнайменше зусиль. Програма, про яку йде мова, працює в умовах, що постійно змінюється, і мені часто доводиться міняти місцями додатки, щоб використовувати нові реалізації відповідно до змін у середовищі.
користувач3223738

1
Радий, що я можу допомогти, і так, я погоджуюся, що найбільша перевага DI - це вільне з'єднання та можливість легко переналаштувати, що це приносить, при цьому тестування одиниць є приємним побічним ефектом.
Стів

1

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

Ви сказали, що це велика програма, тому для початку виберіть невеликий компонент. Переважно компонент "вузол листя", який не використовується нічим іншим. Я не знаю, який стан автоматизованого тестування в цій програмі, але ви порушите всі одиничні тести для цього компонента. Тож будьте готові до цього. Крок 0 - це написання інтеграційних тестів для компонента, який ви будете змінювати, якщо вони вже не існують. В крайньому випадку (відсутність тестової інфраструктури; жодної плати за її написання), з’ясуйте ряд ручних тестів, які можна зробити, щоб переконатися, що цей компонент працює.

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

  1. Інваріантна змінна члена: Це змінні, які встановлюються один раз (як правило, в конструкторі) і не переназначаються протягом життя об'єкта. Для цього ви можете вставити екземпляр об'єкта в конструктор. Ви, як правило, не несете відповідальності за утилізацію цих об'єктів (я не хочу говорити ніколи тут, але ви справді не повинні мати такої відповідальності).

  2. Змінна варіатора / змінної методу: Це змінні, які отримають сміття, зібране в якийсь момент протягом життя об'єкта. Для цього вам потрібно ввести завод у свій клас, щоб надати ці екземпляри. Ви несете відповідальність за утилізацію об'єктів, створених заводом.

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

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

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


1
Гарна порада: починайте з невеликого компонента, а не з "
великого переписування

0

Правильний підхід полягає у використанні конструкторської інжекції, якщо ви використовуєте

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

то ви в кінцевому підсумку використовуєте локатор обслуговування, ніж введення залежності.


Звичайно. Інжекція конструктора. Скажімо, у мене є клас, який приймає реалізацію інтерфейсу як один з аргументів. Але мені все ж потрібно створити екземпляр реалізації інтерфейсу і десь передати його цьому конструктору. Переважно це повинен бути якийсь централізований фрагмент коду.
користувач3223738

2
Ні, коли ви ініціалізуєте контейнер DI, вам потрібно вказати реалізацію інтерфейсів, і контейнер DI створить екземпляр та введено в конструктор.
Пелікан низького польоту

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

2
Якщо вводиться 10 різних служб, це тому, що хтось порушує SRP, який слід розділити на більш дрібні компоненти.
Пелікан низького польоту

1
@Fabio Питання в тому, що б тобі купити. Я ще не бачив приклад, коли мати клас гігантів, який займається десятками абсолютно різних речей, - це гарний дизайн. Єдине, що робить DI - зробити всі ці порушення більш очевидними.
Ву

0

Ви кажете, що хочете ним скористатися, але не вказуйте, чому.

DI - це не що інше, як надання механізму генерації конкрементів з інтерфейсів.

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

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

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


0

Насправді, «правильний» спосіб - це взагалі НЕ використовувати завод, якщо немає іншого вибору (як при тестуванні одиниць та певних макетах - для виробничого коду ви НЕ використовуєте фабрику)! Це - насправді анти-модель, і її слід уникати будь-якою ціною. Вся справа в контейнері DI полягає в тому, щоб гаджет міг зробити роботу за вас.

Як було сказано вище в попередньому дописі, ви хочете, щоб ваш гаджет IoC взяв на себе відповідальність за створення різних залежних об'єктів у вашому додатку. Це означає, що дозволяти вашому гаджету DI створювати та керувати самими різними екземплярами. У цьому вся суть за DI - ваші об’єкти НІКОЛИ не повинні знати, як створювати та / або керувати об’єктами, від яких вони залежать. В іншому випадку розривається нещільна муфта.

Перетворення наявного додатка на всі DI - це величезний крок, але відміняючи очевидні труднощі при цьому, ви також хочете (лише полегшити своє життя), щоб вивчити інструмент DI, який виконає основну частину ваших прив’язок автоматично (Ядро до щось на зразок Ninject - це "kernel.Bind<someInterface>().To<someConcreteClass>()"дзвінки, які ви робите, щоб відповідати оголошенням інтерфейсу тим конкретним класам, які ви хочете використовувати для реалізації цих інтерфейсів. Саме ті дзвінки "Прив'яжіть", які дозволяють вашому гаджету DI перехоплювати ваші виклики конструктора і надавати Для деяких класів типовим конструктором (псевдокодом, показаним тут) може бути:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

Зауважте, що ніде в цьому коді не було жодного коду, який створив / керував / випускав ні екземпляр SomeConcreteClassA, ні SomeOtherConcreteClassB. Власне кажучи, жоден конкретний клас навіть не посилався. Отже ... де трапилася магія?

У частині запуску вашої програми відбулося наступне (знову ж таки, це псевдокод, але він досить близький до реальної речі (Ninject) ...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

Цей маленький код там підказує гаджету Ninject шукати конструктори, сканувати їх, шукати екземпляри інтерфейсів, які він налаштований для обробки (це дзвінки "Прив’язати"), а потім створити та замінити екземпляр конкретного класу, де б не було на посилання на нього.

Є чудовий інструмент, який дуже добре доповнює Ninject під назвою Ninject.Extensions.Conventions (ще один пакет NuGet), який зробить основну частину цієї роботи за вас. Не відмовлятися від чудового досвіду навчання, який ви пройдете, будуючи це самостійно, але для того, щоб розпочати себе, це може бути інструментом для дослідження.

Якщо пам'ять служить, у Unity (формально від Microsoft зараз проект із відкритим кодом) є виклик методу або два, які роблять те саме, інші інструменти мають аналогічні помічники.

Який би шлях ви не вибрали, неодмінно прочитайте книгу Марка Семана для основної частини вашої підготовки до DI, однак, слід зазначити, що навіть "Великі" у світі інженерії програмного забезпечення (як Марк) можуть робити яскраві помилки - Марк забув про все Ninject у своїй книзі, ось ось ще один ресурс, написаний саме для Ninject. Я маю це і добре читаю: Освоєння нінжекту для введення в залежність


0

Немає «правильного шляху», але є кілька простих принципів:

  • Створіть корінь композиції при запуску програми
  • Після створення кореня композиції відкиньте посилання на контейнер / ядро ​​DI (або принаймні інкапсулюйте його, щоб воно не було безпосередньо доступне у вашій програмі)
  • Не створюйте екземпляри за допомогою "new"
  • Передайте всі необхідні залежності як абстракцію конструктору

Це все. Напевно, це принципи, а не закони, але якщо ви їх дотримуєтесь, ви можете бути впевнені, що ви робите DI (будь ласка, виправте мене, якщо я помиляюся).


Отже, як створити об’єкти під час виконання без "нового" і не знаючи контейнера DI?

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.