Сервисы и DI

Когда речь заходит о сервисах и Dependency Injection (DI) в Angular, обычно все сводят к тому, что это «мощно и гибко». Но на практике разработчик сталкивается с дилеммой: какую стратегию DI выбрать под свою конкретную задачу? Ошибка здесь стоит дорого — от раздутого кода до неработающих тестов.
Этот материал — не пересказ документации. Мы разберём, какие подходы к сервисам и DI работают для разных аудиторий: от новичка, собирающего своё первое приложение, до команды, поддерживающей enterprise-систему с тысячью модулей.
Кому подойдёт стандартный сервис на @Injectable с providedIn: 'root'
Это классика. Один экземпляр сервиса на всё приложение. Идеально, когда у вас нет сильной потребности в изоляции — например, сервис логирования, общий кэш или централизованное хранилище данных.
На практике 70% сервисов в Angular-проектах — это именно «корневые». Проблема начинается, когда разработчик по инерции делает все сервисы корневыми, хотя некоторые должны быть локальными. Типичный пример — сервис для работы с корзиной интернет-магазина. Если объявить его providedIn: 'root', то данные корзины будут общими для всех пользователей одного сеанса, что может вызвать путаницу в админке с просмотром чужих корзин.
- Для кого: новички, фрилансеры, малые проекты (до 20 модулей).
- Плюсы: одна строка кода, сборщик tree-shaking удаляет неиспользуемые сервисы, простота понимания.
- Минусы: сложно тестировать изоляцию, невозможно иметь разные версии сервиса для разных фич.
- Когда использовать: сервисы конфигурации, логирования, вспомогательные утилиты без состояния.
Сервисы уровня NgModule: для крупных модульных приложений
Здесь мы указываем провайдеров в декораторе @NgModule({ providers: [...] }). Каждый экземпляр сервиса живёт ровно в контексте своего модуля — разным модулям можно внедрить разные реализации.
Это уже продвинутый уровень. Представьте, что у вас в приложении есть два раздела: «Каталог товаров» и «Панель администратора». В разделе «Каталог» сервис должен показывать все товары (включая скрытые для обычных пользователей), а в админке — только те, что редактирует текущий сотрудник. С корневым сервисом этого не сделать. Только через провайдеры на уровне модуля.
- Для кого: команды, работающие над модульными monorepo-приложениями (от 20 модулей), enterprise-проекты с разными гранулами.
- Плюсы: гибкость, изоляция зависимостей, упрощение тестирования на уровне модуля.
- Минусы: нарушение tree-shaking (все сервисы попадают в сборку), риск случайного создания нескольких экземпляров при добросовестной загрузке модулей.
- Совет: обязательно используйте загрузчик модулей с явным импортом — lazy loading без изучения контекста может породить дубликаты сервисов.
Локальные провайдеры на компоненте: для виджетов и многократных инстансов
Когда один общий сервис не подходит, но и модульный уровень слишком груб, провайдеры на компоненте приходят на помощь. Просто добавляете providers: [MyService] в декоратор @Component, и каждый компонент получает свой экземпляр сервиса.
Это правда жизни для виджетов на одной странице. Например, на микрофинансовой платформе есть 5 разных графиков продаж, каждый со своим набором данных. Если сделать сервис общего состояния, они будут конфликтовать. Решение — каждый компонент графика имеет свой ленгвист сервиса для загрузки данных.
- Для кого: разработчики UI-китов, команды, создающие динамические панели приборов, все, кто работает с повторяющимися компонентами.
- Плюсы: тотальная изоляция, автоматическое уничтожение сервиса вместе с компонентом, независимое тестирование.
- Минусы: нужно следить за памятью (если сервис хранит тонны данных), не подходит для передачи состояния между родственными компонентами.
- Важно: если нужен общий доступ между parent и child, используйте @Host() или @Optional() декораторы —иначе контекст DI запутается.
Использование InjectionToken: когда интерфейсы не имеют декоратора
Классический сервис на @Injectable — не единственный путь. Если вы передаёте примитив, конфигурационный объект или интерфейс (который не является инъектируемым), используйте InjectionToken. Это фабрика для создания провайдеров по контракту, а не по классу.
Частая ошибка новичков: они пытаются внедрить строку или объект конфигурации напрямую через DI — получают тоскливый выброс NullInjectorError. Решение — создание токена с описанием провайдера. Например, токен для передачи API-ключа в сервис: const API_KEY = new InjectionToken<string>('api-key');
- Для кого: продвинутые разработчики, архитекторы, проекты с кастомными фабриками и конфигами.
- Плюсы: можно внедрять любое значение, в том числе с фабричной генерацией, подходит для замены реализаций в тестах.
- Минусы: сложность отладки (стек ошибки абстрактный), требуется типизация.
- Пример из жизни: во всех проектах нашей команды (2026 год) токенами мы описываем URL серверов, флаги фич и конфигурацию логирования — это даёт единый конфиг вне Angular-иерархии.
Нестандартный случай: DI с forwardRef и циклическими зависимостями
Наконец, есть пик мастерства — разрешение циклических зависимостей. Когда сервис A ссылается на сервис B, а B — обратно на A, Angular падает с «circular dependency».
Рецепт от профи: используйте forwardRef, чтобы сообщить загрузчику, что зависимость будет определена позже. Это костыль? Частично. Но в сложных работах, где shared-сервисы общаются друг с другом (как в модели domain-driven design), без forwardRef не обойтись.
- Для кого: только команды, проектирующие глубоко связанные системы (микросервисы на одном приложении) — новичкам не советуем.
- Плюсы: позволяет сохранить чистую архитектуру.
- Минусы: циклы — признак нарушения SOLID, лучше перепроектировать логику.
- Совет: если нужно зацикливание, выносите логику в третий сервис-посредник.
Итог: как выбрать свой путь
Выбор стратегии DI в Angular не техническая прихоть — это архитектурное решение, влияющее на поддержку и развитие продукта в 2026 году.
Резюмируем: если вы инди-разработчик, берите providedIn: 'root' и не парьтесь.
Если работаете в команде над автономными модулями — освойте провайдеры на уровне NgModule.
Если делаете динамические интерфейсы с повторяющимися независимыми виджетами — компонентный уровень DI станет вашим спасением.
Ну а для продвинутых кейсов запомните про InjectionToken и forwardRef.
И вот ключевое отличие этой страницы от других курсов: мы не просто описываем DI, а привязываем его к профилю разработчика — кто, когда и для чего использует каждый вариант. Другие ресурсы дают общую теоретическую базу, а здесь вы видите конкретные сегменты аудитории и критерии выбора. Больше никакого абстрактного «внедрение зависимостей».
Добавлено: 23.04.2026
