Вприскування залежностей: впорскування поля проти введення конструктора?


61

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

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

Однак я нещодавно вступив у дискусію з автором JMockit - глузливої ​​рамки, - в якій він вважає введення конструктора та сетера як погану практику і вказує, що громада JEE погоджується з ним.

Чи є в сучасному світі бажаний спосіб робити ін’єкції? Чи є кращим введення поля?

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



2
Якщо вам заборонено використовувати інжектор конструктора чи інжектор сетерів, то як ви повинні передавати свої залежності класу? Можливо, вам краще посилатись на дискусію, якщо тільки ваша розмова з хлопцем Mockit не була приватною.
Роберт Харві

2
@DavidArno: У незмінному класі стан встановлюється один раз ... у конструкторі.
Роберт Харві

1
@Davkd, що ви описуєте, це анемічна модель домену. У нього, безумовно, є прихильники, але насправді вже не орієнтовано на об'єкти; де дані та функції, що діють на ці дані, живуть разом.
Річард Тінгл

3
@David У функціональному програмуванні немає нічого поганого. Але принципово java розроблена так, щоб орієнтуватися на об'єкти, і ви постійно будете боротися з цим, і в кінцевому підсумку будете з анемічною моделлю домену, якщо ви не дуже обережні. З кодом, орієнтованим на функції, немає нічого поганого, але для цього слід використовувати відповідний інструмент, який не є java (навіть якщо в лямбда додані елементи програмування на основі функцій)
Richard Tingle

Відповіді:


48

Польова ін'єкція - це занадто "моторошна дія на відстані" на мій смак.

Розглянемо приклад, який ви вказали у своїй публікації в групах Google:

public class VeracodeServiceImplTest {


    @Tested(fullyInitialized=true)
    VeracodeServiceImpl veracodeService;
    @Tested(fullyInitialized=true, availableDuringSetup=true)
    VeracodeRepositoryImpl veracodeRepository;
    @Injectable private ResultsAPIWrapper resultsApiWrapper;
    @Injectable private AdminAPIWrapper adminApiWrapper;
    @Injectable private UploadAPIWrapper uploadApiWrapper;
    @Injectable private MitigationAPIWrapper mitigationApiWrapper;

    static { VeracodeRepositoryImpl.class.getName(); }
...
}

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

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

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

