Декоратори і рефлексія в TypeScript: від новачка до експерта (ч. 3)



Ця стаття — третя частина серії:


Минулого разу ми дізналися, що таке декоратори і як вони реалізовані в TypeScript. Ми знаємо, як працювати з декораторами класів, властивостей і методів.

У цій статті ми розповімо про:
  • Останній залишився тип декораторів декоратор
  • Реалізацію фабрики декораторів
  • Реалізацію конфігурованих декораторів


Ми будемо використовувати такий клас для демонстрації даних концепцій:
class Person { 

public name: string;
public surname: string;

constructor(name : string, surname : string) { 
this.name = name;
this.surname = surname;
}

public saySomething(something : string) : string { 
return this.name + " " + this.surname + " says: " + something; 
}
}

Декоратори параметрів

Як ми вже знаємо, сигнатура декоратора параметра виглядає наступним чином:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;


Використання декоратора під назвою
logParameter
буде виглядати так:
class Person { 

public name: string;
public surname: string;

constructor(name : string, surname : string) { 
this.name = name;
this.surname = surname;
}

public saySomething(@logParameter something : string) : string { 
return this.name + " " + this.surname + " says: " + something; 
}
}


При компіляції JavaScript викликається метод
__decorate
(про нього ми говорили першої частини).
Object.defineProperty(Person.prototype, "saySomething",
__decorate([
__param(0, logParameter)
], Person.prototype, "saySomething", Object.getOwnPropertyDescriptor(Person.prototype, "saySomething")));
return Person;


За аналогією з попередніми типами декораторів, ми можемо припустити, що раз викликається метод
Object.defineProperty
, метод
saySomething
буде замінений результатом виклику функції
__decorate
(як у декораторе методу). Це припущення невірно.

Якщо уважно подивитися на код вище, можна помітити, що там є нова функція
__param
. Вона була сгенерирована компілятором TypeScript і виглядає наступним чином:
var __param = this.__param || function (index, decorator) {

// return a decorator function (wrapper)
return function (target, key) {

// apply decorator (return is ignored)
decorator(target, key, index); 
}
};


Функція
__param
повертає декоратор, який обертає декоратор, переданий на вхід (з ім'ям
decorator
).

Можна помітити, що коли декоратор параметра викликається, його значення ігнорується. Це означає, що при виклику функції
__decorate
, результат її виконання не перевизначити метод
saySomething
.

Тому декоратори параметрів нічого не повертають.

Обернення декоратора
__param
використовується, щоб зберегти індекс (позицію декорируемого параметра в списку аргументів) в замиканні.
class foo {
// foo index === 0
public foo(@logParameter foo: string) : string { 
return "bar"; 
}
// bar index === 1
public foobar(foo: string, @logParameter bar: string) : string { 
return "foobar"; 
}
}


Тепер ми знаємо, що декоратор параметра приймає 3 аргументи:
  • Прототип декорируемого класу
  • Назва методу, що містить декоруємі параметр
  • Індекс декорируемого


Давайте реалізуємо
logProperty

function logParameter(target: any, key : string, index : number) {
var metadataKey = `log_${key}_parameters`;
if (Array.isArray(target[metadataKey])) {
target[metadataKey].push(index);
}
else { 
target[metadataKey] = [index];
}
}


Декоратор параметра, описаний вище, додає нову властивість (
metadataKey
) прототип класу. Це властивість — масив, що містить індекси декорируемых параметрів. Ми можемо вважати це властивість метаданими.

Передбачається, що декоратор параметр не використовується для модифікації поведінки конструктора, методу або властивості. Декоратори параметрів повинні використовуватися тільки для створення різних метаданих.

Як тільки метадані створені, ми можемо використовувати інший декоратор для їх читання. Наприклад, нижче наведена модифікована версія декоратора методу другий частини статті.

