Яка різниця між використанням ін'єкції залежності з контейнером та використанням сервісного локатора?


107

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

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

Яка різниця між ними? Чому я б обрав одне над іншим?


3
Це було запропоновано (і відповів) на StackOverflow: stackoverflow.com/questions/8900710 / ...
Kyralessa

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


1
Я здивований, що ніхто не згадував, що за допомогою Локаторів послуг важче зрозуміти, коли може бути знищений перехідний ресурс. Я завжди думав, що це головна причина.
Бух Бух

Відповіді:


126

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

Контейнер, який постачає залежності через аргументи конструктора, дає найбільшу чіткість. Ми бачимо прямо спереду, що об’єкт потребує і а AccountRepository, і а PasswordStrengthEvaluator. Під час використання локатора послуг ця інформація стає менш зрозумілою. Ви відразу побачите випадок, коли об'єкт має, о, 17 залежностей, і скажете собі: "Гм, це здається багато. Що там відбувається?" Виклики до локатора служб можуть поширюватися навколо різних методів і ховатися за умовною логікою, і ви, можливо, не усвідомлюєте, що створили "клас Бога" - такий, який робить усе. Можливо, цей клас може бути перероблений на 3 менші класи, які є більш сфокусованими, а отже, і більш перевіреними.

Тепер розглянемо тестування. Якщо об’єкт використовує сервіс-локатор, щоб отримати свої залежності, вашому тестовому каркасу також знадобиться сервіс-локатор. У тесті ви налаштуєте сервіс-локатор для подачі залежностей до тестуваного об'єкта - можливо, a FakeAccountRepositoryі a VeryForgivingPasswordStrengthEvaluator, а потім запустіть тест. Але це більше роботи, ніж визначення залежностей у конструкторі об'єкта. І ваша тестова структура також стає залежною від сервера-локатора. Ще одна річ, яку ви повинні налаштувати в кожному тесті, що робить тести для письма менш привабливими.

Подивіться "Локатор Serivce - це антивізор" для статті Марка Семана про це. Якщо ви перебуваєте у світі .Net, знайдіть його книгу. Це дуже добре.


1
Читаючи питання, я подумав про Адаптивний код за допомогою C # від Gary McLean Hall, який досить добре відповідає цій відповіді. Він має кілька хороших аналогій, наприклад, що локатор обслуговування є ключем до сейфу; при передачі класу він може створювати залежності за бажанням, які можна виявити непросто.
Денніс

10
Одне, що потрібно додати IMO до constructor supplied dependenciesvs, service locator- це те, що перший може бути підтверджений під час компіляції, тоді як останній може бути перевірений лише під час виконання.
andras

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

1
Щодо тестування - But that's more work than specifying dependencies in the object's constructor.я хотів би заперечити. За допомогою сервісного локатора вам потрібно лише вказати 3 залежності, які вам справді потрібні для вашого тесту. Для DI на основі конструктора вам потрібно вказати ВСІ 10 з них, навіть якщо 7 не використовуються.
Vilx-

4
@ Vilx. Якщо у вас є кілька невикористаних параметрів конструктора, ймовірно, ваш клас порушує принцип єдиної відповідальності.
РБ.

79

Уявіть, ви працівник на фабриці, яка виготовляє взуття .

Ви відповідальні за збір взуття, і для цього вам знадобиться багато речей.

  • Шкіра
  • Мірна стрічка
  • Клей
  • Цвяхи
  • Молоток
  • Ножиці
  • Шнурочки для взуття

І так далі.

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

Service Locator подібний Формана , який може допомогти вам отримати те , що вам потрібно.

Ви запитуєте Службовий локатор кожен раз, коли вам щось потрібно, і вони відходять, щоб знайти його для вас. Локатор послуг був заздалегідь розказаний про те, що ви, можливо, запитаєте і як його знайти.

Ви краще сподіваєтесь, що ви не запитаєте чогось несподіваного. Якщо Локатор не був поінформований заздалегідь про певний інструмент чи матеріал, він не зможе отримати його за вас, і вони просто знизуть на вас.

