Главной задачей Локатора сервисов является предоставление экземпляров сервисов по запросам пользователей. Consumer использует интерфейс IService и запрашивает экземпляр у Локатора сервисов, который возвращает экземпляр той конкретной реализации, на возврат которой он сконфигурирован.
Если рассматривать только статическую структуру классов, контейнеры внедрения зависимостей покажутся очень похожими на локаторы сервисов. Разница между ними очень мала и заключается не в деталях механизмов реализации, а в способах их использования. По существу, запрос контейнеру или локатору на формирование полного графа зависимостей, выполненный из корня компоновки, является корректным способом их применения. Тот же запрос, но сделанный из любого другого места, приводит к тому, что локатор сервисов придется рассматривать как антипаттерн.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public static class Locator { private static readonly Dictionary<Type, object> Services = new Dictionary<Type, object>(); public static T GetService<T>() { return (T)Services[typeof(T)]; } public static void Register<T>(T service) { Services.Add(typeof(T), service); } public static void Reset() { Services.Clear(); } } |
Locator — это класс, содержащий только статические члены, поэтому его можно явно определить как статический класс. Он содержит все конфигурируемые сервисы во внутреннем словаре, который соотносит абстрактные типы с их конкретными экземплярами.
В качестве примера зарегистрируем SomeClass в локатор, а затем запросим ее с помощью статического метода GetService, чтобы получить требуемый экземпляр:
1 2 3 4 5 | var someClass = new SomeClass { Field = "123" }; Locator.Register(someClass); var service = Locator.GetService<SomeClass>(); Console.WriteLine(service.Field); |
Анализ
Локатор сервисов обладает преимуществами модульной архитектуры приложений, а также соответствует требованиям:
- Динамическое связывание возможно за счет возможности изменения регистрации
- Возможна параллельная разработка кода, поскольку программирование ведется в терминах интерфейсов и модули при желании могут заменяться
- Возможно получение хорошего разделения ответственности, поэтому ничто не мешает писать удобный в сопровождении код. Но реализация этого на практике становится намного более сложным делом
- Зависимости могут заменяться на тестовые двойники, что обеспечивает пригодность приложения для тестирования
Однако, локатор сервисов является опасным паттерном, так как он почти всегда работает. Можно осуществлять поиск зависимостей из классов-клиентов и можем заменять эти зависимости на другие реализации — даже когда применяются тестовые двойники при проведении модульного тестирования.
Влияние
Основная проблема, возникающая при использовании локаторе сервисов, заключается в том, что он сильно влияет на возможность переиспользования классов, являющихся его клиентами. Эта проблема проявляется в виде двух симптомов:
- Модуль может быть перемещен только вместе с ненужной зависимостью
- Неочевидно, что используется внедрение зависимостей
Помимо ожидаемой зависимости, которая внедряется в класс, теперь будет еще внедрятся и локатор, следовательно зависеть также еще и от класса Locator. Это означает, что для переиспользования класса придется включить в дистрибутив не только его и соответствующую ему зависимость, но и зависимость Locator, которая нужна только из технических соображений. Если класс Locator будет определен не в том же модуле, в котором определены класс и зависимость, то новые приложения, которые захотят повторно использовать класс, должны будут использовать также и модуль класса Locator.
И наконец, если захотим создать новый экземпляр класса, Visual Studio подскажет нам только, что этот класс имеет конструктор по умолчанию. Однако если затем попытаться выполнить код, который только что написали, мы получим ошибку времени исполнения, если забудем зарегистрировать экземпляр класса в классе Locator. Вероятность, что так и произойдет, весьма высока, если мы недостаточно хорошо знакомы с классом.
Проблема с локатором сервисов заключается в том, что любой клиент, использующий его, вводится в заблуждение относительно уровня его сложности. Выглядит он обманчиво простым, на что указывает его известный API, но на деле оказывается весьма сложным, и мы ничего не знаем о связанных с ним проблемах, пока не получаем ошибку времени исполнения.
При применении локатора сервисов становится проще внести в код изменения, нарушающие его работоспособность.
При проведении модульного тестирования возникает дополнительная проблема, связанная с тем, что тестовый двойник, зарегистрированный в одном наборе тестов, будет вызывать взаимозависимые тесты , поскольку будет оставаться в памяти, когда начнет выполняться другой набор тестов. Следовательно, нужно выполнять метод Teardown после выполнения каждого теста путем вызова Locator.Reset(). О необходимости этого действия нужно помнить разработчику, а забыть об этом очень легко.
Локатор сервисов может казаться безобидным, но способен порождать совершенно любые ошибки времени исполнения.
Рефакторинг в направлении внедрения зависимостей
Если пристально рассмотреть структуру локатора сервисов, он оказывается похожим на окружающий контекст. Оба неявно используют синглтоны, различие же между ними состоит в возможности применения локальных умолчаний.
Окружающий контекст гарантирует, что он всегда может предоставить подходящий экземпляр требуемого сервиса (как правило — только одного). Локатор сервисов не обеспечивает такой гарантии, поскольку он фактически является слабо типизированным контейнером сервисов, о которых отсутствует встроенная информация.
Во многих случаях класс, использующий локатор сервисов, может содержать несколько вызовов, распределенных по всему коду. В таких случаях он действует как замена оператору new, просто в другом виде. Выполнение рефакторинга класса, применяющего локатор сервисов, похоже на рефакторинг класса, использующего антипаттерн диктатор, поскольку локатор сервисов по сути является своеобразной разновидностью антипаттерна диктатор.
Резюме
Поскольку внедрение зависимостей представляет собой набор паттернов и приемов программирования, не существует единственного инструмента, который обеспечил бы механическую проверку того, насколько правильно оно реализовано. В предыдущих постах, мы познакомились с паттернами, определяющими способы корректной реализации внедрения зависимостей, но это лишь одна сторона медали. Помимо этого важно понять, где можно совершить ошибку, даже если вы допустите ее из наилучших побуждений.
Первая и самая главная привычка, от которой нужно избавиться, — это мнимая необходимость прямого управления зависимостями. Легко обнаружить места использования шаблона Диктатор: везде, где вы применяете ключевое слово new для создания экземпляра нестабильной зависимости, вы прибегаете к Диктатору.Единственное место, в котором можно создавать зависимости с помощью new, — это корень композиции.
Диктатор не позволяет реализовать динамическое связывание; другие антипаттерны просто делают динамическое связывание более неуклюжим. Поэтому в первую очередь разберитесь с Диктатором.
Локатор сервисов может показаться вполне нормальным паттерном, но на самом деле он является антипаттерном. Несмотря на то, что он решает некоторые связанные с внедрением зависимостей проблемы, он порождает другие, перевешивающие его достоинства. Нет надобности мириться с этими недостатками, поскольку представленные ранее паттерны(внедрение через конструктор, свойство, метод, окружающий контекст) являются лучшей альтернативой. Это справедливо при замене всех антипаттернов.
Вот и закончился цикл постов про антипаттерны внедрения зависимостей, и я надеюсь теперь вы знаете, чего следует избегать и что следует делать, но если остались вопросы, то прошу задавать их в комментариях. Всем спасибо!