Service Locator

Это может быть трудным – отказаться от идеи прямого контроля зависимостей, поэтому многие разработчики выводят статические фабрики (как описано в разделе 5.1.2) на новый уровень. Это приводит к анти-паттерну Service Locator.

Внимание

Это довольно спорно – называть Service Locator анти-паттерном. Некоторые люди считают его настоящим паттерном проектирования, тогда как другие (и я в том числе) считают анти-паттерном. В этой книге я решил описать его как анти-паттерн, потому что я думаю, что у него больше недостатков, чем преимуществ, но не удивляйтесь, если вы увидите, что он одобрен в других местах. Важно понять преимущества и недостатки, чтобы иметь возможность принять для себя обоснованное решение.

Service Locator был введен в качестве паттерна проектирования Мартином Фаулером в 2004, поэтому обозначение его как анти-паттерн является большим шагом. Короче говоря, он вводит статическую фабрику с дополнительной деталью, что становится возможно внедрить сервисы в эту фабрику.

Примечание

Термин сервис в данном контексте приблизительно эквивалентен термину зависимость.

Как это чаще всего реализуется, Service Locator является статической фабрикой, которая может быть сконфигурирована с конкретными сервисами, пока первый потребитель не начинает ее использовать (см. рисунок 5-7). Это, вероятно, может произойти в Composition Root. В зависимости от конкретной реализации, Service Locator может быть настроен с кодом, когда читаются файлы конфигурации или используется их комбинации.

Рисунок 5-7: Основная ответственность Service Locator заключается в том, чтобы обрабатывать экземпляры сервисов, когда потребители их запрашивают. Consumer использует интерфейс IService и запрашивает экземпляр от Service Locator, который затем возвращает экземпляр той конкретной реализации, которую он должен вернуть

Основная обязанность Service Locator заключается в том, чтобы обрабатывать экземпляры сервисов, когда потребители их запрашивают. Потребитель использует интерфейс IService и запрашивает экземпляр у Service Locator, который затем возвращает экземпляр любой конкретной реализации, которую он сконфигурирован вернуть.

Моя личная история с Service Locator

У меня с Service Locator были интенсивные отношения в течение нескольких лет, прежде чем мы расстались. Хотя я точно не помню, когда я впервые наткнулся на статью Фаулера, мне показалось, что он предоставил мне потенциальное решение проблемы, которую я обдумывал в течение некоторого времени: как внедрить зависимости.

Как было описано, паттерн Service Locator казался ответом на все мои вопросы, и я быстро начал разрабатывать Service Locator для первой версии Microsoft patterns & practices’ Enterprise Library. Это было размещено на ныне не существующем сайте GotDotNet. Хотя у меня все еще есть исходный код, я потерял историю релизов, когда GotDotNet закрыли, поэтому я не могу сказать наверняка, но я, кажется, опубликовал первую версию в середине 2005 года.

В 2007 году я выпустил полностью переписанный релиз, ориентированный на Enterprise Library 2. Он по-прежнему доступен на CodePlex, но я давно уже отказался от него, потому что я вскоре пришел к выводу, что это был действительно анти-паттерн.

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

Внимание

Если вы посмотрите только на статическую структуру классов, DI контейнер выглядит как Service Locator. Разница мизерная и заключается не в механике реализации, а в том, как вы ее используете. В сущности, просьба к контейнеру или локатору разрешить полный граф зависимости из Composition Root является правильным использованием. Просьба о "зернистых" сервисах из любого другого места подразумевает анти-паттерн Service Locator.

Давайте рассмотрим пример, где он сконфигурирован с кодом.

Пример: ProductService, использующий Service Locator

Для примера давайте вернемся к нашему проверенному и истинному ProductService, который требует экземпляр абстрактного класса ProductRepository. В данном случае ProductService может использовать статический метод GetService, чтобы получить необходимый экземпляр:

this.repository = Locator.GetService<ProductRepository>();

В данном примере я реализую методы, используя параметры дженерик типа, чтобы указать тип запрашиваемого сервиса, но я также мог бы использовать экземпляр Type, чтобы указать тип.

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

Листинг 5-5: Минимальная реализация Service Locator
public static class Locator
{
	private readonly static Dictionary<Type, object> services
		= new Dictionary<Type, object>();
	public static T GetService<T>()
	{
		return (T)Locator.services[typeof(T)];
	}
	public static void Register<T>(T service)
	{
		Locator.services[typeof(T)] = service;
	}
	public static void Reset()
	{
		Locator.services.Clear();
	}
}

Строки 5-8: Получить сервис

Locator – это класс с только статическими членами, так что вы могли бы также явно отметить его как статический класс. Он содержит все настроенные сервисы во внутреннем словаре, который связывает абстрактные типы с конкретными экземплярами.