Вихідна версія виводила в консоль назва методу і всі її аргументи при виклику. Нова версія читає метадані, і на їх основі виводить тільки ті аргументи, які позначені відповідним декоратором параметра.
class Person { 

public name: string;
public surname: string;

constructor(name : string, surname : string) { 
this.name = name;
this.surname = surname;
}

@logMethod
public saySomething(@logParameter something : string) : string { 
return this.name + " " + this.surname + " says: " + something; 
}
}

function logMethod(target: Function key: string, descriptor: any) {
var originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {

var metadataKey = `__log_${key}_parameters`;
var indices = target[metadataKey];

if (Array.isArray(indices)) { 

for (var i = 0; i < args.length; i++) { 

if (indices.indexOf(i) !== -1) { 

var arg = args[i];
var argStr = JSON.stringify(arg) || arg.toString();
console.log(`${key} arg[${i}]: ${argStr}`);
}
}
var result = originalMethod.apply(this, args);
return result;
}
else {

var a = args.map(a => (JSON.stringify(a) || a.toString())).join();
var result = originalMethod.apply(this, args);
var r = JSON.stringify(result);
console.log(`Call: ${key}(${a}) => ${r}`);
return result;
}
}
return descriptor;
}


У наступній частині ми дізнаємося найкращий спосіб роботи з метаданими: Metadata Reflection API. Ось невеликий приклад того, що ми вивчимо:
function logParameter(target: any, key: string, index: number) {
var indices = Reflect.getMetadata(`log_${key}_parameters`, target, key) || [];
indices.push(index); 
Reflect.defineMetadata(`log_${key}_parameters`, indices, target, key);
}

Фабрика декораторів

Офіційний proposal декораторів в TypeScript дає наступне визначення фабрики декораторів:
Фабрика декораторів — це функція, яка може приймати будь-яку кількість аргументів і повертає декоратор одного з типів.


Ми вже навчилися реалізовувати та використовувати всі типи декораторів (класу, методи, властивості і параметри), але дещо ми можемо покращити. Припустимо, у нас є такий фрагмент коду:
@logClass
class Person { 

@logProperty
public name: string;

public surname: string;

constructor(name : string, surname : string) { 
this.name = name;
this.surname = surname;
}

@logMethod
public saySomething(@logParameter something : string) : string { 
return this.name + " " + this.surname + " says: " + something; 
}
}


Він працює, як належить, але було б краще, якщо б можна було скрізь використовувати один і той же декоратор, не піклуючись про його тип, як у цьому прикладі:
@log
class Person { 

@log
public name: string;

public surname: string;

constructor(name : string, surname : string) { 
this.name = name;
this.surname = surname;
}

@log
public saySomething(@log something : string) : string { 
return this.name + " " + this.surname + " says: " + something; 
}
}


Домогтися цього ми можемо, обернувши декоратори на фабрику. Фабрика може визначити тип необхідної декоратора по аргументам, переданим в неї:
function log(...args : any[]) {
switch(args.length) {
case 1:
return logClass.apply(this, args);
case 2:
return logProperty.apply(this, args);
case 3:
if(typeof args[2] === "number") {
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
default:
throw new Error("Decorators are not valid here!");
}
}

Конфігуровані декоратори

Останній момент, який хотілося б обговорити в цій статті, це те, як ми можемо передавати аргументи в декоратор при його використанні.
@logClassWithArgs({ when : { name : "remo"} })
class Person { 
public name: string;

// ...
}


Ми можемо скористатися фабрикою декораторів для створення конфігурованих декораторів:
function logClassWithArgs(filter: Object) {
return (target: Object) => {
// реалізація декоратора класу буде тут, декоратор
// буде мати доступ до параметрів декоратора (filter),
// тому що вони зберігаються в замиканні
}
}


Ту ж ідею ми можемо застосувати для інших типів декораторів.

Висновок

Тепер у нас є глибоке розуміння всіх чотирьох існуючих видів декораторів, як створювати фабрики декораторів і як їх завдний.

У наступній статті ми дізнаємося, як використовувати Metadata Reflection API.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.