Розбираємо таємницю Angular Dependency Injection

Усі статті, обговорення, новини про Front-end — в одному місці. Підписуйтеся на телеграм-канал!

Вітаю! Мене звуть Карен, і я фронтенд-розробник у такій доброзичливій компанії як Langate. Якщо ви хоч раз використовували Angular 2+, то ви на 99% стикалися з такою штукою як Dependency Injection. Це дуже важлива тема для Angular розробників і я спробую в короткий час пояснити, що воно є. У сфері веброзробки Angular заслужив репутацію надійного та багатофункціонального інтерфейсного середовища. Одним з його основних стовпів є Dependency Injection (DI), шаблон проєктування, який сприяє модульності коду, тестованості та ремонтопридатності. DI — це концепція, у якій клас чи компонент отримує свої залежності із зовнішніх джерел, а не створює їх чи управляє ними всередині. У цій статті ми заглибимося у світ Angular Dependency Injection, вивчаючи його основні принципи, реалізацію та переваги, які він пропонує. У статті будуть порушені такі теми:

  • ієрархічний DI;
  • DI у дії;
  • типи DI в Angular;
  • недоліки відмови від DI;
  • декоратори DI.

Ієрархічний DI

В Angular, дерево компонентів програми утворює ієрархічну структуру, в якій компоненти можуть бути вкладені одна в одну. Кожен компонент може мати власну систему DI, звану інжектором. Коли компонент або сервіс запитує залежність, система DI Angular спочатку шукає запрошеного провайдера в інжекторі компонента. Якщо провайдер не знайдений, він просувається вгору по дереву компонентів, доки досягне кореневого інжектора, шукаючи необхідного провайдера на кожному рівні. На вершині ієрархії інжекторів знаходиться інжектор рівня програми, що часто називається root injector. Цей injector створюється при запуску програми Angular і відповідає за керування singleton сервісами всієї програми та іншими глобальними залежностями. Кожен модуль Angular також має свій injector, так званий injector модуля. Інжектори рівня модуля керують провайдерами, зареєстрованими в цьому конкретному модулі, інкапсулюючи їх з інших частин програми. Коли компонент або сервіс створюється в модулі, він використовує injector модуля для вирішення своїх залежностей. Ієрархічний DI особливо корисний у таких сценаріях:

  • Спільне використання сервісів: коли сервіс повинен використовуватися кількома компонентами у певному модулі, її доцільно надавати лише на рівні модуля. Таким чином, всі компоненти в модулі спільно використовують один і той самий екземпляр сервісу.
  • Component-Specific сервіси: якщо сервіс тісно пов’язаний з конкретним компонентом і повинен мати унікальний екземпляр для кожного компонента та його дочірніх елементів, він повинен надаватися на рівні компонента.
  • Глобальні сервіси: для сервісів рівня програми, які мають бути singleton і підтримувати global state, надання їх на root level (інжектор рівня програми) гарантує, що у них буде один екземпляр у всьому застосунку.

Dependency Injection In Action

Щоб краще зрозуміти DI у дії, розгляньмо, як воно забезпечує модульність, тестованість та гнучкість у додатках Angular. 1. Модульність та можливість повторного використання: відокремлюючи компоненти від своїх залежностей, Angular спрощує повторне використання коду. Компоненти можна легко повторно використовувати в різних частинах програми або навіть у кількох проєктах, що призводить до створення зручніших у супроводі та масштабованих кодових баз. 2. Тестування: DI Angular відіграє вирішальну роль у спрощенні модульного тестування. Під час тестування ми можемо надати користувацькі реалізації або макети для окремих залежностей, ізолюючи модуль, що тестується, і забезпечуючи надійні сценарії тестування. 3. Гнучкість: завдяки DI компонентам можна легко розширювати або замінювати іншими реалізаціями. Ця гнучкість покращує еволюцію коду і забезпечує захист від майбутніх змін, оскільки оновлення чи зміни в залежностях можуть виконуватися без впливу на основну логіку компонента.

Типи DI в Angular