локатор обслуговування

Dependency Injection (DI) Контейнер як великий ящик , який наповнюється все , що кожен потребує початку дня.

Коли завод починає працювати, Великий Бос, відомий як Корінь Композиції, хапає контейнер і передає все керівникам ліній .

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

Цей процес триває, і залежності залежать від лінії виробництва. Врешті-решт контейнер з матеріалами та інструментами з’явиться для вашої бригадири.

Тепер ваш бригадир поширює саме те, що потрібно вам та іншим працівникам, навіть не вимагаючи їх.

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

контейнер для ін'єкцій залежності


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

21
"Ви краще сподіваєтесь, що ви не запитаєте про щось несподіване. Якщо Локатор не був поінформований заздалегідь про певний інструмент або матеріал", але це також може бути правдою для контейнера DI.
Кеннет К.

5
@FrankHopkins Якщо ви пропустите реєстрацію інтерфейсу / класу у вашому контейнері DI, то ваш код все одно буде компілюватися, і він негайно вийде з ладу під час виконання.
Кеннет К.

3
Обидва мають однакову ймовірність виходу з ладу, залежно від способу їх налаштування. Але з контейнером DI, ви, швидше за все, стикаєтеся з проблемою раніше, коли клас X будується, а не пізніше, коли клас X насправді потребує цієї залежності. Це, мабуть, краще, тому що простіше натрапити на проблему, знайти її і вирішити. Я погоджуюся з Кеннетом, що відповідь дозволяє припустити, що це питання існує лише для сервісних локаторів, що, безумовно, не так.
GolezTrol

3
Чудова відповідь, але я думаю, що це пропускає ще одне; тобто, якщо ви запитаєте бригадира на будь-якій стадії для слона, і він дійсно знає , як отримати один, він отримає один для вас немає питань asked- , навіть якщо вам не потрібно. А хтось, хто намагається налагодити проблему на підлозі (розробник, якщо ви хочете), буде цікаво, що wtf - це слон, який робить тут на цьому столі, тим самим ускладнюючи їх міркувати про те, що насправді пішло не так. В той час, як із DI, ви, клас робітників, заздалегідь заявляйте про кожну залежність, яка вам потрібна, перш ніж ви навіть розпочинаєте роботу, тим самим полегшуючи міркування.
Стівен Берн

10

Кілька додаткових балів, які я знайшов під час пошуку в Інтернеті:

  • Введення залежностей у конструктор полегшує розуміння того, що потрібно класу. Сучасні IDE підкажуть, які аргументи приймає конструктор та їх типи. Якщо ви використовуєте сервіс-локатор, вам доведеться прочитати клас, перш ніж дізнатися, які залежності потрібні.
  • Здається, введення залежності залежить більше від локаторів обслуговування. Призначаючи, що залежність має бути конкретного типу, ви "повідомляєте", які залежності потрібні. Неможливо ідентифікувати клас без проходження необхідних залежностей. За допомогою локатора сервісу ви "запитаєте" про послугу, і якщо локатор служби не налаштований правильно, ви можете не отримати необхідного.

4

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

Яка різниця між використанням ін'єкції залежності з контейнером та використанням сервісного локатора?

Іноді взагалі немає. Що має значення - це те, що відомо про що.

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

Чи означає це, що якщо ви хочете уникати локатора обслуговування, ви не можете використовувати контейнер? Ні. Вам просто потрібно, щоб клієнти не знали про контейнер. Ключова різниця полягає в тому, де ви використовуєте контейнер.

Скажімо, Clientпотреби Dependency. У контейнері є Dependency.

class Client { 
    Client() { 
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        this.dependency = (Dependency) beanfactory.getBean("dependency");        
    }
    Dependency dependency;
}

Ми щойно дотримувались схеми пошуку локатора служб, оскільки Clientзнаємо, як її знайти Dependency. Звичайно, він використовує жорсткий код, ClassPathXmlApplicationContextале навіть якщо ви ввели, що у вас все ще є сервіс-локатор, тому що Clientдзвонить beanfactory.getBean().

