Автор: Sergey Teplyakov

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

Но прежде чем переходить к описанию достоинств и недостатков этого паттерна, давайте дадим его описание и общую структуру.

Описание

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

image// Статический 'локатор' public static class ServiceLocator {
public static object GetService(Type type) {}
public static T GetService<,T>,() {} } // Сервис локатор в виде интерфейса public interface IServiceLocator {
T GetService<,T>,(), }

Сервис локатор может быть статическим классом с набором статических методов, или же может существовать в виде интерфейса для упрощения тестирования.

Назначение

Разорвать жесткую связь между классом и его вспомогательными сервисами, путем добавления специального класса (локатора), который будет за это отвечать.

Применимость

Прочитав описание и назначение этого паттерна, внимательный читатель заметит невероятное сходство Сервис Локатора со своим любимым DI контейнером. И правда, основная суть любого контейнера заключается в регистрации и последующего получения требуемых зависимостей, с рядом дополнительных свистелок, которые делают этот процесс более сложным удобным.

Однако между DI контейнером и его использованием в виде Сервис Локатора существует тонкая грань. По всем правилам, использование контейнера должно быть ограничено минимальным количеством мест. В идеале, в приложении должна быть лишь одна точка, где производится вызов метода container.Resolve(), этот код должен находиться либо в точке инициализации приложения (так называемый Composition Root), либо максимально близко к ней.

Однако наличие в арсенале универсального объекта, способного получить любую зависимость, провоцирует к его использованию напрямую и в других частях приложения.

Предположим, нам во вью модели редактирования карточки сотрудника нам нужно обратиться к сервису из слоя доступа к данным. Мы можем протянуть нужную зависимость через конструктор, а можем просто передать этой вью модели сам контейнер, чтобы она получила требуемую зависимость самостоятельно:class EditEmployeeViewModel {
private Employee _employee,
private IServiceLocator _serviceLocator,
public EditEmployeeViewModel(IServiceLocator serviceLocator)
{
_serviceLocator = serviceLocator,
}
private void OkCommandHandler()
{
ValidateEmployee(_employee),
var repository = _serviceLocator.GetService<,IRepository>,(),
repository.Save(_employee),
}
private void ValidateEmployee(Employee employee) {} }

И хотя использование Сервис Локатора является довольно популярным, у него есть ряд фатальных существенных недостатков.

Недостатки Сервис Локатора

Существует две версии реализации этого паттерна Сервис Локатора. Локатор сам по себе может быть синглтоном (в классическом виде, или в виде класса с набором статических методов), тогда доступ к нему может производиться из любой точки в коде. Или же Сервис Локатор может передаваться требуемым классам через конструктор или свойство в виде объекта класса или интерфейса.

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

Неясный контракт класса

Даже если вы не являетесь поклонником контрактного программирования (а зря,)), вы наверняка ловили себя на мысли, что у любого класса есть две составляющие. С одной стороны, есть требования заказчика (клиента вашего класса): он хочет получить от класса некоторый результат (получить запись о пользователе из базы данных). С другой стороны, для выполнения этой работы класс имеет право требовать что-то от своего клиента (идентификатор пользователя, данные о котором нужно получить из базы данных).

В результате, между классом и его клиентом существует формальный или неформальный контракт, который выражается в виде предусловий (требований к клиенту) и постусловий (гарантий выполнения работы). Однако если класс принимает экземпляр сервис локатора, или, хуже того, использует глобальный локатор, то этот контракт, а точнее требования, которые нужно выполнить клиенту класса, становятся неясными:class EditEmployeeViewModel { private IServiceLocator _serviceLocator,
public EditEmployeeViewModel(IServiceLocator serviceLocator)
{
_serviceLocator = serviceLocator,
}
}

Как понять клиенту данного класса, что от него требуется для того, чтобы данный объект выполнил свою часть работы? Какие предусловия класса EditEmployeeViewModel? Наличие в сервис локаторе IRepository, ILogger, IEMailSender, ISomethingElse? Чтобы понять это нам придется проанализировать исходный код этого класса, что совсем не просто, а иногда еще и невозможно.