Система DI надає розробникам різні підходи для DI у компоненти та служби. Ці різні типи DI призначені для різних сценаріїв та переваг у кодуванні. Розглянемо докладно кожен вид: Впровадження конструктора: у цьому типі, залежності впроваджуються через конструктор компонента. Коли компонент створюється, Angular переглядає параметри конструктора й автоматично дозволяє необхідні залежності, знаходячи відповідних провайдерів. Ось приклад застосування конструктора:

import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-example',
  template: '<p>{{ message }}</p>',,
  providers: [DataService]
})
export class ExampleComponent {
  message: string;

  constructor(private dataService: DataService) {
    this.message = dataService.getData();
  }
}

У цьому прикладі для ExampleComponent потрібна залежність DataService і вона автоматично впроваджується через конструктор. Interface-Based Injection: ангуляр також підтримує впровадження з урахуванням інтерфейсу, коли залежності надаються через інтерфейси. Такий підхід сприяє кращій організації коду та підвищує гнучкість вашої програми. Ось приклад використання через інтерфейс:

import { Component } from '@angular/core';
import { IDataService } from './data.service.interface';

@Component({
  selector: 'app-example',
  template: '<p>{{ message }}</p>',
  providers: [{ provide: IDataService, useClass: DataService }]
})
export class ExampleComponent {
  message: string;

  constructor(private dataService: IDataService) {
    this.message = dataService.getData();
  }
}

У цьому прикладі ми визначили інтерфейс IDataService для представлення методів DataService. Потім ми надаємо токен IDataService і зв’язуємо його з класом DataService, використовуючи синтаксис {provide: IDataService, useClass: DataService}. Тепер, коли ExampleComponent запитує IDataService, Angular автоматично перетворює його на екземпляр DataService. Токени можуть бути рядками або об’єктами і зареєстровані в масиві providers з певним провайдером. Детальніше можна почитати тут. Використовуючи Inject(): починаючи з Angular v14, ми також можемо використовувати функцію inject() у компонентах, директивах. Він відкриває цілий новий світ можливостей. Використовуючи систему DI, ми можемо створювати повторні функції. У сервісі:

@Injectable({
    providedIn: “root”
})
export class MyService {}

У компоненті:

@Component({ ... })
export class MyComponent {
    myService = inject(MyService);
}

Більше про Inject() можна почитати тут.

Недоліки відмови від DI

Нездатність використовувати DI може призвести до кількох недоліків: 1. Тісний зв’язок: без DI компоненти стають тісно пов’язаними зі своїми залежностями, що ускладнює їхнє повторне використання або заміну. 2. Дублювання коду: відсутність модульності може призвести до дублювання коду в програмі. 3. Складне тестування: модульне тестування стає складним, коли залежність тісно інтегрована в компоненти.

Декоратори

Angular надає кілька декораторів, які спрощують та покращують використання DI у компонентах, сервісах та інших класах. Ці декоратори допомагають системі DI автоматично ідентифікувати та впроваджувати правильні залежності. Розгляньмо деякі з ключових декораторів: 1. @Injectable: декоратор `@Injectable` застосовується до класу, щоб зробити його придатним для DI. Коли клас позначений як @Injectable, Angular може впроваджувати його екземпляри як залежність в інші класи, які їх запитують. Приклад:

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

   @Injectable()
   export class DataService {
     // Service Implementation
   }

2. @Inject: декоратор @Inject використовується для вказівки маркера впровадження, пов’язаного із залежністю, коли вона впроваджується. Цей декоратор дозволяє налаштувати дозвіл залежності, якщо є кілька провайдерів. Приклад:

 import { Component, Inject } from '@angular/core';
 import { DATA_SERVICE_TOKEN } from './data-service.token';
 import { DataService } from './data.service';

   @Component({
     selector: 'app-example',
     template: '<p>{{ message }}</p>',
   })
   export class ExampleComponent {
     message: string;

     constructor(@Inject(DATA_SERVICE_TOKEN) private dataService: DataService) {
       this.message = dataService.getData();
     }
   }