Щоб уникнути пошуку локатора, вам не доведеться відмовлятися від цього контейнера. Ви просто повинні перемістити його, Clientщоб Clientне знати про це.

class EntryPoint { 
    public static void main(String[] args) {
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        Client client = (Client) beanfactory.getBean("client");

        client.start();
    }
}

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dependency" class="Dependency">
    </bean>

    <bean id="client" class="Client">
        <constructor-arg value="dependency" />        
    </bean>
</beans>

Зверніть увагу, як Clientзараз поняття не має, контейнер існує:

class Client { 
    Client(Dependency dependency) { 

        this.dependency = dependency;        
    }
    Dependency dependency;
}

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

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

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


1

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

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

Давайте розглянемо наступний клас:

public class MySpecialStringWriter
{
  private readonly IOutputProvider outputProvider;
  public MySpecialFormatter(IOutputProvider outputProvider)
  {
    this.outputProvider = outputProvider;
  }

  public void OutputString(string source)
  {
    this.outputProvider.Output("This is the string that was passed: " + source);
  }
}

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

Погляньте на той самий клас із локатором обслуговування:

public class MySpecialStringWriter
{
  private readonly ServiceLocator serviceLocator;
  public MySpecialFormatter(ServiceLocator serviceLocator)
  {
    this.serviceLocator = serviceLocator;
  }

  public void OutputString(string source)
  {
    this.serviceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

Тепер я додав локатор служби як залежність. Ось проблеми, які відразу очевидні:

  • Найперша проблема з цим полягає в тому, що для досягнення того ж результату потрібно більше коду . Більше коду - це погано. Це не набагато більше коду, але все-таки більше.
  • Друга проблема полягає в тому, що моя залежність вже не є явною . Мені все-таки потрібно щось вписати в клас. За винятком того, що я хочу, не є явним. Це приховано у властивості речі, про яку я попросив. Тепер мені потрібен доступ і до ServiceLocator, і до IOutputProvider, якщо я хочу перенести клас на іншу збірку.
  • Третя проблема полягає в тому, що додаткову залежність може взяти інший розробник, який навіть не усвідомлює, що приймає її, коли додає код до класу.
  • Нарешті, цей код важче перевірити (навіть якщо ServiceLocator є інтерфейсом), оскільки ми маємо знущатися з ServiceLocator та IOutputProvider замість просто IOutputProvider

То чому б нам не зробити локатор сервісу статичним класом? Давайте подивимось:

public class MySpecialStringWriter
{
  public void OutputString(string source)
  {
    ServiceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

Це набагато простіше, правда?

Неправильно.

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

Спробуємо протестувати цей клас. Для тесту нам потрібна інша реалізація IOutputProvider. Як ми пишемо тест?

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

Отже, вам залишається або а) тест, який викликає нав'язливі зміни коду у непов'язаному класі ServiceLocator; або б) взагалі немає тесту. І вам також залишається менш гнучке рішення.

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

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


Локатори обслуговування (SL) та впорскування в залежності (DI) вирішують одне і те ж питання подібними способами. Єдина відмінність, якщо ви "говорите" DI чи "просите" SL про залежності.
Метью Віт

@MatthewWhited Основна відмінність - кількість неявних залежностей, які ви приймаєте за допомогою сервера-локатора. І це має велике значення для довгострокової ремонтопридатності та стабільності коду.
Стівен

Це насправді немає. У вас в будь-якому випадку однакова кількість залежностей.
Метью Уайт

Спочатку так, але ви можете приймати залежності за допомогою сервера-локатора, не розуміючи, що ви приймаєте залежності. Що перемагає значну частину причини, чому ми в першу чергу робимо інверсію залежності. Один клас залежить від властивостей A і B, інший залежить від B і C. Раптом Локатор послуг стає класом богів. Контейнер DI не має, і два класи належним чином залежать лише від A і B і B і C відповідно. Це робить їх рефакторинг у сто разів простішим. Локатори обслуговування підступні тим, що вони виглядають так само, як і DI, але вони не є.
Стівен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.