Dependency Injection (DI) один из столпов, на которых держится фреймворк Angular. Каждый разработчик, так или иначе, сталкивается с ним с первого дня: запрашивает сервисы в конструкторе, добавляет providedIn: 'root' и видит, как «магия» работает. Но именно в этом и кроется ловушка.

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

Эта статья не очередной пересказ официальной документации. Это глубокое погружение в архитектуру и философию Dependency Injection в Angular. Наша цель демистифицировать «магию» и превратить ее в предсказуемый, управляемый и мощный инженерный инструмент в вашем арсенале.

Мы пройдем путь от фундаментальных принципов инверсии контроля (IoC) до тонкостей иерархического инжектора. Мы разберем на атомы все стратегии предоставления зависимостей, научимся управлять их жизненным циклом и областью видимости. Мы изучим продвинутые паттерны с использованием InjectionToken и multi-провайдеров и поймем, как современная функция inject() меняет подход к композиции логики.

Фундамент DI. Что, Зачем и Кто?

Прежде чем погружаться в декораторы и иерархии, необходимо заложить фундамент. Он строится на ответах на три вопроса: что такое Dependency Injection (DI), почему этот паттерн является краеугольным камнем современных фреймворков и кто главные действующие лица в этой архитектуре.

Проблема: Нарушение Контракта и Хрупкость Архитектуры

Представьте компонент, которому для работы необходимы данные с сервера. Без использования DI, его реализация могла бы выглядеть следующим образом:

export class DataService {
  constructor() {
    // Некая сложная логика инициализации
    console.log('DataService был создан');
  }

  public getData(): string {
    return 'какие то рандомные данные';
  }
}

export class UserProfileComponent {
  private dataService: DataService;
  private userData: string;

  constructor() {
    // Компонент САМ создает зависимость, вторгаясь в чужую зону ответственности.
    this.dataService = new DataService(); 
    this.userData = this.dataService.getData();
  }
}

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

  1. Нарушение инкапсуляции на уровне архитектуры. Вызывая new DataService(), UserProfileComponent выходит за рамки своих полномочий. Он становится связан не с публичным контрактом сервиса (его методами, такими как getData), а с его деталями реализации (а именно, с его конструктором). Если конструктор DataService изменится например, начнет требовать HttpClient вам придется найти и исправить каждый класс, который его так создает. Компонент знает слишком много о том, как устроен сервис.

  2. Невозможность подмены и тестирования. UserProfileComponent намертво привязан к конкретной реализации DataService. Это делает невозможной простую подмену зависимости на MockDataService во время юнит-тестирования или на AnotherDataService при изменении бизнес-логики. Тестирование такого компонента в изоляции становится нетривиальной задачей.

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

Решение: Принцип Инверсии Контроля (Inversion of Control)

Для решения этих проблем применяется фундаментальный принцип проектирования Инверсия Контроля (Inversion of Control, IoC).

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

Dependency Injection (DI) это конкретный паттерн проектирования, который реализует принцип IoC. Вместо того чтобы компонент сам создавал свои зависимости, он просто декларирует их в своем конструкторе, а фреймворк (та самая внешняя система) «внедряет» их в нужный момент.

Ключевые Акторы в Экосистеме Angular DI

В системе DI от Angular можно выделить три ключевые роли:

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

  2. Потребитель (Consumer). Класс (компонент, директива, сервис), который запрашивает и использует зависимость.

  3. Инжектор (Injector). Центральный механизм Angular. Это DI-контейнер, который выступает в роли «фабрики» и «реестра» зависимостей. Он знает, как их создавать, управляет их жизненным циклом (например, гарантирует, что синглтон будет создан только один раз) и предоставляет их потребителям по запросу.

Давайте посмотрим на переписанный код, который следует принципам DI:

export class UserProfileComponent {
  private userData: string;

