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

Что предлагает DI:

  • Совместное использование функций между различными компонентами приложения
  • предоставление макетов вместо реальных соединений при модульном тестировании
  • не беспокоясь об экземплярах классов
  • Четкое понимание зависимостей данного класса

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

Если вы новичок в Angular или незнакомы с внедрением зависимостей, я предлагаю вам сначала прочитать, что говорится в официальной документации по этой теме.

Предоставление переменных среды

Если вы какое-то время использовали Angular, скорее всего, вы знакомы с environment.ts файлами. Эти файлы используются для предоставления информации о среде, в которой работает наше приложение. Например, если наше приложение работает в среде разработки, мы хотим, чтобы наши службы выборки данных указывали на серверное приложение, работающее на http://localhost:3000/api, например, и если оно выполняется на временном сервере для ручного контроля качества, мы могли бы хотите, чтобы он указывал, например, на https://qa.local.com/api. Это управляется Angular во время процесса сборки с использованием разных файлов среды, например, у нас может быть два файла с именами environment.ts и environment.qa.ts в нашей папке environments, и когда мы запустим команду ng build --config qa, интерфейс командной строки Angular заменит наш environment.ts файл на environment.qa.ts, и приложение будет работать в режиме QA соответственно.

Но при чем тут DI?

Взгляните на этот компонент:

Здесь мы просто импортируем environment.ts файл и сразу же используем его содержимое.

Но что, если мы хотим ввести некоторые другие значения в эти переменные среды для модульного тестирования? Также обратите внимание на этот сервис, который также использует переменные среды:

Опять же, это выглядит очень нормально, но на самом деле это не лучший способ ссылаться на переменную среды. Представьте, что этот сервис является частью Angular-приложения, состоящего из нескольких проектов. Итак, у нас есть папка projects, содержащая разные приложения (например, веб-компоненты - хороший пример такого сложного приложения). Можем ли мы повторно использовать эту услугу в другом проекте? Теоретически мы можем - просто импортировать его в другой модуль Angular, используя массив providers, верно? Но вот проблема - разные проекты могут иметь разное окружение! Мы не можем просто импортировать один из них, но мы все равно должны обеспечить возможность повторного использования этой службы как можно большим количеством компонентов. Мы можем добиться этого с помощью DI.

Есть несколько возможных способов достижения этой функциональности, но давайте начнем с самого простого - Injection Tokens.

Токены внедрения - это концепция Angular, которая позволяет нам объявлять независимые уникальные токены внедрения зависимостей для внедрения значений в другие классы с помощью декоратора Inject. Читайте больше о них, здесь".

Все, что нам нужно сделать, это просто указать значение для нашей среды в модуле:

Как видите, мы предоставили значение для нашей переменной среды в InjectionToken, так что теперь мы можем использовать его в нашем сервисе:

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

Но все же это не лучшее решение. Мы не предоставили тип для частного поля environment, что само по себе обречено - нам нужны вводы, чтобы сделать наш код менее подверженным ошибкам, а Angular может использовать типизацию для улучшения DI - например, мы действительно можем избавиться от Inject декоратор и InjectionToken в целом! Вот что мы собираемся сделать: написать класс оболочки для описания интерфейса нашей переменной среды, а затем использовать его для предоставления фактического значения. Вот пример такого класса:

В этом классе мы описали, как на самом деле выглядит наша среда. Но мы также можем использовать этот класс в качестве токена внедрения для внедрения переменной окружения! Просто используя useValue в качестве провайдера:

А в нашем сервисе:

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

Но есть еще очень маленькая проблема. Учти это:

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

Дадим это:

Итак, мы «случайно» изменили переменную окружения. Это не очень большая проблема (разработчики обычно не меняют переменные среды случайным образом), но мы все же хотим усилить это правило. Это довольно просто - просто создайте поля в нашем классе readonly:

Теперь у нас есть все возможности DI, но мы также защищены от несчастных случаев.

Предоставление различных услуг в зависимости от среды

Теперь мы знаем, как предоставить переменные среды через DI, но как насчет переключения между различными службами (при сохранении API) в зависимости от среды?