Кроме того, вполне возможно, что EditEmployeeViewModel не выполняет всю работу самостоятельно, а делегирует часть обязанностей другому классу, например, DialogEditViewModel. И уже этот класс получает нужную зависимость у Сервис Локатора для сохранения сотрудника. В результате, чтобы понять требования класса EditEmployeeViewModel придется проанализировать исходный код не только этого класса, но и всех зависимых классов.

image

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

Неопределенная сложность класса

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

Когда наш класс использует сервис локатор, то стабильность класса становится неопределенной. Наш класс, теоретически, может использовать что угодно, поэтому изменение любого класса (или интерфейса) в нашем проекте может затронуть произвольное количество классов и модулей.

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

ПРИМЕЧАНИЕ
Я не знаю, что может быть хуже неявной связности, которая приводит к неявной сложности. Если класс является сложным по своей природе, я стараюсь сделать все для того, чтобы это было ясно через призму его открытого интерфейса. Мы должны постараться спрятать эту сложность внутри класса, но если классу для его работы требуется десять зависимостей, то это требование должно быть явным.

Отсутствие строгой типизации

Сервис локатор можем содержать несколько способов получения (или поиска) зависимостей:class Locator {
// Не строготипизированное получение сервиса public object Resolve(Type type) { }
// Якобы строготипизированное получение сервиса public T Resolve<,T>,() { } }

Многие склонны считать, что метод Resolve, возвращающий object является слабо типизированным, в то время, как обобщенный метод Resolve обеспечивает строгую типизацию.

Как правильно писал Эрик Липперт, проблема здесь в том, что понятие строгой типизации весьма размыто и даже в Вики есть десяток разных определений. Да, обобщенный метод Resolve устраняет необходимость приведения типов (это приведение находится внутри этого метода,)), но ведь отсутствие явного приведения типов не является единственным свойством строгой типизации.

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

Смягчаем проблему

Сейчас должно быть понятно, что от Сервис Локатора стоит держаться подальше, но что делать, если наше приложение уже активно его использует и избавиться от него не так и просто?

Существует два разных способа получения зависимостей у Сервис Локатора. Во-первых, мы можем получать необходимые зависимости по мере необходимости:class EditEmployeeViewModel { private void OkCommandHandler()
{
ValidateEmployee(_employee),
var repository = _serviceLocator.GetService<,IRepository>,(),
repository.Save(_employee),
}
}

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

В качестве альтернативы, мы можем не ждать потребности в зависимостях до самого последнего момента, а получить их в конструкторе класса:class EditEmployeViewModel {
private readonly IRepository _repository,
private readonly ILogger _logger,
private readonly IMailSender _mailSender,
private readonly IServiceLocator _locator,

public EditEmployeViewModel(IServiceLocator locator)
{
_locator = locator,
_repository = locator.GetService<,
IRepository>,(),
_mailSender = locator.GetService<,
IMailSender>,(),
_logger = locator.GetService<,
ILogger>,(),
} }

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

Заключение

Самое страшное в Сервис Локаторе то, что он дает видимость хорошего дизайна. У нас никто не знает о конкретных классах, все завязаны на интерфейсы, все нормально тестируется и расширяется. Но когда вы попробуете использовать ваш код в другом контексте или когда кто-то попробует использовать его повторно, вы с ужасом поймете, что у вас есть дикая логическая связанность, о которой вы и не подозревали.

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

Дополнительные ссылки

Service Locator is an Anti-Pattern by Mark Seemann

Серия статей об управлении зависимостями
  1. Управление зависимостями
  2. Наследование vs Композиция vs Агрегация
  3. DI Паттерны. Constructor Injection
  4. DI Паттерны. Property Injection
  5. DI Паттерны. Method Injection
  6. Критерии плохого дизайна
  7. Аксиома управления зависимостями
  8. Service Locator

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).