  // Мы больше не используем 'new'. Мы декларативно запрашиваем зависимость.
  constructor(private dataService: DataService) { 
    this.userData = this.dataService.getData();
  }
}

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

@Injectable() и Революция providedIn

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

Именно здесь в игру вступает декоратор @Injectable(). Он служит маркером, который сообщает Angular: «Этот класс предназначен для участия в системе Dependency Injection и, что более важно, может иметь собственные зависимости».

Декоратор @Injectable(): Разрешение на Внедрение

@Injectable() // <-- Этот декоратор теперь обязателен
export class DataService {
  // Наш сервис теперь сам зависит от HttpClient и LoggerService
  constructor(
    private http: HttpClient, 
    private logger: LoggerService
  ) {}

  getData(): string {
    this.logger.log('Запрос данных...');
    // ... логика получения данных с помощью http
    return 'Сервер что то вернул, возможно данные)';
  }
}

Без @Injectable() Angular не будет генерировать необходимые метаданные для класса DataService. Следовательно, он не сможет проанализировать его конструктор и внедрить HttpClient и LoggerService, что приведет к ошибке во время выполнения.

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

providedIn: Почему Это Больше, чем Просто Удобство

Самая важная опция декоратора @Injectable() это свойство providedIn. Оно определяет, в каком инжекторе сервис должен быть зарегистрирован по умолчанию. Этот механизм, представленный в Angular 6, был не просто удобством, а небольшим архитектурным переворотом.

До его появления, чтобы сделать сервис глобальным синглтоном, его нужно было явно импортировать и добавлять в массив providers корневого AppModule. Это создавало сильную связь: сервис не мог существовать без модуля, который его регистрирует.

providedIn инвертирует эту связь. Теперь сервис сам декларирует свое место в приложении. Он становится по-настоящему самодостаточным и переиспользуемым.

Ключевое преимущество этого подхода tree-shaking (встряска дерева). Когда сервис регистрируется через providedIn, сборщик (например, Webpack) может определить, используется ли этот сервис в приложении. Если ни один компонент или сервис его не внедряет, код сервиса будет полностью удален из финальной сборки, уменьшая ее размер.

Стратегии Регистрации через providedIn

providedIn: 'root'

Что делает: Регистрирует сервис в корневом инжекторе приложения. Это создает один-единственный экземпляр сервиса на все приложение (паттерн «Синглтон»).

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

providedIn: 'any'

Что делает: Создает синглтон для каждого отдельного «контекста инжектора». На практике это означает, что каждый лениво загружаемый модуль (lazy-loaded module) получит свой собственный, изолированный экземпляр сервиса. Для всех нетерпеливо загружаемых модулей (eager-loaded) экземпляр будет один в корневом инжекторе.

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

providedIn: 'platform'

Что делает: Крайне редкий случай. Регистрирует сервис в инжекторе платформы специальном инжекторе, который является общим для нескольких приложений Angular, запущенных на одной странице (например, при использовании Angular Elements).

Когда использовать: Только при разработке микрофронтендной архитектуры на Angular, где разным приложениям нужен доступ к одному и тому же экземпляру сервиса. В 99.9% стандартных приложений это не требуется.

Архитектура Иерархического Инжектора, Дерево Зависимостей

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

Концептуальная Модель Дерева Инжекторов

Представьте свое приложение как иерархическую структуру. На вершине находится прародитель Root Injector. Он создается при запуске приложения и содержит все сервисы, зарегистрированные глобально (например, через providedIn: 'root').

Далее, по мере того как Angular создает элементы вашего приложения, он формирует дочерние инжекторы, создавая иерархию. Эта иерархическая структура позволяет регистрировать зависимости на разных уровнях, тем самым создавая для них разные области видимости (scopes). Например, сервис, зарегистрированный в ParentComponent, будет доступен самому ParentComponent и всем его дочерним компонентам, но не будет доступен его «соседям» или родительским элементам.

