Является ли ServiceLocator антишаблоном?
Недавно я прочитал статью Марка Симанна об анти-паттерне Service Locator.
Автор указывает на две основные причины, почему ServiceLocator является анти-паттерном:
Проблема использования API (с которой у меня все в порядке)
Когда в классе используется локатор Service, очень трудно увидеть его зависимости, так как в большинстве случаев у класса есть только один конструктор PARAMETERLESS. В отличие от ServiceLocator, подход DI явно предоставляет зависимости через параметры конструктора, поэтому зависимости легко увидеть в IntelliSense.Вопрос обслуживания (который озадачивает меня)
Рассмотрим следующий пример
У нас есть класс MyType, который использует подход локатора службы:
public class MyType
{
public void MyMethod()
{
var dep1 = Locator.Resolve<IDep1>();
dep1.DoSomething();
}
}
Теперь мы хотим добавить еще одну зависимость в класс 'MyType'
public class MyType
{
public void MyMethod()
{
var dep1 = Locator.Resolve<IDep1>();
dep1.DoSomething();
// new dependency
var dep2 = Locator.Resolve<IDep2>();
dep2.DoSomething();
}
}
И здесь начинается мое недоразумение. Автор говорит:
Становится намного сложнее сказать, вводите ли вы критические изменения или нет. Вы должны понимать все приложение, в котором используется локатор служб, и компилятор вам не поможет.
Но подождите секунду, если бы мы использовали подход DI, мы бы ввели зависимость с другим параметром в конструктор (в случае внедрения конструктора). И проблема все еще будет там. Если мы можем забыть настроить ServiceLocator, тогда мы можем забыть добавить новое отображение в наш контейнер IoC, и подход DI будет иметь ту же проблему во время выполнения.
Также автор упомянул о сложностях модульного тестирования. Но разве у нас не будет проблем с подходом DI? Не нужно ли нам обновить все тесты, которые создавали этот класс? Мы обновим их так, чтобы они передавали новую ложную зависимость, чтобы сделать наш тест компилируемым. И я не вижу никакой пользы от этого обновления и затрат времени.
Я не пытаюсь защищать подход Service Locator. Но это недоразумение заставляет меня думать, что я теряю что-то очень важное. Может ли кто-нибудь развеять мои сомнения?
ОБНОВЛЕНИЕ (РЕЗЮМЕ):
Ответ на мой вопрос "Является ли Service Locator антишаблоном" действительно зависит от обстоятельств. И я определенно не предложил бы вычеркнуть это из вашего списка инструментов. Это может стать очень удобным, когда вы начнете работать с устаревшим кодом. Если вам повезло оказаться в самом начале вашего проекта, то подход DI может быть лучшим выбором, поскольку он имеет некоторые преимущества по сравнению с Service Locator.
И вот основные отличия, которые убедили меня не использовать Service Locator для моих новых проектов:
- Самое очевидное и важное: Service Locator скрывает зависимости классов
- Если вы используете какой-либо контейнер IoC, он, вероятно, будет сканировать весь конструктор при запуске, чтобы проверить все зависимости и дать вам немедленную обратную связь по отсутствующим сопоставлениям (или неправильной конфигурации); это невозможно, если вы используете свой контейнер IoC в качестве локатора службы
Для подробностей читайте отличные ответы, которые приведены ниже.
10 ответов
Если вы определяете шаблоны как анти-шаблоны просто потому, что в некоторых ситуациях они не подходят, тогда ДА - это анти-шаблон. Но с этим рассуждением все паттерны также будут анти-паттернами.
Вместо этого мы должны посмотреть, действительно ли используются шаблоны, и для Service Locator есть несколько вариантов использования. Но давайте начнем с примеров, которые вы привели.
public class MyType
{
public void MyMethod()
{
var dep1 = Locator.Resolve<IDep1>();
dep1.DoSomething();
// new dependency
var dep2 = Locator.Resolve<IDep2>();
dep2.DoSomething();
}
}
Кошмар обслуживания с этим классом заключается в том, что зависимости скрыты. Если вы создаете и используете этот класс:
var myType = new MyType();
myType.MyMethod();
Вы не понимаете, что у него есть зависимости, если они скрыты, используя расположение службы. Теперь, если мы вместо этого используем внедрение зависимости:
public class MyType
{
public MyType(IDep1 dep1, IDep2 dep2)
{
}
public void MyMethod()
{
dep1.DoSomething();
// new dependency
dep2.DoSomething();
}
}
Вы можете непосредственно определить зависимости и не можете использовать классы до их удовлетворения.
В типичном бизнес-приложении по этой причине следует избегать использования местоположения службы. Это должен быть шаблон для использования, когда нет других вариантов.
Является ли паттерн анти-паттерном?
Нет.
Например, инверсия контрольных контейнеров не будет работать без сервисного местоположения. Это то, как они разрешают услуги внутри.
Но гораздо лучшим примером являются ASP.NET MVC и WebApi. Как вы думаете, что делает возможным внедрение зависимостей в контроллерах? Правильно - местоположение сервиса.
Ваши вопросы
Но подождите секунду, если бы мы использовали подход DI, мы бы ввели зависимость с другим параметром в конструктор (в случае внедрения конструктора). И проблема все еще будет там.
Есть еще две серьезные проблемы:
- С расположением службы вы также добавляете еще одну зависимость: локатор службы.
- Как вы скажете, какое время жизни должны иметь зависимости, и как / когда они должны быть очищены?
С помощью конструктора с использованием контейнера вы получаете это бесплатно.
Если мы можем забыть настроить ServiceLocator, тогда мы можем забыть добавить новое отображение в наш контейнер IoC, и подход DI будет иметь ту же проблему во время выполнения.
Это правда. Но с внедрением конструктора вам не нужно сканировать весь класс, чтобы выяснить, какие зависимости отсутствуют.
И некоторые лучшие контейнеры также проверяют все зависимости при запуске (сканируя все конструкторы). Таким образом, с этими контейнерами вы получаете ошибку времени выполнения напрямую, а не в какой-то более поздний момент времени.
Также автор упомянул о сложностях модульного тестирования. Но разве у нас не будет проблем с подходом DI?
Нет. Так как у вас нет зависимости от статического сервисного локатора. Вы пытались заставить параллельные тесты работать со статическими зависимостями? Это не весело.
Я также хотел бы отметить, что если вы проводите рефакторинг устаревшего кода, то шаблон Service Locator не только не является анти-шаблоном, но и практической необходимостью. Никто никогда не собирается махать волшебной палочкой за миллионы строк кода, и вдруг весь этот код будет готов к DI. Поэтому, если вы хотите начать вводить DI в существующую кодовую базу, часто бывает так, что вы будете медленно менять положение, чтобы стать сервисами DI, и код, который ссылается на эти сервисы, часто НЕ будет DI-сервисами. Следовательно, этим службам нужно будет использовать указатель службы, чтобы получить экземпляры тех служб, которые были преобразованы для использования DI.
Таким образом, при рефакторинге больших унаследованных приложений, чтобы начать использовать концепции DI, я бы сказал, что Service Locator не только НЕ является паттерном, но и является единственным способом постепенного применения концепций DI к базе кода.
С точки зрения тестирования, Service Locator - это плохо. Посмотрите хорошее объяснение Google Tech Talk Миско Хевери с примерами кода http://youtu.be/RlfLCWKxHJ0 начиная с минуты 8:45. Мне понравилась его аналогия: если вам нужны 25 долларов, просите деньги напрямую, а не отдавайте свой кошелек, откуда деньги будут взяты. Он также сравнивает сервисный локатор с стогом сена, в котором есть нужная игла, и знает, как ее извлечь. Классы, использующие Service Locator, трудно использовать из-за этого.
Вопрос обслуживания (который озадачивает меня)
Есть две разные причины, по которым использование сервисного локатора плохо в этом отношении.
- В вашем примере вы жестко кодируете статическую ссылку на локатор службы в своем классе. Это тесно связывает ваш класс непосредственно с локатором службы, что, в свою очередь, означает, что он не будет функционировать без локатора службы. Кроме того, ваши модульные тесты (и любой другой, кто использует класс) также неявно зависят от локатора сервиса. Одна вещь, которая, казалось, осталась незамеченной, заключается в том, что при использовании инжекции конструктора вам не нужен контейнер DI при модульном тестировании, что значительно упрощает ваши модульные тесты (и способность разработчиков понимать их). Это реальное преимущество модульного тестирования, которое вы получаете от использования инжектора конструктора.
- Что касается важности конструктора Intellisense, то здесь люди, похоже, полностью упустили этот момент. Класс пишется один раз, но он может использоваться в нескольких приложениях (то есть в нескольких конфигурациях DI). Со временем это приносит дивиденды, если вы можете взглянуть на определение конструктора, чтобы понять зависимости класса, вместо того, чтобы смотреть на документацию (возможно, актуальную) или, если это не удастся, вернуться к исходному исходному коду (который может не быть полезным), чтобы определить, каковы зависимости класса. Класс с локатором услуг, как правило, легче написать, но вы более чем оплачиваете стоимость этого удобства при текущем обслуживании проекта.
Понятно и просто: класс с указателем службы в нем сложнее использовать повторно, чем класс, который принимает свои зависимости через конструктор.
Рассмотрим случай, когда вам нужно использовать
LibraryA
что его автор решил использоватьServiceLocatorA
и сервис отLibraryB
чей автор решил использоватьServiceLocatorB
, У нас нет другого выбора, кроме как использовать 2 разных сервисных локатора в нашем проекте. Сколько зависимостей нужно настроить, это игра в догадки, если у нас нет хорошей документации, исходного кода или автора по быстрому набору. В противном случае нам может понадобиться декомпилятор, чтобы выяснить, каковы зависимости. Возможно, нам потребуется настроить 2 совершенно разных API-интерфейса поиска служб, и в зависимости от дизайна может оказаться невозможным просто обернуть существующий DI-контейнер. Совсем не возможно разделить один экземпляр зависимости между двумя библиотеками. Сложность проекта может быть еще более усложнена, если локаторы сервисов не будут фактически находиться в тех же библиотеках, что и сервисы, которые нам нужны - мы неявно перетаскиваем дополнительные ссылки на библиотеки в наш проект.Теперь рассмотрим те же два сервиса, сделанные с помощью инжектора конструктора. Добавить ссылку на
LibraryA
, Добавить ссылку наLibraryB
, Укажите зависимости в конфигурации DI (проанализировав, что необходимо с помощью Intellisense). Готово.У Марка Симанна есть ответ Stackru, который наглядно иллюстрирует это преимущество в графической форме, которая применяется не только при использовании локатора служб из другой библиотеки, но и при использовании внешних значений по умолчанию в службах.
Мои знания недостаточно хороши, чтобы судить об этом, но в целом, я думаю, что если что-то находит применение в конкретной ситуации, это не обязательно означает, что оно не может быть анти-паттерном. Особенно, когда вы имеете дело со сторонними библиотеками, у вас нет полного контроля над всеми аспектами, и вы можете в конечном итоге использовать не самое лучшее решение.
Вот параграф адаптивного кода через C#:
"К сожалению, локатор службы иногда является неизбежным антишаблоном. В некоторых типах приложений, особенно в Windows Workflow Foundation, инфраструктура не поддается внедрению в конструктор. В этих случаях единственной альтернативой является использование локатора службы. лучше, чем вообще не вводить зависимости. При всех моих проблемах с (анти-) шаблоном это бесконечно лучше, чем построение зависимостей вручную. В конце концов, он по-прежнему включает все важные точки расширения, предоставляемые интерфейсами, которые позволяют декораторам, адаптерам, и аналогичные преимущества. "
- Холл, Гэри Маклин. Адаптивный код с помощью C#: гибкое кодирование с шаблонами проектирования и принципами SOLID (Справочник разработчика) (стр. 309). Пирсон Образование.
Да, сервисный локатор является анти-паттерном, он нарушает инкапсуляцию и является солидным.
Я могу предложить рассмотреть общий подход, чтобы избежать недостатков шаблона Service Locator. Он позволяет явно объявлять зависимости классов и заменять макеты и не зависит от конкретного контейнера внедрения зависимостей. Возможные недостатки этого подхода:
- Это делает ваши классы управления универсальными.
- Нелегко переопределить какой-то конкретный интерфейс.
1 Интерфейс первого объявления
public interface IResolver<T>
{
T Resolve();
}
- Создайте «плоский» класс с реализацией разрешения наиболее часто используемых интерфейсов из DI Container и зарегистрируйте его. В этом коротком примере используется Service Locator, но до корня композиции. Альтернативный способ - внедрить каждый интерфейс с помощью конструктора.
public class FlattenedServices :
IResolver<I1>,
IResolver<I2>,
IResolver<I3>
{
private readonly DIContainer diContainer;
public FlattenedServices(DIContainer diContainer)
{
this.diContainer = diContainer;
}
I1 IResolver<I1>.Resolve()
=> diContainer.Resolve<I1>();
I2 IResolver<I2>.Resolve()
=> diContainer.Resolve<I2>();
I3 IResolver<I3>.Resolve()
=> diContainer.Resolve<I3>();
}
- Внедрение конструктора в некоторый класс MyType должно выглядеть так
public class MyType<T> : IResolver<T>
where T : class, IResolver<I1>, IResolver<I3>
{
T servicesContext;
public MyType(T servicesContext)
{
this.servicesContext = servicesContext
?? throw new ArgumentNullException(nameof(serviceContext));
_ = (servicesContext as IResolver<I1>).Resolve() ?? throw new ArgumentNullException(nameof(I1));
_ = (servicesContext as IResolver<I3>).Resolve() ?? throw new ArgumentNullException(nameof(I3));
}
public void MyMethod()
{
var dep1 = ((IResolver<I1>)servicesContext).Resolve();
dep1.DoSomething();
var dep3 = ((IResolver<I3>)servicesContext).Resolve();
dep3.DoSomething();
}
T IResolver<T>.Resolve() => serviceContext;
}
PS Если не надо проходить
servicesContext
дальше в
MyType
, вы можете объявить
object servicesContext;
и сделать общий только ctor, а не класс.
PPS Это
FlattenedServices
класс можно рассматривать как основной контейнер DI, а фирменный контейнер можно рассматривать как дополнительный контейнер.
Автор полагает, что "компилятор вам не поможет" - и это правда. Когда вы определяете класс, вам нужно тщательно выбирать его интерфейс - среди других целей, чтобы сделать его настолько независимым, насколько это... имеет смысл.
Когда клиент принимает ссылку на службу (на зависимость) через явный интерфейс, вы
- неявно получить проверку, поэтому "помогает" компилятор.
- Вы также устраняете необходимость того, чтобы клиент знал что-то о "локаторе" или подобных механизмах, чтобы клиент на самом деле был более независимым.
Вы правы, что у DI есть свои проблемы / недостатки, но упомянутые преимущества намного их перевешивают... ИМО. Вы правы, что в DI есть зависимость, введенная в интерфейс (конструктор), но, надеюсь, это та самая зависимость, которая вам нужна и которую вы хотите сделать видимой и проверяемой.
Я думаю, что автор статьи выстрелил себе в ногу, доказывая, что это антипаттерн с обновлением, написанным спустя 5 лет. Там сказано, что это правильный путь:
public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
{
if (validator == null)
throw new ArgumentNullException("validator");
if (shipper == null)
throw new ArgumentNullException("shipper");
this.validator = validator;
this.shipper = shipper;
}
Затем ниже сказано:
Теперь ясно, что все три объекта необходимы, прежде чем вы сможете вызвать метод Process; эта версия класса OrderProcessor объявляет свои предварительные условия через систему типов. Вы даже не можете скомпилировать клиентский код, если не передадите аргументы конструктору и методу (вы можете передать null, но это другое обсуждение).
Позвольте мне еще раз подчеркнуть последнюю часть:
вы можете передать null, но это другое обсуждение
Почему это другое обсуждение? Это огромная сделка. Объект, который получает свои зависимости в качестве аргументов, полностью зависит от предыдущего выполнения приложения (или тестов) для предоставления этих объектов в качестве действительных ссылок/указателей. Он не «инкапсулирован» в терминах, выраженных автором, поскольку он зависит от многих внешних механизмов, чтобы он работал удовлетворительным образом, чтобы объект вообще был сконструирован, а затем работал должным образом, когда ему нужно использовать другой. сорт.
Автор утверждает, что это Service Locator, который не инкапсулирован, потому что он зависит от дополнительного объекта, от которого вы не можете изолировать тесты. Но этот другой объект вполне может быть тривиальной картой или вектором, так что это чистые данные без какого-либо поведения. Например, в C++ контейнеры не являются частью языка, поэтому вы полагаетесь на контейнеры (векторы, хэш-карты, строки и т. д.) для всех нетривиальных классов. Разве они не изолированы, потому что полагаются на контейнеры? Я так не думаю.
Я думаю, что как с помощью ручного внедрения зависимостей, так и с помощью локатора сервисов объекты на самом деле не изолированы от остальных: им нужны свои зависимости, да или да, но они предоставляются по-другому. Я, например, думаю, что локатор даже помогает с принципом DRY, так как он подвержен ошибкам и повторяется при повторной передаче указателей через приложение. Локатор службы также может быть более гибким в том смысле, что объект может извлекать свои зависимости при необходимости (при необходимости), а не только через конструктор.
Проблема недостаточно явных зависимостей через конструктор объекта (на объекте, использующем локатор сервисов) решается именно тем, что я подчеркивал ранее: передачей нулевых указателей. Его можно даже использовать для смешивания и сопоставления обеих систем: если указатель нулевой, используйте локатор службы, в противном случае используйте указатель. Теперь это осуществляется через систему типов, и это очевидно для пользователя класса. Но мы можем сделать еще лучше.
Еще одно решение, которое, безусловно, выполнимо с C++ (я не знаю о Java/C#, но я полагаю, что это тоже можно сделать), состоит в том, чтобы написать вспомогательный класс, который будет создан, например
Сервисный локатор (SL)
Service Locator
решает [
DIP
+
DI
] проблема. Это позволяет по названию интерфейса удовлетворить потребности
class A {
IB ib
init() {
ib = ServiceLocator.Resolve<IB>();
}
}
Проблема здесь в том, что неясно, какие именно классы (реализации IB) используются клиентом (A). Service Locator может быть одноэлементным или может быть передан в конструктор.
предложение:
class A {
IB ib
init(ib: IB) {
self.ib = ib
}
}
SL против DI IoC Container (фреймворк)
SL - это сохранение экземпляров, когда DI IoC Container(framework) больше касается создания экземпляров.
SL работает как команда PULL, когда он извлекает зависимости внутри конструктора. DI IoC Container(framework) работает как команда PUSH, когда он помещает зависимости в конструктор