Представьте себе следующую ситуацию: нам нужны некоторые отчеты о сбоях / статистика использования из нашего приложения, когда оно работает. Но природа журналов различается в зависимости от среды, которую мы используем: в среде разработки мы хотим просто записывать ошибку или предупреждение на консоль; в среде контроля качества мы хотим вызвать API, который будет собирать наши ошибки в файле Excel и отправлять их руководству; а в производстве нам нужен отдельный файл журнала на бэкэнде, поэтому мы вызываем другой API. Итак, мы пришли к идее создать службу Logger, которая будет обрабатывать эту функцию. Вот наивная реализация этой услуги:

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

  • Мы выполняем проверки каждый раз, когда вызывается logError метод. Это довольно бесполезно, потому что после создания приложения значение enviroment.name никогда не меняется! Результат оператора switch всегда будет одним и тем же, независимо от того, сколько раз и в каких ситуациях мы вызываем этот метод.
  • Сама реализация метода выглядит довольно некрасиво, она не так проста, как может показаться.
  • Что, если нам нужно регистрировать больше различной информации? Надо ли прописывать все эти проверки в каждом методе?

Что я предлагаю? Напишите отдельный LoggerServices для каждого сценария и вставьте только один из них условно, в зависимости от среды, используя factory. Вот как:

Разбивка того, что мы сделали:

  • Мы сокращаем LoggerService до объявления его API, без реализации. Мы собираемся использовать его как токен инъекции, а также как подсказку для TS, чтобы узнать его интерфейс.
  • Мы создаем отдельные классы для каждой среды, следя за реализацией LoggerService в каждой из них, чтобы у нас был один и тот же интерфейс API.
  • Каждый класс выполняет одни и те же методы, но по-разному. Не нужно проверять окружающую среду.

Хорошо, но как мы собираемся сообщить нашим компонентам, какую версию LoggerService использовать? Вот где factories вступает в игру.

Фабрика - это чистая функция, которая получает зависимости в качестве аргументов и возвращает значение, которое будет предоставлено для данного токена. Вот как мы собираемся предоставить наш LoggerService:

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

Но, конечно, это только часть решения: нам все еще нужно указать Angular использовать нашу фабрику и предоставить необходимые зависимости через массив deps.

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

Предоставление глобальных синглтонов

Angular гарантирует, что внутри данного модуля все компоненты получают один и тот же экземпляр зависимости. Например, если мы предоставляем услугу в AppModule, затем объявляем SomeComponent в этом модуле и вводим в него SomeService, а также в AnotherComponent, объявленный в том же модуле, и SomeComponent, и OtherComponent получат один и тот же экземпляр SomeService. Но в разных модулях это не будет одинаковым: каждый модуль имеет свой собственный инжектор зависимостей и будет порождать разные экземпляры одной и той же службы для разных модулей. Но что, если нам нужен один и тот же экземпляр для каждого компонента, службы или всего, что пытается использовать нашу зависимость? Итак, мы, по сути, хотим синглтона.

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

Здесь нет ничего особенного, но мы должны указать Angular вызвать наш метод getInstance:

Готово. Теперь мы можем просто вставить StoreService в любой компонент где угодно и убедиться, что у нас есть тот же экземпляр. Мы также можем предоставить зависимости фабрике, как обычно.

Общие советы по DI

Вот несколько коротких правил работы с DI в Angular:

  1. Всегда вставляйте каждое значение в свой компонент, никогда не полагайтесь на глобальные переменные, переменные из других файлов и так далее. Как правило, если вы обнаружите, что какой-либо метод вашего класса ссылается на что-либо, кроме свойств из этого класса или локальных переменных, измените свой класс, так как он получает это значение как внедренную зависимость (как мы это сделали с переменными среды).
  2. Никогда не используйте строковые токены для DI. В Angular можно дать декоратору Inject строку для поиска зависимости. Никогда не делайте этого - всегда возможна опечатка. Кроме того, вы не можете полагаться на IntelliSense для автозаполнения его за вас. Вместо этого используйте InjectionToken.
  3. Помните, что экземпляры служб совместно используются компонентами, по крайней мере, на уровне модуля, поэтому, если какие-либо свойства этих служб не предназначены для изменения из внешнего мира, подумайте о том, чтобы пометить их как readonly.
  4. Если вы используете класс, который будет предоставлен вместо другого, убедитесь, что вы implement их, как мы это сделали во втором примере. Таким образом, если интерфейс замененной зависимости изменится, нам придется заново реализовать класс, который предназначен для его замены, чтобы мы не столкнулись с загадочными ошибками.

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

Подпишитесь на меня в Medium и Twitter, чтобы узнать больше об Angular, Rxjs, React и Javascript в целом.