Путь Поиска (Resolution Path): Как Angular Находит Зависимость

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

  1. Проверить собственный инжектор. Сначала Angular смотрит, зарегистрирована ли зависимость непосредственно в инжекторе самого компонента (в его массиве providers).

  2. Подняться к родителю. Если в собственном инжекторе зависимость не найдена, Angular поднимается на один уровень вверх к инжектору родительского компонента и повторяет поиск.

  3. Повторять до корня. Этот процесс продолжается вверх по иерархии, от инжектора к инжектору, пока зависимость не будет найдена.

  4. Финальная остановка Root Injector. Если поиск дошел до самого верха и зависимость найдена в корневом инжекторе отлично, она будет предоставлена.

  5. Ошибка! Если даже в Root Injector зависимость не найдена, Angular прекращает поиск и выбрасывает знаменитую ошибку NullInjectorError: No provider for SomeService!. Этот сигнал означает, что Angular прошел весь путь по дереву до самого корня, но ни один инжектор не был «научен», как создавать запрошенный сервис.

viewProviders: Граница Инкапсуляции для DI

В декораторе @Component можно найти два свойства для регистрации зависимостей: providers и viewProviders. Разница между ними тонкая, но критически важная для создания надежных и инкапсулированных компонентов. Она связана с проекцией контента (Content Projection) через <ng-content>.

  • providers: Зависимость, зарегистрированная здесь, доступна как самому компоненту (и его дочерним элементам в шаблоне), так и любому контенту, спроецированному извне через <ng-content>.

  • viewProviders: Зависимость, зарегистрированная здесь, доступна только самому компоненту и его «родным» дочерним элементам (тем, что определены в его шаблоне). Она невидима для спроецированного контента.

Когда это нужно? viewProviders это ваш инструмент для создания архитектурной инкапсуляции на уровне DI. Его можно рассматривать как private для зависимостей.

Представьте, что вы создаете сложный компонент <custom-form-field>. Для внутренней координации между меткой, полем ввода и сообщениями об ошибках вы используете служебный FormFieldStateService.

@Component({
  selector: 'custom-form-field',
  template: `
    <label>...</label>
    <ng-content></ng-content> <!-- Сюда пользователь вставит свой <input> -->
    <div class="errors">...</div>
  `,
  // Этот сервис - внутренняя деталь реализации. Он не должен утекать наружу.
  viewProviders: [FormFieldStateService]
})
export class CustomFormFieldComponent { /* ... */ }

Используя viewProviders, вы гарантируете, что никакой другой компонент, который пользователь может передать внутрь через <ng-content>, не сможет случайно внедрить и нарушить работу вашего внутреннего FormFieldStateService. Это защищает ваш компонент от «загрязнения зависимостями» и делает его публичный API более надежным и предсказуемым. Это особенно важно при разработке UI-библиотек.

Паттерны Предоставления, Стратегии для Инжектора

Мы знаем, что инжектор ищет зависимости вверх по дереву. Но как именно мы «регистрируем» зависимость на определенном уровне? Для этого существует массив providers в декораторах @Component или @NgModule.

Простая регистрация, например providers: [DataService], является лишь сокращенной записью для более полного и гибкого синтаксиса. Каждая запись в массиве providers это объект-«рецепт» с двумя ключами: provide, который является токеном поиска, и один из четырех ключей-стратегий: useClass, useValue, useExisting или useFactory.

Стратегии Предоставления Зависимостей:

useClass: Паттерн «Стратегия»

Что делает: Указывает инжектору: «Когда кто-то запросит зависимость по токену A, создай и верни новый экземпляр класса B».

Стратегическое применение: Это реализация классического паттерна проектирования «Стратегия» на уровне DI. Идеально подходит для подмены реализаций. Основные сценарии:

Тестирование: Подмена реального ApiService на MockApiService в тестовом окружении.