3. @Self: декоратор @Self обмежує пошук провайдера власним інжектором компонента. Він каже Angular не проходити вгору ієрархічним деревом інжекторів для дозволу залежності, гарантуючи, що провайдер буде знайдений тільки всередині самого компонента. Приклад:

  import { Component, Inject, Self } from '@angular/core';
  import { DataService } from './data.service';

   @Component({
     selector: 'app-example',
     template: '<p>{{ message }}</p>',
     providers: [DataService]
   })
   export class ExampleComponent {
     message: string;

     constructor(@Self() private dataService: DataService) {
       this.message = dataService.getData();
     }
   }

4. @Optional: декоратор `@Optional` вказує, що залежність є необов’язковою, і якщо її неможливо знайти, Angular не випустить помилку під час впровадження. Натомість він надасть нульове значення для залежності. Приклад:

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

   @Component({
     selector: 'app-example',
     template: '<p>{{ message }}</p>',
   })
   export class ExampleComponent {
     message: string;

     constructor(@Optional() private loggerService: LoggerService) {
       if (this.loggerService) {
         this.message = this.loggerService.getLogMessage();
       } else {
         this.message = 'Logger service is not available.';
       }
     }
   }

5.@SkipSelf: декоратор @SkipSelf використовується для обходу інжектора поточного компонента та пошуку провайдера в батьківських інжекторах (рух вгору ієрархічним деревом). Цей декоратор особливо корисний, коли ви маєте кілька вкладених інжекторів, і ви хочете отримати доступ до провайдера з інжектора вищого рівня, а не з того, що надається на рівні компонента. Приклад:

import { Component, Inject, SkipSelf } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-child',
  template: '<p>{{ message }}</p>',
  providers: [DataService]
})
export class ChildComponent {
  message: string;

  constructor(@SkipSelf() private dataService: DataService) {
    this.message = dataService.getData();
  }
}

Висновок

Angular Dependency Injection — це фундаментальна концепція, що дозволяє розробникам створювати надійні, модульні та масштабовані застосунки. Застосовуючи DI як шаблон проєктування, розробники можуть забезпечити чистішу кодову базу, ефективне тестування та підвищену гнучкість. Розуміння різних типів DI та ієрархічної системи DI Angular, а також декораторів і токенів впровадження, дозволяє розробникам використовувати весь потенціал DI. Вирушаючи в подорож по Angular, пам’ятайте, що Dependency Injection — це потужний інструмент у вашому розпорядженні, що полегшує розробку високоякісних вебзастосунків. Із задоволенням відповім на ваші коментарі та запитання.

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Класна стаття, ще можна доповнити до @Self, @SkipSelf @Optional — @Host, який працює схожим чином до @Self, але якщо не знаходить у поточному інжекторі компонента то йде дивитись до батьківського інжектора тобто хоста, тобто вказує на те, що об’єкт слід ініціалізувати в контексті батьківського компонента або модуля, а не в контексті самого компонента. :)

Доброго дня. Багато пропущено фундаментальних і базових понять про Injector hierarchy. Коли ви розписуєте про декоратори як

@Injectable(), @Self

і т.д., розкажіть про те як ангурял знаходить ваші депенденсі. Про це на жаль ні слова в статті. Ієрархія інджекторів Root Injector -> Platform Injector (Created hen we call method platformBrowserDynamic()) -> NullInjector — зовсім не розписана. Рекомендую подивитись відео www.youtube.com/...​9sfmJ6AaZj9eDlAKrJrEul4Vz яке вірдазу розставить все по своїм місцям. Про

@Inject

зовсім не розписано, хоча цей декоратор — це складова при написанні складних ангуляр сайтів. Просто якраз таки за допомогою @Inject, ніхто не інджектить сервіси як ви описали. Так часто інджектять реальні дані (наприклад environment змінні можна заінджектити і т.д.)

Доброго дня, дякую за коментар та за пропозицію. Буду мати на увазі.

Підписатись на коментарі