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


107

Я понимаю, что непосредственное создание зависимостей внутри класса считается плохой практикой. Это имеет смысл, поскольку это тесно связывает все, что в свою очередь делает тестирование очень трудным.

Похоже, что почти все фреймворки, с которыми я сталкивался, предпочитают внедрение зависимостей с контейнером, а не использование сервисных локаторов. Кажется, что они оба достигают одного и того же, позволяя программисту указать, какой объект должен быть возвращен, когда классу требуется зависимость.

В чем разница между двумя? Почему я бы выбрал одно над другим?


3
Об этом спрашивали (и отвечали) в StackOverflow: stackoverflow.com/questions/8900710/…
Kyralessa

2
По моему мнению, разработчики нередко обманывают других, думая, что их сервисный локатор - это внедрение зависимостей. Они делают это, потому что внедрение зависимости часто считается более продвинутым.
Герман


1
Я удивлен, что никто не упомянул, что с локаторами служб сложнее узнать, когда временный ресурс может быть уничтожен. Я всегда думал, что это было основной причиной.
Бух Бух

Ответы:


126

Когда сам объект отвечает за запрос своих зависимостей, а не принимает их через конструктор, он скрывает некоторую важную информацию. Это только немного лучше, чем очень тесно связанный случай использования newдля создания своих зависимостей. Это уменьшает связь, потому что вы можете фактически изменить зависимости, которые он получает, но у него все еще есть зависимость, которую он не может поколебать: локатор службы. Это становится вещью, от которой все зависит.

Контейнер, который предоставляет зависимости через аргументы конструктора, дает наибольшую ясность. Мы сразу видим, что объект нуждается как в AccountRepository, так и в PasswordStrengthEvaluator. При использовании сервисного локатора эта информация менее очевидна. Вы бы сразу увидели случай, когда объект имеет, о-о, 17 зависимостей, и сказали бы себе: «Хм, это похоже на многое. Что там происходит?» Обращения к локатору службы могут распространяться вокруг различных методов и скрываться за условной логикой, и вы можете не осознавать, что создали «класс Бога» - тот, который делает все. Возможно, этот класс можно было бы реорганизовать в 3 небольших класса, которые были бы более сфокусированными и, следовательно, более тестируемыми.

Теперь рассмотрим тестирование. Если объект использует локатор службы для получения своих зависимостей, вашей тестовой среде также понадобится локатор службы. В тесте вы сконфигурируете локатор службы для предоставления зависимостей тестируемому объекту - возможно, a FakeAccountRepositoryи a VeryForgivingPasswordStrengthEvaluator, а затем запустите тест. Но это больше работы, чем указание зависимостей в конструкторе объекта. И ваш тестовый фреймворк также становится зависимым от локатора сервиса. Это еще одна вещь, которую вы должны настроить в каждом тесте, что делает написание тестов менее привлекательным.

Посмотрите на статью "Serivce Locator - Anti-Pattern" для статьи Марка Симана об этом. Если ты в мире .Net, возьми его книгу. Это очень хорошо.


1
Читая вопрос, который я подумал об адаптивном коде через C # , Гэри Маклин Холл, который очень хорошо согласуется с этим ответом. У него есть несколько хороших аналогий, таких как указатель службы, являющийся ключом к сейфу; при передаче классу он может создавать зависимости по желанию, которые не всегда легко обнаружить.
Деннис

10
Одна вещь, которую IMO нужно добавить к constructor supplied dependenciesvs, service locatorэто то, что первая может быть проверена во время компиляции, а вторая может быть проверена только во время выполнения.
Андрас

2
Кроме того, локатор службы не препятствует тому, чтобы у компонента было много много зависимостей. Если вам нужно добавить 10 параметров в ваш конструктор, у вас будет хорошее чувство, что с вашим кодом что-то не так. Однако, если вам просто выпадет 10 статических вызовов к локатору службы, может быть не совсем очевидно, что у вас возникла какая-то проблема. Я работал над большим проектом с поиском сервисов, и это было самой большой проблемой. Просто было слишком легко замкнуть новый путь, а не сидеть сложа руки, размышлять, перепроектировать и реорганизовывать.
Pace

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
Отличный ответ, но я думаю, что он упускает еще одну вещь; то есть, если вы попросите у мастера на любом этапе слона, и он действительно знает, как его получить, он получит его для вас без вопросов - даже если вам это не нужно. И кому-то, пытающемуся отладить проблему на полу (разработчик, если хотите), будет удивляться, что 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;
}

Переместите контейнер из всех клиентов и вставьте его в main, где он может построить граф объектов из всех ваших долгоживущих объектов. Выберите один из этих объектов для извлечения и вызовите метод для него, и вы начнете тикать весь график.

Это перемещает всю статическую конструкцию в контейнеры 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) и Dependency Injection (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.