Коли не доцільно використовувати схему введення залежності?


124

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


1
Пов'язані (ймовірно , не дублювати) - stackoverflow.com/questions/2407540 / ...
Steve314

3
Звучить трохи як анти-візерунок «Золотий молоток». en.wikipedia.org/wiki/Anti-pattern
Cuga

Відповіді:


123

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

  • По-перше, в основному, DI припускає, що щільне з'єднання реалізацій об'єктів ЗАВЖДИ погано . У цьому суть принципу інверсії залежності: "залежність ніколи не повинна бути зроблена на конкременті; лише на абстракції".

    Це закриває залежний об'єкт для зміни на основі зміни конкретної реалізації; клас залежно від ConsoleWriter конкретно потрібно буде змінити, якщо для виведення потрібно перейти до файлу, але якщо клас залежав лише від IWriter, що відкриває метод Write (), ми можемо замінити ConsoleWriter, який зараз використовується, на FileWriter і наш Залежний клас не знав би різниці (Принцип заміни Лішкова).

    Однак дизайн НІКОЛИ не може бути закритий для всіх типів змін; якщо сама структура інтерфейсу IWriter змінюється, щоб додати параметр Write (), додатковий об'єкт коду (інтерфейс IWriter) тепер повинен бути змінений, поверх об'єкта / методу реалізації та його використання. Якщо зміни у фактичному інтерфейсі є більш імовірними, ніж зміни у реалізації зазначеного інтерфейсу, нещільне з'єднання (та DI-ing слабко пов'язаних залежностей) може спричинити більше проблем, ніж це вирішує.

  • По-друге, і як наслідок, DI передбачає, що залежний клас НІКОЛИ не є хорошим місцем для створення залежності . Це стосується принципу єдиної відповідальності; якщо у вас є код, який створює залежність, а також його використовує, то є дві причини, залежні класу, можливо, доведеться змінити (зміна використання або АБО реалізація), що порушує SRP.

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

  • Нарешті, DI за своєю природою централізує знання про всі залежності та їх реалізацію . Це збільшує кількість посилань, які повинна мати збірка, яка виконує впорскування, і в більшості випадків НЕ зменшує кількість посилань, необхідних для фактичних залежностей класів.

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

    Він також зазвичай не видаляє вказані посилання з нижнього коду; залежний повинен все-таки посилатися на бібліотеку, що містить інтерфейс для її залежності, який знаходиться в одному з трьох місць:

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

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


1
SRP = єдиний принцип відповідальності для всіх, хто цікавиться.
Теодор Мердок

1
Так, і LSP - це принцип заміни Ліскова; враховуючи залежність від A, залежність повинна бути спроможна задовольнити B, що походить від A без будь-яких інших змін.
KeithS

ін'єкційна залежність спеціально допомагає там, де вам потрібно отримати клас А з класу В з класу D тощо. У такому випадку (і тільки в цьому випадку) рамка DI може генерувати менше монтажу, ніж введення бідного чоловіка. також у мене ніколи не було вузького місця, спричиненого DI .. думаю, що витрати на обслуговування, ніколи не витрачаються на процесорний процес, тому що код завжди можна оптимізувати, але це робити без причин коштує
GameDeveloper

Чи було б іншим припущенням "якщо залежність зміниться, вона зміниться всюди"? Інакше вам доведеться все-таки переглянути всі споживачі заняття
Річард Тінгл,

3
Ця відповідь не дозволяє вирішити вплив доказовості введення залежностей від їх жорсткого кодування. Дивіться також Принцип явних залежностей ( deviq.com/explicit-dependitions-principle )
ssmith

30

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

Ви трохи зменшили зв’язок між A і B, але зменшили інкапсуляцію A і збільшили зв'язок між A і будь-яким класом, який повинен побудувати екземпляр A, приєднавши їх також до залежності А.

Тож введення залежності (без рамки) приблизно так само шкідливо, як і корисно.

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

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

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

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

