company_banner

Что можно положить в механизм Dependency Injection в Angular?

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

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

    Давайте посмотрим на этот механизм в Angular чуть глубже.




    Знаете ли вы свои зависимости?


    Иногда нелегко понять, сколько зависимостей имеет ваш код.

    Например, взгляните на этот псевдокласс и посчитайте, сколько зависимостей он имеет:

    import { API_URL } from '../../../env/api-url';
    import { Logger } from '../../services/logger';
     
    class PseudoClass {
       request() {
           fetch(API_URL).then(...);
       }
     
       onError(error) {
           const logger = new Logger();
     
           logger.log(document.location, error);
       }
    }
    

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

    API_URL — импортированные данные из другого файла тоже можно считать зависимостью вашего класса (зависимость от расположения файла).

    new Logger() — также импортированные данные из другого файла и пересоздания множества экземпляров класса, когда нам достаточно лишь одного.

    document — также браузерное API и завязка на глобальную переменную.


    Ну и что же не так?


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

    Другая ситуация: document и fetch будут без проблем работать в вашем браузере. Но если однажды вам потребуется перенести приложение в Server Side Rendering, то в nodejs окружении необходимых глобальных переменных может не быть.

    Так и что же за DI и зачем он нужен?


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

    Если хотите рассмотреть DI с более теоретической стороны, почитайте о принципе инверсии управления. Также можете посмотреть интересные видеоматериалы по теме: серия видео про IoC и DI у Ильи Климова на русском или небольшое видео про IoC на английском.

    Вся магия возникает от порядка, в котором мы поставляем и берем зависимости.

    Схема работы областей видимости в DI:


    Что мы можем положить в DI?


    Первая из операций с DI — что-то положить в него. Собственно, для этого Angular позволяет нам прописывать providers-массив в декораторах наших модулей, компонентов или директив. Давайте посмотрим, из чего этот массив может состоять.

    Providing класса


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

    Angular создает экземпляр класса, когда вы запрашиваете его в первый раз. А с Angular 6 мы можем и вовсе не прописывать классы в массив providers, а указать самому классу, в какое место в DI ему встать с providedIn:

    providers: [
       {
           provide: SomeService,
           useClass: SomeService
       },
       // Angular позволяет сократить эту запись как самый частый кейс:
       SomeService
    ]
    


    Providing значения


    Также через DI можно поставлять константные значения. Это может быть как простая строка с URL вашего API, так и сложный Observable с данными.

    Providing значения обычно реализуется в связке с InjectionToken. Этот объект — ключ для DI-механизма. Сначала вы говорите: «Я хочу получить вот эти данные по такому ключу». А позже приходите к DI и спрашиваете: «Есть ли что-то по этому ключу?»

    Ну и частый кейс — проброс глобальных данных из корня приложения.

    Лучше посмотреть это сразу в действии, поэтому давайте взглянем на stackblitz с примером:

    Развернуть пример



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

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


    Providing фабрики


    На мой взгляд, это самый мощный инструмент в механизме внедрения зависимостей Angular.

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

    Вот еще один stackbitz с подробным примером создания фабрики со стримом.

    Развернуть пример



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

    Providing существующего экземпляра


    Не самый частый случай, но этот вариант бывает очень полезным инструментом.

    Вы можете положить в токен сущность, которая уже была создана. Хорошо работает в связке с forwardRef.

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

    Развернуть пример



    Хитрости с DI-декораторами


    DI-декораторы позволяют сделать запросы к DI более гибкими.

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

    Не многие также знают, что DI-декораторы можно использовать в массиве deps, который готовит аргументы для фабрики в providers.

    
    providers: [
       {
         provide: SOME_TOKEN,
         /**
          * Чтобы фабрика получила декорированное значение, используйте такой
          * [new Decorator(), new Decorator(),..., TOKEN]
          * синтаксис.
          *
          * В этом случае вы получите ‘null’, если не будет значения для
          * OPTIONAL_TOKEN
          */
         deps: [[new Optional(), OPTIONAL_TOKEN]],
         useFactory: someTokenFactory
       }
     ]
    


    Фабрика токена


    Конструктор InjectionToken принимает два аргумента.

    Второй аргумент — объект с конфигурацией токена.

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

    Посмотрите пример реализации функционала стрима нажатия кнопок, но уже на фабрике токена.

    Развернуть пример



    Заключение


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

    Надеюсь, этой статьей мне удалось дать вам фундамент, на основе которого вы сможете придумать собственные решения для упрощения работы с данными в ваших приложениях и библиотеках.
    TINKOFF
    IT’s Tinkoff — просто о сложном

    Comments 11

      0
      Спасибо за статью! Раньше думал, что закинуть побольше вещей в DI, круто и правильно, то сейчас скажу нет. Стандартные вещи как window, document — считаю излишеством закидывать в DI. Ваша зависимость либо эмулирует поведение стандартной фичи, либо возвращает null(undefined). В целом сказал бы бесполезная процедура. Да и лишние затраты на создание провайдеров. Из опыта SSR, также добавлю, что в node.js чаще всего придется эмулировать браузерные объекты. Редко проекты обходятся без плагинов, которые заточены только под браузер
        +1
        Привет!

        У нас как раз наоборот получилось — раньше опирались на глобальные объекты, использовали isBrowser (например, в библиотеке шаренных компонентов, которые могут быть использованы в любом контекте). Потом перешли на токены — тестировать полностью независимые сущности всегда проще, не надо думать о контексте исполнения

        Обычно нужны не сами window и document, а их преобразования в различные location / performance / userAgent, их уже мокаем в тестах или стабаем в SSR. Даже вот опенсорсную либу накрутили, чтобы в SSR можно было подставлять объекты вроде UserAgent github.com/ng-web-apis/universal
        0

        Возможно, это локальная проблема, но при открытии этой статьи процессор загружается на 100%. На мобильной версии сайт пару раз перегружается, после чего выскакивает "проблема с открытием этой страницы". Возникает только на этом посте.

          0

          Ну ещё бы. Тут же в статью встроена полноценная IDE, притом аж в 4х независимых экземплярах...

            0
            Столько лет на Хабре нельзя было встроить стэкблитц и спустя всё это время они не утрудились даже сделать, чтобы он загружал их только при доскролле. Даже в спойлере грузит сразу и плевать хотел на аргументы стэкблица «запускать по клику»… :(
              0
              У меня, к тому же, все 4 экземпляра открывают один и тот же пример…
                0
                Перепроверил примеры — все на местах, странно. Похоже, что эксперимент со Stackblitz'ами на Хабре вышел неудачный :(

                У меня основной блог на медиуме на английском, там такие штуки на радость заходят и хорошо работают, вот и здесь пробую
            +1
            Чтобы уж полностью проникнуться, без всякой магии Angular, то вот: inversify.io — годный контейнер. Давно пользуюсь и на бэке и на фронте.
              0
              Почитал, посмотрел на примеры. Просто вот вопросы:
              1. Вы считаете что зависимость от расположение файла это повод для создания DI. Соврменная IDE может запросто поменять все пути к этому файлу во всех местах, где он использутеся. Я не понимаю профита от DI в этом случае
              2. В последнем примере вы точно так же можете поменять расположение pressed-key.ts файла и всё сломается если не делать этого через IDE.
              Для себя я понял, DI очень крут когда нужно мокать сервисы во время тестирования. В остальных случаях я не особо понимаю профита, я не знаю может это прийдёт с опытом, но я уже какой раз пытаюсь найти какой то полезный кейс для себя, и не вижу его.
                0
                Добрый день!

                Нет, я не считаю, что зависимость от расположения обязательно повод для создания DI. Тот псевокласс показывал, какие есть зависимости и что нам не всегда их легко подметить. Он ни к чему не призывал, так что извиняюсь, что смутил этим :)
                  0
                  прийдёт с опытом

                  На самом деле чем больше опыта, тем больше осознание что DI тут какой-то ущербный :( В статье например не сказано что все зависимости по сути синглтоны (пусть и в пределах модуля), даже фабрика и та создает синглтоны (кмк несколько неочевидно), хотя это по-моему упоминается в документации. Если синглтон не нужен приходиться явно добавлять сервис в компонент и писать @Self() — очень раздражает и легко ошибиться (особенно когда компоненты наследуются). Надеюсь что когда-нибудь они добавят providedIn: 'component' или что-то подобное ...


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

                Only users with full accounts can post comments. Log in, please.