Клиенты, такие как ProductService, могут использовать метод GetService, чтобы запросить экземпляр абстрактного типа T. Поскольку в этом примере кода не содержится ограждающее условие или обработка ошибок, данный метод сгенерирует довольно туманное KeyNotFoundException, если запрашиваемый тип не имеет записи в словаре, но вы можете представить, как добавить код, чтобы выбросить более осмысленное исключение.

Метод GetService может возвращать только экземпляр запрошенного типа, если он ранее был вставлен во внутренний словарь. Это можно сделать с помощью метода Register. Опять же, этот пример кода не содержит ограждающего условия, так что можно было бы зарегистрировать null, но более надежная реализация не должна этого допустить.

В некоторых случаях (особенно при модульном тестировании), важно иметь возможность сбросить Service Locator. Эта функциональность обеспечивается методом Reset, который очищает внутренний словарь.

Такие классы, как ProductService полагаются на сервис, чтобы быть доступными в Service Locator, поэтому очень важно, что он был ранее настроен. В модульных тестах это может быть сделано при помощи тестового дублера (поддельного объекта, Test Double), реализованного динамической mock библиотекой, такой как Moq, как в этом примере:

var stub = new Mock<ProductRepository>().Object;
Locator.Register<ProductRepository>(stub);

Сначала мы создаем заглушку абстрактного класса ProductRepository, а затем с помощью статического метода Register настраиваем Service Locator с этим экземпляром. Если это сделано, прежде чем ProductService используется в первый раз, ProductService будет использовать настроенный Stub, чтобы работать с ProductRepository. В производственном приложении Service Locator будет настроен с правильной реализацией ProductRepository в Composition Root.

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

Анализ

Service Locator является опасным паттерном, потому что он почти работает. Мы можем обнаружить зависимости из потребляющих классов, и мы можем заменить эти зависимости различными реализациями – даже поддельными объектами из юнит тестов.

Если мы применим модель анализа, изложенную в главе 1, чтобы оценить, соответствует ли Service Locator преимуществам модульной конструкции приложении, мы увидим, что в основном он соответствует:

  • У нас есть поддержка для позднего (отложенного) связывания при смене регистрации.
  • Возможна параллельная разработка, потому что мы программируем, используя интерфейсы, и можем по желанию заменить модули.
  • Мы можем достичь хорошего разделения понятий, поэтому нас ничего не останавливает от написания поддерживаемого кода. Но это все сложнее сделать.
  • Мы можем заменить зависимости дублирующими элементами, поэтому тестируемость возможна.

Существует только одна область, где Service Locator не дотягивает.

Влияние

Основная проблема с Service Locator заключается в том, что он влияет на повторное использование классов, которые его потребляют. Это проявляется в двух направлениях:

  • Модуль потянет за собой избыточную зависимость.
  • Это не очевидно, что используется DI.

Давайте сначала посмотрим на граф зависимостей для ProductService из примера в разделе 5.4.1, который показан на рисунке 5-8. В дополнение к ожидаемой ссылке на абстрактный класс ProductRepository, ProductService также зависит от класса Locator.

Рисунок 5-8: Граф зависимости для реализации ProductService, которая использует Service Locator, чтобы обработать экземпляры абстрактного класса ProductRepository.

Это означает, что для повторного использования класса ProductService, мы должны перераспределить не только его релевантную зависимость ProductRepository, а также зависимость Locator, которая существует только по механическим причинам. Если класс Locator определен в другом модуле, чем ProductService и ProductRepository, новые приложения, которые желают повторно использовать ProductService, должны также принять и этот модуль.

Чтобы понять, в чем дело, представьте, что новое приложение, повторно использующее ProductService, уже использует другую DI стратегию, сосредотачиваясь на внедрении в конструктор. ProductService не вписывается в эту стратегию, но вместо этого навязывает свою собственную стратегию, которая сильно загрязняет DI архитектуру нового приложения. Чтобы это использовать, разработчики должны принять существование Service Locator, и после того как он введен, он может быть случайно использован начинающим разработчикам для других целей, где существуют лучшие альтернативы.

Возможно, мы могли даже просто перетерпеть эту дополнительную зависимость для Locator, если бы это было действительно необходимо для работы DI – мы бы рассматривали это в качестве налога, подлежащего уплате, чтобы получить другие преимущества. Тем не менее, есть лучшие варианты (например, внедрение в конструктор), так что эта зависимость является избыточной.

Чтобы добавить соли на рану, ни эта избыточная зависимость, ни ее соответствующий дубликат, ProductRepository, явно не видны разработчикам, желающим использовать класс ProductService. Рисунок 5-9 показывает, что Visual Studio не может предложить никаких рекомендаций по использованию этого класса.

Рисунок 5-9: Единственная вещь, которую нам может сказать IntelliSense о классе ProductService, это то, что у него есть конструктор по умолчанию. Его зависимости невидимы.