Полиморфное поведение: Предоставление разных реализаций общего абстрактного класса или интерфейса (Logger) в зависимости от окружения или конфигурации (ConsoleLogger для разработки, HttpLogger для продакшена).

// В компоненте
providers: [
  { provide: Logger, useClass: AdvancedLogger }
]
// В конструкторе
constructor(private logger: Logger) {
  // Сюда будет внедрен экземпляр AdvancedLogger
}

useValue: Для Неизменяемых Констант

Что делает: Указывает инжектору: «Когда кто-то запросит зависимость по токену A, не создавай ничего нового, просто верни вот это готовое значение B».

Стратегическое применение: Основное назначение внедрение неизменяемых (immutable) значений: объектов конфигурации, feature-флагов, API-ключей.

Антипаттерн: Использовать useValue для предоставления изменяемых объектов, которые модифицируются в рантайме. Это может привести к непредсказуемому поведению и состоянию гонки. Если значение должно быть вычислено или зависит от других сервисов, useValue неверный выбор. Используйте useFactory.

export const APP_CONFIG = new InjectionToken<any>('app.config');

providers: [
  { provide: APP_CONFIG, useValue: { apiUrl: '/api/v2', retries: 3 } }
]

useExisting: Паттерн «Адаптер»

Что делает: Создает псевдоним (алиас). Указывает инжектору: «Когда кто-то запросит токен A, не создавай ничего нового, а найди и верни уже существующий экземпляр, зарегистрированный по токену B».

Стратегическое применение: Это паттерн «Адаптер» или «Фасад» для DI. Его главная цель обеспечение обратной совместимости во время масштабного рефакторинга. Если вы переименовали OldLogger в NewLogger, вы можете создать алиас, чтобы старые компоненты, все еще запрашивающие OldLogger, прозрачно получали экземпляр NewLogger, избегая массовых правок в кодовой базе.

providers: [
  NewLogger, // Сначала регистрируем новый сервис, чтобы он существовал
  { provide: OldLogger, useExisting: NewLogger } // Затем создаем для него алиас
]

useFactory: Для Динамического Создания

Что делает: Самая мощная стратегия. Указывает инжектору: «Когда кто-то запросит токен A, выполни вот эту функцию-фабрику B и верни то, что она вернет».

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

// Фабрика может сама запрашивать зависимости
export function dataServiceFactory(logger: LoggerService, config: AppConfig) {
  return config.isLoggingEnabled ? new DataService(logger) : new DataService(new SilentLogger());
}

providers: [{ 
  provide: DataService, 
  useFactory: dataServiceFactory,
  deps: [LoggerService, APP_CONFIG] // Явно указываем зависимости для фабрики
}]

Модификаторы Поиска: Управление Поведением Инжектора

Это декораторы, которые размещаются перед зависимостью в конструкторе и уточняют, как инжектор должен ее искать.

  • @Optional(): «Попробуй найти эту зависимость. Если не найдешь не выбрасывай ошибку, просто верни null». Используется для необязательных зависимостей.

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

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

  • @Host(): Специализированный модификатор, который ограничивает поиск «вверх» границами хост-компонента. пример: кастомная директива-валидатор, которая должна зарегистрировать себя в NgForm своего хоста.

@Directive({ selector: '[myRequiredField]' })
export class RequiredFieldDirective {
  // Директива требует, чтобы ее хост (`<input>`) находился внутри `ngForm`
  constructor(@Host() private form: NgForm) {
    // @Host гарантирует, что мы получим именно ту форму, к которой
    // относится этот инпут, а не какую-то другую выше по дереву.
    // Это делает директиву надежной и предсказуемой.
  }
}

Продвинутый Инструментарий, InjectionToken и Архитектура Плагинов

До сих пор мы в основном использовали имена классов в качестве токенов для инъекции. Это просто и интуитивно: constructor(private dataService: DataService) {}. Но эта модель ломается, когда мы хотим внедрить не экземпляр класса, а что-то другое: объект конфигурации, строковое значение, функцию или флаг.