Розглянемо альтернативу (це буде C #, тому що я знаю це краще, але, можливо, є точний еквівалент у Java):

public class VeracodeService
{        
    private readonly IResultsAPIWrapper _resultsApiWrapper;
    private readonly IAdminAPIWrapper _adminApiWrapper;
    private readonly IUploadAPIWrapper _uploadApiWrapper;
    private readonly IMitigationAPIWrapper _mitigationApiWrapper;

    // Constructor
    public VeracodeService(IResultsAPIWrapper resultsApiWrapper, IAdminAPIWrapper adminApiWrapper, IUploadAPIWrapper uploadApiWrapper,         IMitigationAPIWrapper mitigationApiWrapper)
    {
         _resultsAPIWrapper = resultsAPIWrapper;
         _adminAPIWrapper = adminAPIWrapper;
         _uploadAPIWrapper = uploadAPIWrapper;
         _mitigationAPIWrapper = mitigationAPIWrapper;
    }
}

Я вже знаю деякі речі про цей клас. Це непорушний клас; стан може бути встановлений лише в конструкторі (посилання в цьому конкретному випадку). А оскільки все походить від інтерфейсу, я можу поміняти місцями реалізації в конструкторі, саме там надходять ваші макети.

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

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


Якщо я неправильно прочитав вашу публікацію, ви насправді не розглядаєте приклад належним чином. Моя реалізація VeracodeServiceмайже ідентична тій, яку ви написали (хоча і в Java проти C #). VeracodeServiceImplTestФактично клас Unit Test. У @Injectableполях, по суті , знущалися об'єкти, вставленим в контекст. @TestedПоле об'єкт / клас з DI конструктор , певний. Я погоджуюся з вашою точкою зору, що надає перевагу впорскуванню конструктора над полем впорскування. Однак, як я вже згадував, автор JMockit відчуває навпаки, і я намагаюся зрозуміти, чому
Ерік Б.

Якщо ви подивитесь на першу публікацію в цій дискусії, ви побачите, що в моєму класі Repo у мене майже такий самий defn.
Ерік Б.

Якщо ви говорите лише про тестові заняття, це може не мати значення. Тому що, тестові заняття.
Роберт Харві

Ні - я не кажу про тестові заняття. Я говорю про виробничі класи. Просто буває так, що приклад у дискусійній групі є одиничним тестом, оскільки фреймворк - це глузуючий / тестуючий фреймворк. (Вся дискусія обертається навколо, якщо глузливий фреймворк повинен підтримувати конструкторські введення)
Ерік Б.

3
+1: Так, ніякої магії у версії C #. Це стандартний клас, він має залежності, всі вони вводяться в один момент, перш ніж клас буде використаний. Клас можна використовувати з контейнером DI, чи ні. Ніщо в класі не говорить про це МОК або "Автоматична залежність введення".
Бінарний страшник

27

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

Чи хочу я, щоб мій клас був миттєвим лише за допомогою роздумів?

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

Друге питання - це продуктивність . Виклик конструктора (прямого або за допомогою відображення) завжди швидше, ніж купа завдань поля відображення. Рамки введення залежностей повинні використовувати аналіз відображення для побудови дерева залежностей та створення конструкторів відображення. Цей процес спричиняє додаткові досягнення ефективності.

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

Це все викликає багато нових питань:

  1. Яка ймовірність використання частин коду на іншій платформі?
  2. Скільки одиниць тестів буде написано?
  3. Наскільки швидко мені потрібно підготувати кожен новий випуск?
  4. ...

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

Багато, багато аргументів проти введення поля.


3
+1 Ти вдарився нервом. Мені не подобається, що мені потрібна рефлексія чи рамки DI / IoC для інстанціювання класу. Це як їхати до Риму, щоб дістатися до Парижа з Амстердаму. Зверніть увагу, я люблю DI. Просто не впевнений в рамках.
Мар'ян Венема

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

19

Польова ін'єкція отримує від мене певний голос «Ні».

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

Як і Мацей Чалапук, мені не подобається потребувати роздумів чи рамки DI / IoC для інстанціювання класу. Це як їхати до Риму, щоб дістатися до Парижа з Амстердаму.

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

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

І це робить мої тести залежними від глобального стану рамок. Тобто, я більше не можу паралельно виконувати два тести, які вимагають встановити екземпляр класу X з різними залежностями.

Принаймні, з використанням інжектора конструктора та сетера у вас є можливість не використовувати рамки та / або відображення у своїх тестах.


Якщо ви використовуєте, наприклад, Mockito mockito.org , вам не потрібна рамка DI / IoC навіть при використанні польового введення, і ви можете проводити тест паралельно тощо.
Ганс-Пітер Штерр

1
@hstoerr Ніцца. Однак це повинно означати, що Mockito використовує рефлексію (або все, що називається Java еквівалентом того, що називається) ...
Marjan Venema

5

Ну, це здається полемічною дискусією. Перше, на що потрібно звернути увагу, - це те, що ін'єкція поля відрізняється від ін'єкції Сетера . Я вже працюю з деякими людьми, які вважають, що ін'єкція Field and Setter - це одне і те ж.

Отже, щоб було зрозуміло:

Полеве впорскування:

@Autowired
private SomeService someService;

Введення сетера:

@Autowired
public void setSomeService(SomeService someService) {
    this.someService = someService;
}

Але, для мене, вони поділяють ті самі проблеми. І, мій улюблений, конструктор:

@AutoWired
public MyService(SomeService someService) {
    this.someService = someService;
}

У мене є наступні причини вважати, що введення конструктора краще, ніж введення сетера / поля (я наведу кілька весняних посилань, щоб продемонструвати свою точку зору):

  1. Попередити кругові залежності : Весна здатна виявити кругові залежності між бобами, як пояснив @Tomasz. Дивіться також команду весни, яка сказала це.
  2. Залежності класу очевидні : залежності класу очевидні в конструкторі, але ховаються при введенні поля / сетера. Я відчуваю свої заняття, як "чорний ящик" з введенням поля / сетера. І існує (на мій досвід) тенденція, щоб розробники не турбувались про клас із багатьма залежностями, що очевидно, коли ви намагаєтеся знущатися над класом за допомогою інжектора конструктора (див. Наступний пункт). Проблема, яка турбує розробників, швидше за все, буде вирішена. Також клас із занадто великою кількістю залежностей, ймовірно, порушує принцип єдиної відповідальності (SRP).
  3. Легкий і надійний для глузування : порівняно з Field injection , простіше знущатися в одиничному тесті, тому що вам не потрібен якийсь контекстний макет або техніка рефлексії, щоб дістатися до заявленого приватного Bean всередині класу, і ви його не обдурите . Ви просто започатковуєте клас та передаєте знущаються боби. Простий для розуміння та легкий.
  4. Інструменти якості коду можуть вам допомогти : такі інструменти, як Sonar, можуть попередити вас про надто складний клас, тому що клас з великою кількістю параметрів, ймовірно, занадто складний клас і потребує деякого рефактора. Цей інструмент не може визначити ту саму проблему, коли клас використовує введення Field / Setter.
  5. Кращий і незалежний код : При меншому @AutoWiredпоширенні коду ваш код менше залежить від фреймворку. Я знаю, що можливість простої зміни рамки DI в проекті не виправдає цього (хто це робить, правда?). Але це показує для мене кращий код (я це навчився важко за допомогою EJB і пошуку). А за допомогою Spring ви можете навіть видалити @AutoWiredанотацію за допомогою конструкторської інжекції.
  6. Більше анотацій - це не завжди правильна відповідь : З якихось причин я дійсно намагаюся використовувати примітки лише тоді, коли вони справді корисні.
  7. Ви можете зробити ін’єкції непорушними . Незмінність - дуже вітальна особливість.

Якщо ви шукаєте додаткову інформацію, я рекомендую цю стару (але все ще актуальну) статтю з Spring Blog, в якій розповідаєте, чому вони використовують стільки ін'єкцій сетера та рекомендацію щодо використання конструкторської інжекції:

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

І ця примітка про те, чому вони вважають, що конструктор більше підходить для коду програми:

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

Заключні думки

Якщо ви не дуже впевнені в перевазі конструктора, можливо, ідея змішати сеттер (не поле) з конструктором введення конструктора - це варіант, як пояснила команда Spring :

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

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


-5

Можливо, ваша залежність зміниться протягом життя класу? У цьому випадку використовуйте ін'єкцію поля / власності. В іншому випадку використовуйте конструктор впорскування.


2
Це насправді нічого не пояснює. Навіщо вводити приватні поля, коли ви можете зробити це належним чином?
Роберт Харві

Де йдеться про приватні поля? Я говорю про публічний інтерфейс класу /
Даррен Янг

У цьому питанні немає жодного аргументу щодо методу ін'єкції проти інжектора конструктора. Автор JMockit сприймає обоє як погані. Подивіться назву питання.
Роберт Харві

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

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