Когда мы хотим создать новый экземпляр класса ProductService, Visual Studio может сказать нам только, что класс имеет конструктор по умолчанию. Однако если мы впоследствии попытаемся запустить код, который мы только что написали, мы получим сообщение об ошибке времени выполнения, если мы забыли зарегистрировать экземпляр ProductRepository при помощи класса Locator. Это может произойти, если мы хорошо не знаем класс ProductService.

Совет

Представьте себе, что код, который мы пишем, попадает в незадокументированный, туманный .dll. Как легко его может использовать кто-то еще? Это возможно – разработать API, которые близки к самодокументированию, и хотя для этого требуется практика, это достойная цель.

Примечание

Проблема с Service Locator заключается в том, что любой клиент, использующий его, не знает о его уровне сложности. Он выглядит простым, но оказывается сложным – и мы этого не узнаем, пока не получим исключение времени выполнения.

Проблема с классом ProductService заключается в том, что он далек от самодокументируемости: мы не можем сказать, какая зависимость должна присутствовать, прежде чем он будет работать. На самом деле, разработчики ProductService могут даже решить добавить несколько зависимостей в будущие версии, поэтому код, который работает в текущем варианте, может не сработать в будущей версии, и мы даже не получим ошибку компиляции, которая предупредит нас. С Service Locator легко можно случайно ввести критические изменения.

Внимание

Использование дженериков может ввести вас в заблуждение, что Service Locator строго типизирован. Однако даже API, как в листинге 5-5, слабо типизированный, потому что мы можем запросить любой тип. Возможность компилировать код при помощи вызова метода GetService<T> не дает нам никакой гарантии, что он не будет выбрасывать исключения налево и направо во время выполнения.

Примечание

При модульном тестировании у нас есть дополнительная проблема, что тестирующий дублер, зарегистрированный в одном тесте, вызовет взаимозависимые тесты (Interdependent Tests), потому что он останется в памяти, когда будет выполнен следующий тест. Поэтому необходимо использовать методы тестовых фикстур (Fixture Teardown) после каждого теста, вызывая Locator.Reset(), и мы должны помнить все время, что это делается вручную, а это легко забыть.

Это все действительно плохо. Service Locator может показаться безобидным, но это может привести ко всяким неприятным ошибкам выполнения. Как избежать этих проблем?

Рефакторинг по направлению к DI

Когда мы решаем избавиться от Service Locator, мы должны найти способ сделать это. Как всегда, главной альтернативой по умолчанию должно быть внедрение в конструктор, если ни один из других DI паттернов из главы 4 не подходит лучше.

Внимание

Когда мы смотрим на структуру Service Locator, она близка к Ambient Context. Оба неявно используют Одиночки (Singletons) но разница заключается в наличие Local Default. Ambient Context гарантирует, что он всегда может предоставить соответствующий экземпляр запрошенного сервиса (как правило, имеется только один). А Service Locator не может дать такую гарантию, потому что он, в сущности, является слабо типизированным контейнером сервисов, о которых он не имеет встроенных знаний.

Во многих случаях класс, который потребляет Service Locator, может вызываться по всему коду. В таких случаях он действует в качестве замены для ключевого слова new. Если это так, первый шаг рефакторинга заключается в закреплении создания каждой зависимости в одном методе.

Если мы не будем иметь поле для хранения экземпляра зависимостей, мы можем ввести такое поле и убедитесь, что остальная часть кода использует это поле, когда потребляет зависимость. Отметьте поле как readonly, чтобы оно не могло быть изменено за пределами конструктора. Это заставляет нас присваивать значения полю из конструктора при помощи Service Locator. Теперь мы можем ввести параметр конструктора, который присваивает значение полю, вместо Service Locator, который затем может быть удален. Представляя параметр зависимости конструктору, можно нарушить работу существующих потребителей, поэтому мы также должны справиться с этим и переместить продвижение всех зависимостей в Composition Root.

Рефакторинг класса, который использует Service Locator, похож на рефакторинг класса, который использует Control Freak, потому что Service Locator – это просто окольный вариант Control Freak. Раздел 5.1.3 содержит дополнительные заметки о рефакторинге реализациий Control Freak к использованию DI.

На первый взгляд, Service Locator может выглядеть как настоящий DI паттерн, но не обманывайте себя: он может решить вопрос слабой связанности, но он создает другие проблемы на этом пути. DI паттерны, представленные в главе 4, предлагают лучшие альтернативы с меньшим количеством недостатков. Это верно как для анти-паттерна Service Locator, так и для других анти-паттернов, представленных в этой главе. Даже если они разные, все они имеют общую черту – решить создаваемые ими проблемы можно с помощью одного из DI паттернов из главы 4.

или RSS канал: Что новенького на smarly.net