Декоратори та рефлектор
Давайте почнемо з очевидного - TypeScript-синтаксис частково відрізняється від JavaScript-синтаксису, бо має можливості для статичної типізації. Під час компіляції TypeScript-коду у JavaScript-код компілятор може надати додатковий JavaScript-код, який можна використовувати для отримання інформації про статичні типи.
Давайте спробуємо проекспериментувати. Створіть файл src/app/services.ts
в репозиторію ditsmod/rest-starter, та вставте у нього наступний код:
class Service1 {}
class Service2 {
constructor(service1: Service1) {}
}
Як бачите, в конструкторі Service2
вказано статичний тип даних для параметра service1
. Якщо запустити команду:
npm run build
TypeScript-код скомпілюється і попаде у файл dist/app/services.js
. Він матиме такий вигляд:
class Service1 {
}
class Service2 {
constructor(service1) { }
}
Тобто інформація про тип параметра в конструкторі Service2
втрачена. Але якщо ми використаємо декоратор класу, TypeScript-компілятор вивантажить більше JavaScript-коду з інформацією про статичну типізацію. Наприклад, давайте скористаємось декоратором injectable
:
import { injectable } from '@ditsmod/core';
class Service1 {}
@injectable()
class Service2 {
constructor(service1: Service1) {}
}
Тепер за допомогою команди npm run build
TypeScript-компілятор перетворює цей код на наступний JavaScript-код і вставляє його у dist/app/services.js
:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { injectable } from '@ditsmod/core';
class Service1 {
}
let Service2 = class Service2 {
constructor(service1) { }
};
Service2 = __decorate([
injectable(),
__metadata("design:paramtypes", [Service1])
], Service2);
На щастя, проглядати теку dist
та аналізувати скомпільований код вам навряд чи прийдеться часто, але для загального уявлення про механізм перенесення статичної типізації у JavaScript-код, інколи буває корисним глянути на нього. Найцікавіша частина знаходиться в останніх чотирьох рядках. Очевидно, що TypeScript-компілятор тепер пов'язує масив [Service1]
із Service2
. Цей масив - це і є інформація про статичні типи параметрів, знайдені компілятором в конструкторі Service2
.
Подальший аналіз скомпільованого коду вказує нам, що для збереження метаданих зі статичною типізацію використовується клас Reflect
. Передбачається що цей клас імпортується з бібліотеки reflect-metadata. API даної бібліотеки потім використовується Ditsmod, щоб зчитувати вищенаведені метадані. Цим займається так званий рефлектор.
Давайте поглянемо, які інструменти має Ditsmod для роботи з рефлектором. Ускладнимо попередній приклад, щоб побачити як можна витягувати метадані та формувати складні ланцюжки залежностей. Розглянемо три класи з наступною залежністю Service3
-> Service2
-> Service1
. Вставте у src/app/services.ts
наступний код:
import { injectable, getDependencies } from '@ditsmod/core';
class Service1 {}
@injectable()
class Service2 {
constructor(service1: Service1) {}
}
@injectable()
class Service3 {
constructor(service2: Service2) {}
}
console.log(getDependencies(Service3)); // [ { token: [class Service2], required: true } ]
Функція getDependencies()
використовує рефлектор і повертає масив безпосередніх залежностей Service3
. Мабуть ви здогадуєтесь, що передавши Service2
до getDependencies()
, ми побачимо залежність від Service1
. Таким чином можна автоматично скласти весь ланцюжок залежностей Service3
-> Service2
-> Service1
. Такий процес в DI називають "вирішенням залежностей". І тут слово "автоматично" спеціально виділено жирним шрифтом, бо це дуже важлива фіча, яку підтримує DI. Користувачі передають до DI всього лише Service3
, і їм не треба вручну досліджувати від чого цей клас залежить, DI може вирішити залежність автоматично. До речі, користувачам навряд чи прийдеться користуватись функцією getDependencies()
, за виключенням окремих рідких випадків.
Строго кажучи, механізм збереження та отримання метаданих від рефлектора за допомогою декораторів - це ще не Dependecy Injection. Але Dependecy Injection широко використовує декоратори та рефлектор у своїй роботі, тому інколи в цій документації може говоритись, що DI отримує інформацію про залежності класу, хоча насправді за це відповідає рефлектор.
Код в останньому прикладі можна скомпілювати та запустити наступною командою:
tput reset && npm run build && node dist/app/services.js
Щоб після кожної зміни код автоматично виконувався, можна скористатись двома терміналами. У першому терміналі можна запустити команду для компіляції коду:
npm run build -- --watch
А в другому терміналі можна запустити команду для запуску скопільованого коду:
node --watch dist/app/services.js
Тепер, якщо у src/app/services.ts
, у функцію getDependencies()
передати Service1
, через пару секунд у другому терміналі ви повинні побачити вивід [ { token: [class Service1], required: true } ]
.