Внедрение зависимостей: inject() и конструктор

Вы не создаёте зависимости сами — вы их запрашиваете, а Angular приносит готовые. Это и есть внедрение зависимостей.

«Не звоните нам — мы позвоним вам». Компонент не строит сервисы, он лишь объявляет, что они нужны».

Внедрение зависимостей (DI) — сердце Angular. Идея проста: вместо того чтобы компонент сам создавал нужные ему сервисы, он объявляет зависимости, а специальный инжектор подставляет готовые экземпляры. Это снижает связанность: компонент не знает, как именно собран сервис, и его легко подменить в тестах.

Современный способ запросить зависимость — функция inject():

import { Component, inject } from '@angular/core';
import { CartService } from './cart.service';

@Component({
  selector: 'app-buy-button',
  standalone: true,
  template: `
    <button (click)="onBuy()">Купить</button>
    <span>В корзине: {{ cart.total }}</span>
  `,
})
export class BuyButtonComponent {
  private cart = inject(CartService);   // запросили — получили
  onBuy() { this.cart.add('Кофемолка'); }
}

Альтернатива — классическое внедрение через конструктор, оно тоже работает:

export class BuyButtonComponent {
  constructor(private cart: CartService) {}
}

Оба варианта эквивалентны: Angular видит тип CartService, находит его в инжекторе и подставляет тот самый общий экземпляр. inject() предпочтительнее в современном коде — он гибче и работает в большем числе контекстов.

Как работает под капотом

Инжектор — это, по сути, реестр: карта «токен → как создать экземпляр». Когда компонент просит CartService, инжектор смотрит, есть ли уже созданный экземпляр; если нет — создаёт, разрешая попутно его собственные зависимости, кеширует и отдаёт. Инжекторы образуют иерархию: запрос идёт вверх по дереву, пока не найдётся провайдер.

   inject(CartService)
        |
        v
   ИНЖЕКТОР: есть экземпляр? --да--> вернуть кешированный
        |
        нет
        |
   создать new CartService(...зависимости...) -> кеш -> вернуть

Запускаемая врезка: DI-контейнер на Map

Сделаем игрушечный инжектор, чтобы увидеть суть. «Попробуй сам ▶».

// мини DI-контейнер: реестр фабрик + кеш синглтонов
function createInjector() {
  const factories = new Map();
  const cache = new Map();
  return {
    provide(token, factory) { factories.set(token, factory); },
    inject(token) {
      if (cache.has(token)) return cache.get(token);   // синглтон
      const factory = factories.get(token);
      if (!factory) throw new Error('Нет провайдера: ' + token);
      const instance = factory(this);   // фабрика может звать inject
      cache.set(token, instance);
      return instance;
    },
  };
}

const di = createInjector();
di.provide('CartService', () => ({ items: [], add(x){ this.items.push(x); } }));

const a = di.inject('CartService');
const b = di.inject('CartService');
a.add('Кофемолка');

console.log('один экземпляр?', a === b); // true
console.log('видно из b:', b.items);     // ["Кофемолка"]

Частые ошибки

  • Звать inject() вне контекста. Его можно вызывать в инициализаторах полей и конструкторе, но не внутри произвольного метода.
  • Циклические зависимости. A зависит от B, B — от A: инжектор не сможет разрешить.
  • Внедрять то, что не помечено @Injectable и не имеет провайдера.

Best practices

  • В новом коде предпочитайте inject() — он лаконичнее и работает в функциях вроде гвардов и резолверов.
  • Делайте поля с зависимостями private, если они не нужны в шаблоне.
  • Не злоупотребляйте локальными провайдерами — большинству сервисов хватает providedIn: 'root'.

Итоги. DI избавляет компонент от создания зависимостей: он их объявляет, инжектор подставляет. inject(Service) — современный способ, конструктор — классический. Под капотом инжектор кеширует синглтоны и разрешает их по иерархии. Дальше — провайдеры и токены.

Закрепляем

Внедрение зависимостей переворачивает привычный порядок: вместо того чтобы компонент сам создавал нужные ему объекты, он лишь объявляет, что они нужны, а инжектор приносит готовые. Этот «принцип Голливуда» — «не звоните нам, мы позвоним вам» — снижает связанность кода. Компонент не знает и не должен знать, как именно собран сервис и какие у того свои зависимости; он просто получает рабочий экземпляр. А значит, в тестах сервис легко подменить на фейковый, не трогая компонент.

Запомните два равноценных способа запросить зависимость. Функция inject(Service) — современный, лаконичный и более гибкий вариант, который работает не только в классах, но и в функциях вроде гвардов маршрутов и резолверов. Внедрение через параметр конструктора — классический способ, знакомый по старому коду. Оба дают один и тот же экземпляр из инжектора, поэтому выбор между ними — вопрос стиля; в новом коде стоит предпочитать inject(). Под капотом же инжектор просто ведёт реестр и кеширует синглтоны, разрешая запросы по иерархии снизу вверх.

СпособКогда использовать
inject(Service)Современный код, функции, гварды
constructor(private s: Service)Классический стиль
ИнжекторРеестр и кеш экземпляров
Проверьте себя
1. В чём суть внедрения зависимостей (DI)?
AКомпонент сам создаёт все сервисы через new
BКомпонент объявляет нужные зависимости, а инжектор подставляет готовые экземпляры
CЗависимости загружаются с сервера
DВсе сервисы хранятся в одном файле
2. Чем inject(CartService) отличается от внедрения через конструктор?
AРезультат другой
BРезультат тот же — оба получают тот же экземпляр из инжектора; inject() лишь гибче и современнее
Cinject() создаёт новый экземпляр каждый раз
DКонструктор работает только в сервисах