Перейти до основного вмісту

Декоратори та рефлектор

Давайте почнемо з очевидного - 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 } ].