У рамках залежності від введення залежності компроміси дещо відрізняються; те, що ви втрачаєте, вводячи залежність, - це можливість легко знати, на яку реалізацію ви покладаєтесь, і перекладати відповідальність за рішення, на яку залежність ви покладаєтесь, на якийсь автоматизований процес вирішення (наприклад, якщо нам потрібен @ Inject'ed Foo , повинно бути щось, що @Provides Foo і чиї введені залежності доступні), або якийсь файл конфігурації високого рівня, який прописує, який провайдер слід використовувати для кожного ресурсу, або якийсь гібрид із двох (наприклад, бути автоматизованим процесом розв’язання залежностей, які при необхідності можуть бути замінені, використовуючи файл конфігурації).

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

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

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


6
Просто короткий коментар про глузування залежності в тестах і "ви повинні знати про свої залежності" залежностей "... Це зовсім не так. Не потрібно знати або дбати про конкретні залежності від впровадження, якщо макет вводиться. Потрібно лише знати про прямі залежності досліджуваного класу, які задовольняються макетами.
Ерік Кінг

6
Ви неправильно розумієте, я не говорю про те, коли ви вводите макет, я кажу про реальний код. Розглянемо клас A із залежністю B, яка, в свою чергу, має залежність C, яка, в свою чергу, має залежність D. Без DI, A будує B, B будує C, C конструкцій D. З інжекцією в конструкцію A, спочатку потрібно побудувати B, щоб побудувати B, ви повинні спочатку побудувати C, а щоб побудувати C, ви повинні спочатку побудувати D. Отже, клас A тепер повинен знати про D, залежність залежності від залежності, щоб побудувати B. Це призводить до надмірної сполученості.
Теодор Мердок

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

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

2
@Sprague Dependency Device переміщується навколо залежностей у злегка від'ємній грі, якщо ви застосовуєте її у випадках, коли це не слід застосовувати. Мета цієї відповіді - нагадати людям, що введення залежності не є за своєю суттю хорошим, це схема дизайну, яка неймовірно корисна в правильних обставинах, але невиправдана вартість у звичайних обставинах (принаймні, в областях програмування, в яких я працював , Я розумію, що в деяких областях це частіше корисно, ніж в інших). ІН іноді стає модним словом і самоціллю, що пропускає суть.
Теодор Мердок

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

  2. якщо вам потрібно створити примітивні об'єкти, такі як ints або longs. Також вам слід створити "від руки" більшість стандартних об'єктів бібліотеки, таких як дати, путівники тощо.

  3. якщо ви хочете вставити рядки конфігурації, мабуть, краще ввести деякі об’єкти конфігурації (загалом, рекомендується загортати прості типи в значущі об'єкти: int temperatureInCelsiusDegrees -> температура CelciusDeegree)

І не використовуйте службовий локатор як альтернативу введення залежності, це анти-шаблон, більше інформації: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx


Усі моменти стосуються використання іншої форми ін'єкції, а не взагалі. Хоча +1 для посилання.
Ян Худек

3
Службовий локатор НЕ є анти-шаблоном, і хлопець, з яким ви пов’язали свій блог, не є знавцем моделей. Те, що він отримав знаменитий StackExchange, є своєрідним засобом для дизайну програмного забезпечення. Мартін Фаулер (автор основних зразків архітектури прикладних програм підприємства) має більш розумний погляд на це питання: martinfowler.com/articles/injection.html
Колін

1
@Colin, навпаки, Service Locator - це, безумовно, анти-модель, і ось у двох словах, це причина .
Kyralessa

@Kyralessa - ти розумієш, що рамки DI внутрішньо використовують шаблон локатора послуг для пошуку служб, правда? Існують обставини, коли обидва доречні, і жоден з них не є антитілом, незважаючи на ненависть StackExchange, яку деякі хочуть скинути.
Колін

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

8

Коли ви не витримуєте нічого, щоб зробити свій проект сталою та перевіреною.

Серйозно кажучи, я люблю IoC та DI в цілому, і я б сказав, що 98% часу я буду використовувати цю схему безвідмовно. Це особливо важливо в середовищі для багатьох користувачів, де ви можете знову і знову використовувати код різних членів команди та різних проектів, оскільки це відділяє логіку від реалізації. Ведення журналу - це яскравий приклад цього, інтерфейс ILog, введений у клас, у тисячу разів більш ретельний, ніж просто підключення до вашої реєстраційної системи du-jour, оскільки у вас немає гарантії, що інший проект буде використовувати той самий фреймворк журналу (якщо він використовує один взагалі!).

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

Загалом, розумно застосовувати прагматичний підхід до будь-якої методології чи структури, оскільки нічого не застосовується у 100% часу.

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


Я не розумію, що ви маєте на увазі під час реєстрації. Звичайно, всі реєстратори не збираються відповідати одному інтерфейсу, тому вам доведеться написати власну обгортку для перекладу між цим проектом способом ведення журналу та певним реєстратором (якщо припустити, що ви хочете легко змінювати реєстратори). Отже, після чого, що вам дає DI?
Річард Тінгл

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

1

Хоча інші відповіді зосереджуються на технічних аспектах, я хотів би додати практичний вимір.

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

  1. Має бути причина для його запровадження.

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

  2. Команду потрібно пройти навчання та на борту.

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

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

  3. Вам потрібно протестувати

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


1

Це не повна відповідь, а лише черговий момент.

Коли у вас є програма, яка запускається один раз, працює тривалий час (наприклад, веб-додаток) DI може бути корисним.

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


Я не бачу, як живий час подання заявки стосувався DI
Майкл Фрейджім

@MichaelFreidgeim Потрібен час, щоб ініціалізувати контекст, зазвичай контейнери DI досить важкі, наприклад, Spring. Зробіть привіт світовий додаток лише з одного класу і зробіть один з Spring та почніть обидва 10 разів, і ви побачите, що я маю на увазі.
Корай Тугай

Це звучить як проблема з індивідуальним контейнером DI. У світі .Net я не чув про час ініціалізації як суттєвої проблеми для контейнерів DI
Michael Freidgeim

1

Спробуйте використовувати основні принципи OOP: використовуйте успадкування для отримання загальної функціональності, інкапсуляції (приховування) речей, які слід захищати від зовнішнього світу, використовуючи приватні / внутрішні / захищені члени / типи. Використовуйте будь-яку потужну тестову рамку для введення коду лише для тестів, наприклад https://www.typemock.com/ або https://www.telerik.com/products/mocking.aspx .

Потім спробуйте переписати його за допомогою DI і порівняйте код, що ви зазвичай бачите з DI:

  1. У вас більше інтерфейсів (більше типів)
  2. Ви створили дублікати підписів публічних методів, і вам доведеться подвоїти його вдвічі (ви не можете просто змінити якийсь параметр один раз, вам доведеться це зробити двічі; в основному всі рефакторинг і навігаційні можливості ускладнюються)
  3. Ви перемістили деякі помилки компіляції, щоб запустити збої в часі (за допомогою DI ви можете просто ігнорувати деяку залежність під час кодування, і не впевнений, що вона буде виявлена ​​під час тестування)
  4. Ви відкрили інкапсуляцію. Тепер захищені члени, внутрішні типи тощо стали загальнодоступними
  5. Загальна кількість коду збільшена

Я б сказав, що з того, що я бачив, майже завжди якість коду знижується разом із DI.

Однак якщо ви використовуєте лише "загальнодоступний" модифікатор доступу в декларації класу та / або публічні / приватні модифікатори для членів, та / або у вас немає вибору купувати дорогі інструменти тестування, і тоді ж вам потрібно тестування одиниць, які можуть " не замінюючи інтеграційне тестування, та / або у вас вже є інтерфейси для класів, які ви думаєте вводити, DI - хороший вибір!

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


0

Альтернатива Dependency Injection використовує Service Locator . Локатор послуг легше зрозуміти, налагодити і спрощує побудову об'єкта, особливо якщо ви не використовуєте рамки DI. Локатори послуг - це хороший зразок для управління зовнішніми статичними залежностями , наприклад, базами даних, які в іншому випадку вам доведеться передати кожному об'єкту на рівні доступу до даних.

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

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

TLDR : Якщо ваш клас має статичні залежності або ви переробляєте застарілий код, Локатор послуг, напевно, кращий, ніж DI.


12
Локатор послуг приховує залежності у вашому коді . Це не вдалий зразок для використання.
Kyralessa

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

@Kyralessa Я погоджуюся, що у Локатора послуг є багато недоліків, і DI майже завжди є кращим. Однак, я вважаю, є кілька винятків із правила, як і у всіх принципах програмування.
Гарретт Холл

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

Цитуючи Мартіна Фаулера, "Вибір між (DI та службою пошуку) є менш важливим, ніж принцип відокремлення конфігурації від використання". Ідея про те, що DI ЗАВЖДИ краще, - це хогва. Якщо послуга використовується у всьому світі, то використовувати ДІ більш громіздко і крихко. Крім того, DI є менш сприятливим для архітектур плагінів, де реалізації не відомі до часу виконання. Подумайте, як незручно було б завжди передавати додаткові аргументи Debug.WriteLine () тощо?
Колін
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.