JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Внедрение зависимостей через конструктор компонента

Внедрение зависимостей через конструктор компонента

Модуль 4: Node.js, Next.js и Angular
15 уровень , 3 лекция
Открыта

1. Внедрение зависимостей (Dependency Injection) в Angular

Если вы когда-нибудь собирали мебель из IKEA, то знаете: в инструкции всегда написано, какие детали и инструменты понадобятся для сборки. Примерно так же работает и Angular — только вместо шурупов и шестигранников у нас сервисы и другие зависимости.

Внедрение зависимостей (Dependency Injection, DI) — это паттерн, при котором объект (например, компонент) не сам создаёт свои зависимости, а получает их "снаружи". Это делает код гибким, тестируемым и избавляет от "жёсткой связки" между классами.

В Angular DI — один из краеугольных камней архитектуры. Когда вы хотите использовать сервис в компоненте, вы не делаете new MyService(). Вместо этого вы просите Angular: "Дай мне, пожалуйста, экземпляр этого сервиса". Angular сам решит, где его взять, когда создать и кому ещё его отдать.

Зачем внедрять сервисы через конструктор?

Можно было бы сделать так:

import { MyService } from './my.service';

@Component({ /* ... */ })
export class MyComponent {
  myService = new MyService(); // Плохо!
}

Но это плохая практика. Почему?

  • Тестируемость: Такой компонент невозможно подменить на тестовый сервис при юнит-тестах.
  • Повторное использование: Если сервис зависит от других сервисов, их придётся создавать вручную.
  • Жизненный цикл: Angular не сможет управлять созданием и уничтожением сервисов.
  • Синглтоны: Каждый раз будет новый экземпляр, а не общий на всё приложение.

Вместо этого Angular предлагает внедрять зависимости через параметры конструктора компонента. Это и есть "внедрение через конструктор".

2. Как это выглядит в коде?

Давайте разберёмся на простом примере.

Создаём сервис

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class LoggerService {
  log(message: string) {
    console.log('[LoggerService]', message);
  }
}

Внедряем сервис в компонент через конструктор

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app-root',
  template: `<h1>Привет, Angular!</h1>`
})
export class AppComponent {
  // Внедряем сервис через конструктор
  constructor(private logger: LoggerService) {
    this.logger.log('AppComponent создан!');
  }
}

Ключевые моменты:

  • В конструкторе компонента указываем параметр с модификатором private (или public, или protected — на ваш вкус).
  • Angular сам найдёт нужный сервис и передаст его в конструктор.
  • Не нужно делать new LoggerService() — Angular всё сделает за вас.

Аналогия

Это как если бы вы пришли в кофейню и сказали: "Мне, пожалуйста, капучино". Бариста (Angular) сам решит, кто будет готовить кофе, из каких зёрен и с каким молоком. Вам не нужно идти на склад и варить кофе самому!

3. Как Angular находит и создаёт сервисы?

Когда вы просите Angular "внедрить" сервис, он ищет его в так называемом инжекторе. Это специальный объект, который знает, как создавать сервисы и где они объявлены.

Порядок поиска:

  1. Сначала Angular смотрит, не объявлен ли сервис в массиве providers текущего компонента.
  2. Если не найден — идёт выше по иерархии компонентов.
  3. Если нигде не найден — обращается к корневому инжектору (providedIn: 'root').

Результат:
- Если сервис объявлен в providedIn: 'root', экземпляр будет один на всё приложение (синглтон).
- Если сервис объявлен в providers компонента — будет отдельный экземпляр для этого компонента и его потомков.

4. Пример: внедрение нескольких сервисов

Допустим, у нас есть два сервиса: LoggerService и UserService.

@Injectable({ providedIn: 'root' })
export class UserService {
  getUser() {
    return { name: 'Вася', id: 123 };
  }
}

В компоненте можно внедрить оба сервиса:

@Component({ /* ... */ })
export class ProfileComponent {
  constructor(
    private logger: LoggerService,
    private userService: UserService
  ) {
    const user = this.userService.getUser();
    this.logger.log(`Пользователь: ${user.name}`);
  }
}

Вопрос: Можно ли внедрять сколько угодно сервисов?
Ответ: Да! Angular не против. Главное — не переборщить, иначе конструктор станет похож на список покупок перед Новым годом.

5. Полезные нюансы

Внедрение сервисов только для чтения

Когда вы указываете параметр конструктора с модификатором private, Angular автоматически создаёт поле этого класса и присваивает ему сервис. Это удобно, если вы хотите использовать сервис внутри класса.

Если вы хотите использовать сервис только внутри конструктора (например, для инициализации), можете не указывать модификатор доступа:

constructor(logger: LoggerService) {
  logger.log('Компонент создан');
}

В этом случае Angular всё равно передаст сервис в конструктор, но поле класса не будет создано.

Внедрение сервисов в других сервисах

Сервисы могут зависеть друг от друга. Это называется цепочка зависимостей.

@Injectable({ providedIn: 'root' })
export class AnalyticsService {
  constructor(private logger: LoggerService) {}

  trackEvent(event: string) {
    this.logger.log(`Событие: ${event}`);
  }
}

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

6. Практика: добавляем сервис в наше учебное приложение

Допустим, мы разрабатываем ToDo-приложение (куда же без него!). У нас есть сервис для работы со списком задач:

@Injectable({ providedIn: 'root' })
export class TodoService {
  private todos: string[] = [];

  add(todo: string) {
    this.todos.push(todo);
  }

  getAll() {
    return this.todos;
  }
}

В компоненте внедряем сервис:

@Component({
  selector: 'app-todo-list',
  template: `
    <input [(ngModel)]="newTodo">
    <button (click)="addTodo()">Добавить</button>
    <ul>
      <li *ngFor="let todo of todos">{{ todo }}</li>
    </ul>
  `
})
export class TodoListComponent {
  newTodo = '';
  todos: string[] = [];

  constructor(private todoService: TodoService) {}

  ngOnInit() {
    this.todos = this.todoService.getAll();
  }

  addTodo() {
    if (this.newTodo.trim()) {
      this.todoService.add(this.newTodo);
      this.todos = this.todoService.getAll();
      this.newTodo = '';
    }
  }
}

Что здесь происходит:

  • Сервис TodoService внедряется через конструктор.
  • Вся логика работы со списком задач — в сервисе.
  • Компонент только отображает данные и реагирует на действия пользователя.

7. Типичные ошибки при внедрении зависимостей через конструктор

Ошибка №1: Забыли указать сервис в декораторе @Injectable или не добавили его в providers

Если забыть декоратор @Injectable() или не указать сервис в массиве providers (если он не с providedIn: 'root'), Angular не сможет создать сервис и выдаст ошибку:
NullInjectorError: No provider for MyService!

Ошибка №2: Использование new вместо DI

Если вы пишете const s = new MyService(), Angular не сможет управлять этим экземпляром, и вы лишаетесь всех преимуществ DI: не будет синглтона, не будет цепочки зависимостей, не будет моков в тестах.

Ошибка №3: Несоответствие области видимости

Если вы случайно объявили сервис в providers компонента, а ожидали синглтон — получите отдельный экземпляр для этого компонента. Иногда это сюрприз, особенно при работе с состоянием.

Ошибка №4: Не используете модификатор доступа в конструкторе, но хотите обращаться к сервису через this

Если вы пишете constructor(logger: LoggerService), а потом в методах класса пишете this.logger.log(), получите ошибку: такого поля нет. Нужно писать private logger: LoggerService.

Ошибка №5: Внедряете сервис, который зависит от другого, но забыли указать зависимости

Если сервис A зависит от сервиса B, а B не инжектируется (нет в @Injectable или providers), получите ошибку при запуске приложения.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