Использование строкового литерала (provide: 'RETRY_COUNT') это антипаттерн, порождающий две проблемы:

  1. Коллизии имен: Две разные библиотеки могут случайно использовать один и тот же строковый токен, что приведет к непредсказуемым конфликтам.

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

Для элегантного решения этих проблем и был создан InjectionToken.

InjectionToken: Типизированные Токены для Не-классовых Зависимостей

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

Как это работает:

Создание Токена: Токены принято выносить в отдельный файл (app.tokens.ts), чтобы их можно было импортировать по всему приложению.

// Создаем токен, указывая его тип <number> и уникальное описание для отладки.
export const RETRY_COUNT = new InjectionToken<number>('app.retry.count');

Регистрация Провайдера: Используем созданный токен в качестве ключа provide.

// В @Component или @NgModule
providers: [
  { provide: RETRY_COUNT, useValue: 5 }
]

Внедрение Зависимости: Используем декоратор @Inject(), чтобы указать Angular, какой именно токен нужно найти.

constructor(@Inject(RETRY_COUNT) private retryCount: number) {
  // this.retryCount строго типизирован как number и равен 5
}

Продвинутый InjectionToken: Самодостаточные Провайдеры

InjectionToken может быть еще мощнее. При создании ему можно передать фабрику по умолчанию, превратив его в самостоятельный, tree-shakable провайдер.

export const USER_PREFERENCES = new InjectionToken<Preferences>('user.preferences', {
  providedIn: 'root', // Будет зарегистрирован в корневом инжекторе
  factory: () => {
    // Эта фабрика будет вызвана для создания значения по умолчанию
    const savedPrefs = localStorage.getItem('prefs');
    return savedPrefs ? JSON.parse(savedPrefs) : { theme: 'dark', lang: 'ru' };
  }
});

Теперь вам даже не нужно регистрировать его в providers! Можно сразу внедрять USER_PREFERENCES в любом сервисе или компоненте, и Angular сам выполнит фабрику при первом запросе.

Правило Близости: Как Angular Разрешает Конфликты Провайдеров

Это фундаментальный закон иерархии DI. Что произойдет, если у токена USER_PREFERENCES есть своя фабрика, но мы регистрируем для него другой провайдер в компоненте?

Правило: Провайдер, зарегистрированный на более низком уровне иерархии (в компоненте или ленивом модуле), всегда имеет приоритет над провайдером, зарегистрированным выше по дереву (включая providedIn-конфигурацию токена).

@Component({
  providers: [
    // Мы ПЕРЕОПРЕДЕЛЯЕМ глобальное поведение для этого компонента и его дочерних элементов
    { provide: USER_PREFERENCES, useValue: { theme: 'light', lang: 'fr' } } 
  ]
})
export class MySpecialComponent {
  prefs = inject(USER_PREFERENCES); // Получит { theme: 'light', lang: 'fr' }
}

Внутри MySpecialComponent и его дочерних элементов инъекция вернет локальное значение. В остальной части приложения будет использоваться значение из глобальной фабрики. Это мощнейший механизм для локального переопределения зависимостей и тонкой настройки поведения.

multi: true: Архитектура Расширяемости (Плагинов)

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

Для этого существует опция multi: true. Она изменяет поведение инжектора с «заменить» на «добавить в массив». Это основа для создания архитектуры плагинов в Angular.

Классический пример HTTP_INTERCEPTORS.

