Dependency Injection
Підготовка
В наступних прикладах даного розділу припускається, що ви клонували репозиторій ditsmod/rest-starter. Це дасть вам змогу отримати базову конфігурацію для застосунку та експериментувати у теці src/app даного репозиторію.
Окрім цього, якщо ви ще не знаєте, що саме робить рефлектор і що таке "вирішення залежностей", рекомендуємо вам спочатку прочитати попередній розділ Декоратори та рефлектор.
Інжектор, токени та провайдери
В контексті Dependency Injection (DI) часто говорять про інжектори (їх також називають контейнерами), токени, сервіси та провайдери. Якщо для вас ці терміни є новими, вам мають допомогти наступні асоціації з р еального життя:
- Токен — Хоча це слово у побуті часто означає жетон для метро або фішку в грі, у програмуванні (зокрема в DI) воно працює як квиток у камеру схову. Тобто в DI токен - це ідентифікатор, за яким можна знайти потрібне значення.
- Провайдер - В побутовому житті цей термін звичний. Провайдер - це компанія, яка надає певний сервіс або товар. Наприклад, провайдер може надавати послуги доступу до інтернету, і разом з тим - продавати вам модеми. В Dependency Injection цей термін хоча й близький до побутового значення, але має деяку відмінність, оскільки під "Провайдером" розуміють не просто "Компанію, що надає сервіс або товар", а радше - "Контракт, де записано назву товару, і яка компанія надає цей товар". В технічному плані, найпростіше уявляти провайдер, наприклад, у вигляді такого об'єкту:
Тобто це інструкція, яка говорить: "Коли запитують значення за таким токеном - потрібно видати ось це значення".
{ token: 'some-token', useValue: 'some-value' } - Інжектор - цей термін у побуті також використовується рідко, але його можна уявити як "Центр замовлень", який виступає посередником між провайдерами та споживачами. Спочатку провайдери укладають "Контракти" з "Центром замовлень", а потім споживачі звертаються до цих центрів замовлень з "Назвами товарів" щоб отримати відповідний товар. В технічному плані, спочатку провайдери реєструють в інжекторах, а потім споживачі звертаються з токенами до інжекторів щоб отримати відповідне значення.
Давайте тепер завершимо спрощену аналогію з побутового життя, і повернемось до технічних деталей. Далі всі терміни використовуються вже у технічному сенсі.
У попередньому розділі ми побачили як в конструкторі можна вказувати залежність одного класу від іншого класу, а також як можна автоматично визначити ланцюжок залежностей за допомогою рефлектора. В контексті Dependency Injection такі класи часто називають сервісами. Тепер давайте познайомимось ближче з інжектором - механізмом, який дозволяє отримувати інстанси сервісів, з врахуванням їхніх залежностей. Інжектор працює дуже просто: приймає токен і повертає значення, пов'язане з цим токеном. Очевидно, що для такої функціональності потрібні інструкції між тим, що запитують в інжектора, і тим що він видає. Такі інструкції забезпечуються так званими провайдерами.
Давайте розглянемо наступний приклад, який трохи розширює останній приклад з розділу Декоратори та рефлектор:
import { Injector, injectable } from '@ditsmod/core';
class Service1 {}
@injectable()
class Service2 {
constructor(service1: Service1) {}
}
@injectable()
class Service3 {
constructor(service2: Service2) {}
}
const injector = Injector.resolveAndCreate([
{ token: Service1, useClass: Service1 },
{ token: Service2, useClass: Service2 },
{ token: Service3, useClass: Service3 }
]);
const service3 = injector.get(Service3); // інстанс Service3
service3 === injector.get(Service3); // true
Як бачите, метод Injector.resolveAndCreate() на вході приймає масив провайдерів, а на виході видає інжектор, який вміє створювати інстанс кожного переданого класу за допомогою методу injector.get(), з урахуванням усього ланцюжка залежностей (Service3 -> Service2 -> Service1).
Отже, які задачі стоять перед інжектором, і що робить його метод injector.get():
- Під час створення інжектора, йому передається масив провайдерів - тобто масив інструкцій між тим, що у нього запитують (токеном), та тим, що він повинен видавати (значенням). Цей етап є дуже важливим для подальшого функціонування інжектора. Якщо ви не передасте усіх необхідних провайдерів, інжектор не матиме відповідних інструкцій, коли ви будете запитувати певний токен.
- Після створення інжектора, коли у нього запитують токен
Service3, він проглядає масив провайдерів і бачить там інструкцію{ token: Service3, useClass: Service3 }, тому він "розуміє", що для токенаService3треба видавати інстанс класуService3. - Потім він проглядає конструктор класу
Service3, бачить залежність відService2. - Далі інжектор проглядає свій масив провайдерів і бачить там інструкцію
{ token: Service2, useClass: Service2 }, тому він "розуміє", що для токенаService2треба видавати інстанс класуService2. - Потім проглядає конструктор у
Service2, бачить залежність відService1. - Далі інжектор проглядає масив провайдерів і бачить там інструкцію
{ token: Service1, useClass: Service1 }, тому він "розуміє", що для токенаService1треба видавати інстанс класуService1. - Потім проглядає конструктор у
Service1, не знаходить там залежності, і тому першим створює інстансService1. - Потім створює інстанс
Service2використовуючи інстансService1. - І останнім створює інстанс
Service3використовуючи інстансService2. - Якщо пізніше будуть запитувати повторно інстанс
Service3, методinjector.get()буде повертати раніше створений інстансService3з кешу даного інжектора.
У підсумку ми можемо констатувати, що injector.get() дійсно працює дуже просто: приймає токен Service3, і видає його значення - інстанс класу Service3. Але щоб так діяти, інжектор, по-перше, бере до уваги наданий йому масив провайдерів. По-друге, він враховує ланцюжок залежностей кожного із провайдерів.
До речі, два наступні інжектори отримують еквівалентні провайдери:
const injector1 = Injector.resolveAndCreate([
{ token: Service1, useClass: Service1 },
{ token: Service2, useClass: Service2 },
{ token: Service3, useClass: Service3 }
]);
const injector2 = Injector.resolveAndCreate([
Service1,
Service2,
Service3
]);
Давайте тепер порушимо пункт 1, і спробуємо під час створення інжектора передати йому пустий масив. В такому разі виклик injector.get() кидатиме помилку:
const injector = Injector.resolveAndCreate([]);
const service3 = injector.get(Service3); // Error: No provider for Service3!
Як і варто було очікувати, коли ми передаємо пустий масив замість масиву провайдерів, а потім запитуємо в інжектора токен Service3, інжектор кидає помилку вимагаючи провайдера для даного токена.
Щоб краще зрозуміти якими можуть бути провайдери, давайте передамо інжектору масив провайдерів в наступній формі:
import { Injector } from '@ditsmod/core';
class Service1 {}
class Service2 {}
class Service3 {}
class Service4 {}
const injector = Injector.resolveAndCreate([
{ token: Service1, useValue: 'value for Service1' },
{ token: Service2, useClass: Service2 },
{ token: Service3, useFactory: () => 'value for Service3' },
{ token: Service4, useToken: Service3 },
]);
injector.get(Service1); // value for Service1
injector.get(Service2); // інстанс Service2
injector.get(Service3); // value for Service3
injector.get(Service4); // value for Service3
Зверніть увагу, що у цьому прикладі не використовується декоратор injectable, оскільки кожен представлений тут клас не має конструктора, де б можна було вказати залежності.