providers: [
  { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]

// В вашем LoggingModule
providers: [
  { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }
]

AuthModule и LoggingModule ничего не знают друг о друге. Они оба просто «вносят свой вклад» в коллекцию по токену HTTP_INTERCEPTORS. Когда HttpClient будет создан, он запросит эту зависимость и получит массив из двух инстансов: [AuthInterceptor, LoggingInterceptor]. Этот паттерн невероятно гибок и позволяет создавать системы, которые можно расширять, просто добавляя новые модули.

Продвинутые Техники, Граничные Случаи и Парадигмы

Мы освоили фундамент, иерархию и паттерны DI. Настало время рассмотреть особые случаи, современные подходы и компромиссы, которые делают систему DI в Angular по-настоящему завершенной.

Иногда два класса могут ссылаться друг на друга, создавая циклическую зависимость. ParentService зависит от ChildService, а ChildService, в свою очередь, от ParentService.

@Injectable()
export class ParentService {
  constructor(private childService: ChildService) {} // ОШИБКА: ChildService еще не определен
}

Когда компилятор TypeScript обрабатывает parent.service.ts, он встречает ChildService в конструкторе, но определение этого класса еще не было загружено. Это приводит к ReferenceError.

Для разрыва этого цикла на этапе компиляции Angular предоставляет утилиту forwardRef(). Она оборачивает ссылку на класс в функцию-замыкание, которую Angular выполнит "лениво", только когда зависимость действительно понадобится для инъекции. К тому моменту все классы уже будут определены.

@Injectable()
export class ParentService {
  constructor(
    @Inject(forwardRef(() => ChildService)) // <-- Ссылка разрешится позже
    private childService: ChildService
  ) {}
}

Используйте forwardRef() с крайней осторожностью. Часто это является «запахом кода» (code smell), сигнализирующим о том, что архитектура ваших классов слишком запутана или нарушен принцип однонаправленного потока зависимостей. Прежде чем прибегать к forwardRef(), всегда задайте вопрос: «Можно ли перепроектировать эти классы, чтобы разорвать цикл?» Иногда вынесение общей логики в третий, независимый сервис, является более чистым решением.

inject(): Парадигма DI за Пределами Конструктора

радиционно внедрение зависимостей было привилегией конструктора класса. Функция inject(), появившаяся в Angular 14, это не просто синтаксический сахар для уменьшения кода, а фундаментальный сдвиг парадигмы. Ее главная ценность возможность получать зависимости внутри контекста инъекции, что открывает дорогу к созданию мощных композитных функций (composition functions).

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

Пример: От класса к композитной функции

// Классический подход
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree {
    return this.authService.isAdmin() ? true : this.router.createUrlTree(['/forbidden']);
  }
}

С inject() мы можем полностью избавиться от класса:

// Современный функциональный подход
// Это не метод класса, это самодостаточная композитная функция
export const canActivateAdmin: CanActivateFn = (route, state) => {
  const authService = inject(AuthService); // Получаем сервис без конструктора
  const router = inject(Router);

  return authService.isAdmin() ? true : router.createUrlTree(['/forbidden']);
};

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

Заключение: От Механизма к Философии

Мы прошли полный путь: от фундаментального вопроса «зачем?» до продвинутых техник, таких как multi-провайдеры и функция inject(). Мы увидели, что Dependency Injection в Angular это не просто механизм для получения экземпляров классов. Это глубокая и гибкая система, основанная на философии Inversion of Control, которая формирует саму архитектуру приложения.

Освоение DI это переход от мышления «как мне получить этот сервис?» к мышлению «каков контракт у этой зависимости и кто должен управлять её жизненным циклом?».

Давайте закрепим ключевые принципы, которые отличают экспертное использование Dependency Injection:

  1. DI это про контракты, а не про классы. Ваш компонент не должен знать, как создается зависимость. Он должен зависеть только от её публичного API.

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

  3. Думайте паттернами. За useClass, useExisting, multi: true стоят классические паттерны проектирования «Стратегия», «Адаптер», «Плагин». Распознавание этих паттернов позволяет вам выбирать правильный инструмент для конкретной архитектурной задачи.

  4. Стремитесь к слабой связанности. Используйте InjectionToken для не-классовых зависимостей и forwardRef только в крайних случаях, рассматривая его как сигнал к возможному рефакторингу.

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

Комментарии (0)