Изследвайте силата на TypeScript декораторите за метапрограмиране, аспектно-ориентирано програмиране и подобряване на кода с декларативни шаблони. Изчерпателно ръководство за глобални разработчици.
TypeScript Декоратори: Овладяване на метапрограмни шаблони за стабилни приложения
В необятния пейзаж на съвременното разработване на софтуер, поддържането на чисти, мащабируеми и управляеми кодови бази е от първостепенно значение. TypeScript, със своята мощна типова система и разширени функции, предоставя на разработчиците инструменти за постигането на това. Сред най-интригуващите и трансформиращи характеристики са Декораторите. Въпреки че все още са експериментална функция към момента на писане (предложение от Етап 3 за ECMAScript), декораторите се използват широко във фреймуърци като Angular и TypeORM, като фундаментално променят начина, по който подхождаме към дизайнерски шаблони, метапрограмиране и аспектно-ориентирано програмиране (AOP).
Това изчерпателно ръководство ще навлезе дълбоко в TypeScript декораторите, изследвайки тяхната механика, различни типове, практически приложения и най-добри практики. Независимо дали изграждате мащабни корпоративни приложения, микроуслуги или интерфейси за уеб приложения от страна на клиента, разбирането на декораторите ще ви даде възможност да пишете по-декларативен, поддържан и мощен TypeScript код.
Разбиране на основната концепция: Какво е декоратор?
В основата си, декораторът е специален вид декларация, която може да бъде прикачена към декларация на клас, метод, аксесор, свойство или параметър. Декораторите са функции, които връщат нова стойност (или модифицират съществуваща) за целта, която украсяват. Тяхната основна цел е да добавят метаданни или да променят поведението на декларацията, към която са прикачени, без да променят директно основната структура на кода. Този външен, декларативен начин за обогатяване на кода е невероятно мощен.
Мислете за декораторите като за анотации или етикети, които прилагате към части от вашия код. Тези етикети след това могат да бъдат прочетени или обработени от други части на вашето приложение или от фреймуърци, често по време на изпълнение, за да предоставят допълнителна функционалност или конфигурация.
Синтаксис на декоратор
Декораторите са префикснати със символа @
, последван от името на функцията-декоратор. Те се поставят непосредствено преди декларацията, която украсяват.
@MyDecorator
class MyClass {
@AnotherDecorator
myMethod() {
// ...
}
}
Активиране на декоратори в TypeScript
Преди да можете да използвате декоратори, трябва да активирате опцията на компилатора experimentalDecorators
във вашия файл tsconfig.json
. Освен това, за разширени възможности за метарефлексия (често използвани от фреймуърци), ще ви трябват и emitDecoratorMetadata
и полифилът reflect-metadata
.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Също така трябва да инсталирате reflect-metadata
:
npm install reflect-metadata --save
# или
yarn add reflect-metadata
И да го импортирате в самия връх на входната точка на вашето приложение (например, main.ts
или app.ts
):
import "reflect-metadata";
// Вашият код на приложението следва
Декораторски фабрики: Персонализация на една ръка разстояние
Докато основният декоратор е функция, често ще трябва да подавате аргументи на декоратор, за да конфигурирате поведението му. Това се постига чрез използване на декораторска фабрика. Декораторската фабрика е функция, която връща действителната функция-декоратор. Когато прилагате декораторска фабрика, вие я извиквате с нейните аргументи, а тя след това връща функцията-декоратор, която TypeScript прилага към вашия код.
Пример за създаване на проста декораторска фабрика
Нека създадем фабрика за декоратор Logger
, която може да записва съобщения с различни префикси.
function Logger(prefix: string) {
return function (target: Function) {
console.log(`[${prefix}] Клас ${target.name} е дефиниран.`);
};
}
@Logger("APP_INIT")
class ApplicationBootstrap {
constructor() {
console.log("Приложението стартира...");
}
}
const app = new ApplicationBootstrap();
// Изход:
// [APP_INIT] Клас ApplicationBootstrap е дефиниран.
// Приложението стартира...
В този пример, Logger("APP_INIT")
е извикване на декораторска фабрика. Тя връща действителната функция-декоратор, която приема target: Function
(конструкторът на класа) като аргумент. Това позволява динамична конфигурация на поведението на декоратора.
Типове декоратори в TypeScript
TypeScript поддържа пет различни типа декоратори, всеки приложим за специфичен вид декларация. Сигнатурата на функцията-декоратор варира в зависимост от контекста, към който е приложена.
1. Класови декоратори
Класовите декоратори се прилагат към декларации на класове. Функцията-декоратор получава конструктора на класа като единствен аргумент. Класовият декоратор може да наблюдава, модифицира или дори да замени декларацията на класа.
Сигнатура:
function ClassDecorator(target: Function) { ... }
Връщана стойност:
Ако класовият декоратор върне стойност, той ще замени декларацията на класа с предоставения конструктор. Това е мощна функция, често използвана за миксини или обогатяване на класове. Ако не бъде върната стойност, се използва оригиналният клас.
Случаи на употреба:
- Регистриране на класове в контейнер за инжектиране на зависимости.
- Прилагане на миксини или допълнителни функционалности към клас.
- Конфигурации, специфични за фреймуърк (напр. маршрутизиране във фреймуърк за уеб приложения).
- Добавяне на куки за жизнен цикъл към класове.
Пример за класов декоратор: Инжектиране на услуга
Представете си прост сценарий за инжектиране на зависимости, при който искате да маркирате клас като "инжектируем" и по избор да предоставите име за него в контейнер.
const InjectableServiceRegistry = new Map<string, Function>();
function Injectable(name?: string) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
const serviceName = name || constructor.name;
InjectableServiceRegistry.set(serviceName, constructor);
console.log(`Регистрирана услуга: ${serviceName}`);
// По избор, можете да върнете нов клас тук, за да обогатите поведението
return class extends constructor {
createdAt = new Date();
// Допълнителни свойства или методи за всички инжектирани услуги
};
};
}
@Injectable("UserService")
class UserDataService {
getUsers() {
return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
}
}
@Injectable()
class ProductDataService {
getProducts() {
return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];
}
}
console.log("--- Регистрирани услуги ---");
console.log(Array.from(InjectableServiceRegistry.keys()));
const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
const userServiceInstance = new userServiceConstructor();
console.log("Потребители:", userServiceInstance.getUsers());
// console.log("UserService Създаден в:", userServiceInstance.createdAt); // Ако се използва върнатият клас
}
Този пример демонстрира как класов декоратор може да регистрира клас и дори да модифицира неговия конструктор. Декораторът Injectable
прави класа откриваем от теоретична система за инжектиране на зависимости.
2. Декоратори на методи
Декораторите на методи се прилагат към декларации на методи. Те получават три аргумента: целевия обект (за статични членове, конструкторната функция; за членове на екземпляр, прототипът на класа), името на метода и дескриптора на свойството на метода.
Сигнатура:
function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Връщана стойност:
Декораторът на метод може да върне нов PropertyDescriptor
. Ако го направи, този дескриптор ще бъде използван за дефиниране на метода. Това ви позволява да модифицирате или замените оригиналната имплементация на метода, което го прави невероятно мощен за AOP.
Случаи на употреба:
- Записване на извиквания на методи и техните аргументи/резултати.
- Кеширане на резултати от методи за подобряване на производителността.
- Прилагане на проверки за оторизация преди изпълнение на метод.
- Измерване на времето за изпълнение на методи.
- Debouncing или throttling на извиквания на методи.
Пример за декоратор на метод: Мониторинг на производителността
Нека създадем декоратор MeasurePerformance
за записване на времето за изпълнение на метод.
function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const start = process.hrtime.bigint();
const result = originalMethod.apply(this, args);
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1_000_000;
console.log(`Метод "${propertyKey}" изпълнен за ${duration.toFixed(2)} ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@MeasurePerformance
processData(data: number[]): number[] {
// Симулиране на сложна, отнемаща време операция
for (let i = 0; i < 1_000_000; i++) {
Math.sin(i);
}
return data.map(n => n * 2);
}
@MeasurePerformance
fetchRemoteData(id: string): Promise<string> {
return new Promise(resolve => {
setTimeout(() => {
resolve(`Данни за ID: ${id}`);
}, 500);
});
}
}
const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));
Декораторът MeasurePerformance
обвива оригиналния метод с логика за време, като отпечатва продължителността на изпълнение, без да претрупва бизнес логиката в самия метод. Това е класически пример за Аспектно-ориентирано програмиране (AOP).
3. Декоратори на аксесори
Декораторите на аксесори се прилагат към декларации на аксесори (get
и set
). Подобно на декораторите на методи, те получават целевия обект, името на аксесора и неговия дескриптор на свойството.
Сигнатура:
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Връщана стойност:
Декораторът на аксесор може да върне нов PropertyDescriptor
, който ще бъде използван за дефиниране на аксесора.
Случаи на употреба:
- Валидация при задаване на свойство.
- Трансформиране на стойност, преди да бъде зададена или след като е получена.
- Контролиране на разрешенията за достъп до свойства.
Пример за декоратор на аксесор: Кеширане на Getters
Нека създадем декоратор, който кешира резултата от скъпо изчисление на getter.
function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalGetter = descriptor.get;
const cacheKey = `_cached_${String(propertyKey)}`;
if (originalGetter) {
descriptor.get = function() {
if (this[cacheKey] === undefined) {
console.log(`[Cache Miss] Изчисляване на стойност за ${String(propertyKey)}`);
this[cacheKey] = originalGetter.apply(this);
} else {
console.log(`[Cache Hit] Използване на кеширана стойност за ${String(propertyKey)}`);
}
return this[cacheKey];
};
}
return descriptor;
}
class ReportGenerator {
private data: number[];
constructor(data: number[]) {
this.data = data;
}
// Симулира скъпа изчислителна операция
@CachedGetter
get expensiveSummary(): number {
console.log("Извършва се скъпо изчисление на резюмето...");
return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
}
}
const generator = new ReportGenerator([10, 20, 30, 40, 50]);
console.log("Първи достъп:", generator.expensiveSummary);
console.log("Втори достъп:", generator.expensiveSummary);
console.log("Трети достъп:", generator.expensiveSummary);
Този декоратор гарантира, че изчислението на getter expensiveSummary
се изпълнява само веднъж, последващите извиквания връщат кешираната стойност. Този шаблон е много полезен за оптимизиране на производителността, когато достъпът до свойство включва тежки изчисления или външни извиквания.
4. Декоратори на свойства
Декораторите на свойства се прилагат към декларации на свойства. Те получават два аргумента: целевия обект (за статични членове, конструкторната функция; за членове на екземпляр, прототипът на класа) и името на свойството.
Сигнатура:
function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }
Връщана стойност:
Декораторите на свойства не могат да връщат стойност. Основната им употреба е да регистрират метаданни за свойството. Те не могат директно да променят стойността на свойството или неговия дескриптор по време на декориране, тъй като дескрипторът за свойство все още не е напълно дефиниран, когато се изпълняват декораторите на свойства.
Случаи на употреба:
- Регистриране на свойства за сериализация/десериализация.
- Прилагане на правила за валидация към свойства.
- Задаване на стойности по подразбиране или конфигурации за свойства.
- ORM (Object-Relational Mapping) картографиране на колони (напр.
@Column()
в TypeORM).
Пример за декоратор на свойство: Валидация на задължително поле
Нека създадем декоратор, за да маркираме свойство като "задължително" и след това да го валидираме по време на изпълнение.
interface ValidationRule {
property: string | symbol;
validate: (value: any) => boolean;
message: string;
}
const validationRules: Map<Function, ValidationRule[]> = new Map();
function Required(target: Object, propertyKey: string | symbol) {
const rules = validationRules.get(target.constructor) || [];
rules.push({
property: propertyKey,
validate: (value: any) => value !== null && value !== undefined && value !== "",
message: `${String(propertyKey)} е задължително.`
});
validationRules.set(target.constructor, rules);
}
function validate(instance: any): string[] {
const classRules = validationRules.get(instance.constructor) || [];
const errors: string[] = [];
for (const rule of classRules) {
if (!rule.validate(instance[rule.property])) {
errors.push(rule.message);
}
}
return errors;
}
class UserProfile {
@Required
firstName: string;
@Required
lastName: string;
age?: number;
constructor(firstName: string, lastName: string, age?: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
const user1 = new UserProfile("John", "Doe", 30);
console.log("Грешки при валидация на user1:", validate(user1)); // []
const user2 = new UserProfile("", "Smith");
console.log("Грешки при валидация на user2:", validate(user2)); // ["firstName е задължително."]
const user3 = new UserProfile("Alice", "");
console.log("Грешки при валидация на user3:", validate(user3)); // ["lastName е задължително."]
Декораторът Required
просто регистрира правилото за валидация с централна карта validationRules
. Отделна функция validate
след това използва тези метаданни, за да провери екземпляра по време на изпълнение. Този шаблон отделя логиката за валидация от дефиницията на данните, което я прави повторно използваема и чиста.
5. Декоратори на параметри
Декораторите на параметри се прилагат към параметрите в конструктор или метод на клас. Те получават три аргумента: целевия обект (за статични членове, конструкторната функция; за членове на екземпляр, прототипът на класа), името на метода (или undefined
за параметри на конструктора) и порядъковия индекс на параметъра в списъка с параметри на функцията.
Сигнатура:
function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
Връщана стойност:
Декораторите на параметри не могат да връщат стойност. Подобно на декораторите на свойства, тяхната основна роля е да добавят метаданни за параметъра.
Случаи на употреба:
- Регистриране на типове параметри за инжектиране на зависимости (напр.
@Inject()
в Angular). - Прилагане на валидация или трансформация към специфични параметри.
- Извличане на метаданни за параметри на заявки към API във фреймуърци за уеб приложения.
Пример за декоратор на параметър: Инжектиране на данни от заявка
Нека симулираме как уеб фреймуърк може да използва декоратори на параметри, за да инжектира специфични данни в параметър на метод, като потребителски ID от заявка.
interface ParameterMetadata {
index: number;
key: string | symbol;
resolver: (request: any) => any;
}
const parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();
function RequestParam(paramName: string) {
return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
const targetKey = propertyKey || "constructor";
let methodResolvers = parameterResolvers.get(target.constructor);
if (!methodResolvers) {
methodResolvers = new Map();
parameterResolvers.set(target.constructor, methodResolvers);
}
const paramMetadata = methodResolvers.get(targetKey) || [];
paramMetadata.push({
index: parameterIndex,
key: targetKey,
resolver: (request: any) => request[paramName]
});
methodResolvers.set(targetKey, paramMetadata);
};
}
// Хипотетична функция на фреймуърк за извикване на метод с разрешени параметри
function executeWithParams(instance: any, methodName: string, request: any) {
const classResolvers = parameterResolvers.get(instance.constructor);
if (!classResolvers) {
return (instance[methodName] as Function).apply(instance, []);
}
const methodParamMetadata = classResolvers.get(methodName);
if (!methodParamMetadata) {
return (instance[methodName] as Function).apply(instance, []);
}
const args: any[] = Array(methodParamMetadata.length);
for (const meta of methodParamMetadata) {
args[meta.index] = meta.resolver(request);
}
return (instance[methodName] as Function).apply(instance, args);
}
class UserController {
getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {
console.log(`Извличане на потребител с ID: ${userId}, Токен: ${authToken || "N/A"}`);
return { id: userId, name: "Jane Doe" };
}
deleteUser(@RequestParam("id") userId: string) {
console.log(`Изтриване на потребител с ID: ${userId}`);
return { status: "deleted", id: userId };
}
}
const userController = new UserController();
// Симулиране на входяща заявка
const mockRequest = {
id: "user123",
token: "abc-123",
someOtherProp: "xyz"
};
console.log("\n--- Изпълнение на getUser ---");
executeWithParams(userController, "getUser", mockRequest);
console.log("\n--- Изпълнение на deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });
Този пример показва как декораторите на параметри могат да събират информация за необходимите параметри на методи. Фреймуъркът след това може да използва тези събрани метаданни, за да разрешава и инжектира подходящи стойности автоматично, когато методът бъде извикан, което значително опростява логиката на контролерите или услугите.
Композиране и ред на изпълнение на декораторите
Декораторите могат да бъдат прилагани в различни комбинации, а разбирането на техния ред на изпълнение е от решаващо значение за предсказване на поведението и избягване на неочаквани проблеми.
Множество декоратори върху една цел
Когато множество декоратори се прилагат към една декларация (напр. клас, метод или свойство), те се изпълняват в специфичен ред: отдолу нагоре, или отдясно наляво, за тяхната оценка. Техните резултати обаче се прилагат в обратен ред.
@DecoratorA
@DecoratorB
class MyClass {
// ...
}
Тук DecoratorB
ще бъде оценен пръв, след това DecoratorA
. Ако те модифицират класа (напр. като върнат нов конструктор), модификацията от DecoratorA
ще обвие или приложи върху модификацията от DecoratorB
.
Пример: Верижно свързване на декоратори на методи
Разгледайте два декоратора на методи: LogCall
и Authorization
.
function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] Извикване на ${String(propertyKey)} с аргументи:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] Метод ${String(propertyKey)} върна:`, result);
return result;
};
return descriptor;
}
function Authorization(roles: string[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const currentUserRoles = ["admin"]; // Симулиране на извличане на роли на текущия потребител
const authorized = roles.some(role => currentUserRoles.includes(role));
if (!authorized) {
console.warn(`[AUTH] Достъп отказан за ${String(propertyKey)}. Необходими роли: ${roles.join(", ")}`);
throw new Error("Неоторизиран достъп");
}
console.log(`[AUTH] Достъп предоставен за ${String(propertyKey)}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class SecureService {
@LogCall
@Authorization(["admin"])
deleteSensitiveData(id: string) {
console.log(`Изтриване на чувствителни данни за ID: ${id}`);
return `Данни ID ${id} изтрити.`;
}
@Authorization(["user"])
@LogCall // Редът е променен тук
fetchPublicData(query: string) {
console.log(`Извличане на публични данни с заявка: ${query}`);
return `Публични данни за заявка: ${query}`;
}
}
const service = new SecureService();
try {
console.log("\n--- Извикване на deleteSensitiveData (Администратор Потребител) ---");
service.deleteSensitiveData("record123");
} catch (error: any) {
console.error(error.message);
}
try {
console.log("\n--- Извикване на fetchPublicData (Потребител без администраторски права) ---");
// Симулиране на потребител без администраторски права, който се опитва да достъпи fetchPublicData, което изисква 'user' роля
const mockUserRoles = ["guest"]; // Това ще доведе до неуспех на автентикацията
// За да направим това динамично, ще ни е необходима DI система или статичен контекст за роли на текущия потребител.
// За простота, ще приемем, че декораторът Authorization има достъп до текущия контекст на потребителя.
// Нека коригираме декоратора Authorization, за да приеме винаги 'admin' за целите на демонстрацията,
// така че първото извикване да успее, а второто да се провали, за да се покажат различни пътища.
// Повторно изпълнение с роля 'user' за fetchPublicData, за да успее.
// Представете си, че currentUserRoles в Authorization става: ['user']
// За този пример, нека го запазим просто и да покажем ефекта от реда.
service.fetchPublicData("search term"); // Това ще изпълни Auth -> Log
} catch (error: any) {
console.error(error.message);
}
/* Очакван изход за deleteSensitiveData:
[AUTH] Достъп предоставен за deleteSensitiveData
[LOG] Извикване на deleteSensitiveData с аргументи: [ 'record123' ]
Изтриване на чувствителни данни за ID: record123
[LOG] Метод deleteSensitiveData върна: Данни ID record123 изтрити.
*/
/* Очакван изход за fetchPublicData (ако потребителят има 'user' роля):
[LOG] Извикване на fetchPublicData с аргументи: [ 'search term' ]
[AUTH] Достъп предоставен за fetchPublicData
Извличане на публични данни с заявка: search term
[LOG] Метод fetchPublicData върна: Публични данни за заявка: search term
*/
Забележете реда: за deleteSensitiveData
, Authorization
(отдолу) се изпълнява първо, след това LogCall
(отгоре) го обвива. Вътрешната логика на Authorization
се изпълнява първа. За fetchPublicData
, LogCall
(отдолу) се изпълнява първо, след това Authorization
(отгоре) го обвива. Тази разлика е от решаващо значение за кръстосано-разрязващи се аспекти като записване или обработка на грешки, където редът на изпълнение може значително да повлияе на поведението.
Ред на изпълнение за различни цели
Когато един клас, неговите членове и параметри имат декоратори, редът на изпълнение е ясно дефиниран:
- Декоратори на параметри се прилагат първо, за всеки параметър, започвайки от последния параметър към първия.
- След това се прилагат декоратори на методи, аксесори или свойства за всеки член.
- Накрая, класови декоратори се прилагат към самия клас.
В рамките на всяка категория, множество декоратори върху една и съща цел се прилагат отдолу нагоре (или отдясно наляво).
Пример: Пълен ред на изпълнение
function log(message: string) {
return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
if (typeof descriptorOrIndex === 'number') {
console.log(`Декоратор на параметър: ${message} върху параметър #${descriptorOrIndex} на ${String(propertyKey || "constructor")}`);
} else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
console.log(`Декоратор на метод/аксесор: ${message} върху ${String(propertyKey)}`);
} else {
console.log(`Декоратор на свойство: ${message} върху ${String(propertyKey)}`);
}
} else {
console.log(`Декоратор на клас: ${message} върху ${target.name}`);
}
return descriptorOrIndex; // Връща дескриптор за метод/аксесор, undefined за други
};
}
@log("Клас ниво D")
@log("Клас ниво C")
class MyDecoratedClass {
@log("Статично свойство A")
static staticProp: string = "";
@log("Свойство на екземпляр B")
instanceProp: number = 0;
@log("Метод D")
@log("Метод C")
myMethod(
@log("Параметър Z") paramZ: string,
@log("Параметър Y") paramY: number
) {
console.log("Метод myMethod е изпълнен.");
}
@log("Getter/Setter F")
get myAccessor() {
return "";
}
set myAccessor(value: string) {
//...
}
constructor() {
console.log("Конструкторът е изпълнен.");
}
}
new MyDecoratedClass();
// Извикване на метод за задействане на декоратор на метод
new MyDecoratedClass().myMethod("hello", 123);
/* Предвиден ред на изхода (приблизителен, в зависимост от конкретната версия на TypeScript и компилацията):
Декоратор на параметър: Параметър Y върху параметър #1 на myMethod
Декоратор на параметър: Параметър Z върху параметър #0 на myMethod
Декоратор на свойство: Статично свойство A върху staticProp
Декоратор на свойство: Свойство на екземпляр B върху instanceProp
Декоратор на метод/аксесор: Getter/Setter F върху myAccessor
Декоратор на метод/аксесор: Метод C върху myMethod
Декоратор на метод/аксесор: Метод D върху myMethod
Декоратор на клас: Клас ниво C върху MyDecoratedClass
Декоратор на клас: Клас ниво D върху MyDecoratedClass
Конструкторът е изпълнен.
Метод myMethod е изпълнен.
*/
Точното време на конзолните логове може леко да варира в зависимост от това кога се извиква конструкторът или методът, но редът, в който самите функции-декоратори се изпълняват (и следователно техните странични ефекти или връщани стойности се прилагат), следва горните правила.
Практически приложения и дизайнерски шаблони с декоратори
Декораторите, особено в комбинация с полифила reflect-metadata
, отварят ново царство на метапрограмно програмиране. Това позволява мощни дизайнерски шаблони, които абстрахират повтарящия се код и кръстосано-разрязващите се аспекти.
1. Инжектиране на зависимости (DI)
Една от най-забележителните употреби на декораторите е във фреймуърци за инжектиране на зависимости (като @Injectable()
, @Component()
и т.н. в Angular, или обширната употреба на DI в NestJS). Декораторите ви позволяват да декларирате зависимости директно върху конструктори или свойства, което позволява на фреймуърка автоматично да инстанцира и предоставя правилните услуги.
Пример: Опростено инжектиране на услуги
import "reflect-metadata"; // Задължително за emitDecoratorMetadata
const INJECTABLE_METADATA_KEY = Symbol("injectable");
const INJECT_METADATA_KEY = Symbol("inject");
function Injectable() {
return function (target: Function) {
Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
};
}
function Inject(token: any) {
return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];
existingInjections[parameterIndex] = token;
Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);
};
}
class Container {
private static instances = new Map<any, any>();
static resolve<T>(target: { new (...args: any[]): T }): T {
if (Container.instances.has(target)) {
return Container.instances.get(target);
}
const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);
if (!isInjectable) {
throw new Error(`Клас ${target.name} не е маркиран като @Injectable.`);
}
// Получаване на типовете на параметрите на конструктора (изисква emitDecoratorMetadata)
const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
const explicitInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target) || [];
const dependencies = paramTypes.map((paramType, index) => {
// Използвайте изричен @Inject токен, ако е предоставен, в противен случай изведете типа
const token = explicitInjections[index] || paramType;
if (token === undefined) {
throw new Error(`Не може да се разреши параметър на индекс ${index} за ${target.name}. Възможно е да има кръгова зависимост или примитивен тип без изрично @Inject.`);
}
return Container.resolve(token);
});
const instance = new target(...dependencies);
Container.instances.set(target, instance);
return instance;
}
}
// Дефиниране на услуги
@Injectable()
class DatabaseService {
connect() {
console.log("Свързване към базата данни...");
return "DB Connection";
}
}
@Injectable()
class AuthService {
private db: DatabaseService;
constructor(db: DatabaseService) {
this.db = db;
}
login() {
console.log(`AuthService: Автентикация чрез ${this.db.connect()}`);
return "Потребител влязъл";
}
}
@Injectable()
class UserService {
private authService: AuthService;
private dbService: DatabaseService; // Пример за инжектиране чрез свойство, използвайки персонализиран декоратор или функция на фреймуърк
constructor(@Inject(AuthService) authService: AuthService,
@Inject(DatabaseService) dbService: DatabaseService) {
this.authService = authService;
this.dbService = dbService;
}
getUserProfile() {
this.authService.login();
this.dbService.connect();
console.log("UserService: Извличане на потребителски профил...");
return { id: 1, name: "Global User" };
}
}
// Разрешаване на основната услуга
console.log("--- Разрешаване на UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());
console.log("\n--- Разрешаване на AuthService (трябва да е кеширано) ---");
const authService = Container.resolve(AuthService);
authService.login();
Този подробен пример демонстрира как декораторите @Injectable
и @Inject
, в комбинация с reflect-metadata
, позволяват на персонализиран Container
автоматично да разрешава и предоставя зависимости. Метаданните design:paramtypes
, автоматично излъчени от TypeScript (когато emitDecoratorMetadata
е активиран), са от решаващо значение тук.
2. Аспектно-ориентирано програмиране (AOP)
AOP се фокусира върху модуларизирането на кръстосано-разрязващи се аспекти (напр. записване, сигурност, транзакции), които преминават през множество класове и модули. Декораторите са отличен избор за прилагане на AOP концепции в TypeScript.
Пример: Записване с декоратор на метод
Връщайки се към декоратора LogCall
, той е перфектен пример за AOP. Той добавя функционалност за записване към всеки метод, без да модифицира оригиналния код на метода. Това разделя "какво да се направи" (бизнес логика) от "как да се направи" (записване, мониторинг на производителността и т.н.).
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG AOP] Влизане в метод: ${String(propertyKey)} с аргументи:`, args);
try {
const result = originalMethod.apply(this, args);
console.log(`[LOG AOP] Излизане от метод: ${String(propertyKey)} с резултат:`, result);
return result;
} catch (error: any) {
console.error(`[LOG AOP] Грешка в метод ${String(propertyKey)}:`, error.message);
throw error;
}
};
return descriptor;
}
class PaymentProcessor {
@LogMethod
processPayment(amount: number, currency: string) {
if (amount <= 0) {
throw new Error("Сумата на плащането трябва да е положителна.");
}
console.log(`Обработка на плащане от ${amount} ${currency}...`);
return `Плащане от ${amount} ${currency} обработено успешно.`;
}
@LogMethod
refundPayment(transactionId: string) {
console.log(`Възстановяване на плащане за ID на транзакция: ${transactionId}...`);
return `Възстановяването е инициирано за ${transactionId}.`;
}
}
const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
processor.processPayment(-50, "EUR");
} catch (error: any) {
console.error("Хваната грешка:", error.message);
}
Този подход поддържа класа PaymentProcessor
фокусиран единствено върху логиката на плащане, докато декораторът LogMethod
се грижи за кръстосано-разрязващия аспект на записване.
3. Валидация и трансформация
Декораторите са изключително полезни за дефиниране на правила за валидация директно върху свойства или за трансформиране на данни по време на сериализация/десериализация.
Пример: Валидация на данни с декоратори на свойства
Примерът с @Required
по-горе вече демонстрира това. Ето още един пример с валидация на числов диапазон.
interface FieldValidationRule {
property: string | symbol;
validator: (value: any) => boolean;
message: string;
}
const fieldValidationRules = new Map<Function, FieldValidationRule[]>();
function addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {
const rules = fieldValidationRules.get(target.constructor) || [];
rules.push({ property: propertyKey, validator, message });
fieldValidationRules.set(target.constructor, rules);
}
function IsPositive(target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} трябва да бъде положително число.`);
}
function MaxLength(maxLength: number) {
return function (target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} трябва да бъде най-много ${maxLength} символа дълго.`);
};
}
class Product {
@MaxLength(50)
name: string;
@IsPositive
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
static validate(instance: any): string[] {
const errors: string[] = [];
const rules = fieldValidationRules.get(instance.constructor) || [];
for (const rule of rules) {
if (!rule.validator(instance[rule.property])) {
errors.push(rule.message);
}
}
return errors;
}
}
const product1 = new Product("Laptop", 1200);
console.log("Грешки за product1:", Product.validate(product1)); // []
const product2 = new Product("Много дълго име на продукт, което надвишава ограничението от петдесет символа за целите на теста", 50);
console.log("Грешки за product2:", Product.validate(product2)); // ["name трябва да бъде най-много 50 символа дълго."]
const product3 = new Product("Book", -10);
console.log("Грешки за product3:", Product.validate(product3)); // ["price трябва да бъде положително число."]
Тази настройка ви позволява декларативно да дефинирате правила за валидация върху свойствата на вашите модели, което прави вашите модели на данни самоописващи се по отношение на техните ограничения.
Най-добри практики и съображения
Въпреки че декораторите са мощни, те трябва да се използват разумно. Неправилната им употреба може да доведе до код, който е по-труден за дебъгване или разбиране.
Кога да използваме декоратори (и кога не)
- Използвайте ги за:
- Кръстосано-разрязващи аспекти: Записване, кеширане, оторизация, управление на транзакции.
- Деклариране на метаданни: Дефиниране на схема за ORM, правила за валидация, конфигурация на DI.
- Интеграция с фреймуърк: Когато изграждате или използвате фреймуърци, които използват метаданни.
- Намаляване на повтарящ се код: Абстрахиране на повтарящи се шаблони на код.
- Избягвайте ги за:
- Прости извиквания на функции: Ако обикновено извикване на функция може да постигне същия резултат ясно, предпочитайте това.
- Бизнес логика: Декораторите трябва да обогатяват, а не да дефинират, основната бизнес логика.
- Прекомерно усложняване: Ако използването на декоратор прави кода по-малко четим или по-труден за тестване, преосмислете.
Въздействие върху производителността
Декораторите се изпълняват по време на компилация (или по време на дефиниция в JavaScript среда, ако е транспилиран). Трансформацията или събирането на метаданни се случва, когато класът/методът е дефиниран, а не при всяко извикване. Следователно, въздействието върху производителността по време на изпълнение от *прилагането* на декоратори е минимално. Въпреки това, *логиката вътре* във вашите декоратори може да има въздействие върху производителността, особено ако извършват скъпи операции при всяко извикване на метод (напр. сложни изчисления в декоратор на метод).
Поддържаемост и четимост
Декораторите, когато се използват правилно, могат значително да подобрят четимостта, като преместят повтарящия се код извън основната логика. Ако обаче извършват сложни, скрити трансформации, дебъгването може да стане предизвикателство. Уверете се, че вашите декоратори са добре документирани и тяхното поведение е предвидимо.
Експериментален статус и бъдещето на декораторите
Важно е да се повтори, че TypeScript декораторите се основават на предложение на TC39 от Етап 3. Това означава, че спецификацията е до голяма степен стабилна, но все още може да претърпи леки промени, преди да стане част от официалния стандарт ECMAScript. Фреймуърци като Angular са ги приели, залагайки на тяхната бъдеща стандартизация. Това предполага известна степен на риск, въпреки че предвид тяхното широко разпространение, значителни промени, нарушаващи съвместимостта, са малко вероятни.
Предложението на TC39 се е развило. Текущата имплементация на TypeScript се основава на по-стара версия на предложението. Съществува разграничение между "Наследствени декоратори" и "Стандартни декоратори". Когато официалният стандарт бъде приложен, TypeScript вероятно ще актуализира своята имплементация. За повечето разработчици, използващи фреймуърци, този преход ще бъде управляван от самия фреймуърк. За автори на библиотеки, разбирането на фините разлики между наследствени и бъдещи стандартни декоратори може да стане необходимо.
Компилаторната опция emitDecoratorMetadata
Тази опция, когато е зададена на true
в tsconfig.json
, инструктира TypeScript компилатора да излъчва определени метаданни за типовете по време на проектиране в компилирания JavaScript. Тези метаданни включват типа на параметрите на конструктора (design:paramtypes
), типа на връщане на методи (design:returntype
) и типа на свойствата (design:type
).
Тези излъчени метаданни не са част от стандартната JavaScript среда за изпълнение. Те обикновено се използват от полифила reflect-metadata
, който след това ги прави достъпни чрез функциите Reflect.getMetadata()
. Това е абсолютно критично за напреднали шаблони като инжектиране на зависимости, където контейнерът трябва да знае типовете на зависимостите, които класът изисква, без изрична конфигурация.
Напреднали шаблони с декоратори
Декораторите могат да бъдат комбинирани и разширявани за изграждане на още по-сложни шаблони.
1. Декориране на декоратори (Декоратори от по-висок ред)
Можете да създавате декоратори, които модифицират или композират други декоратори. Това е по-рядко срещано, но демонстрира функционалната природа на декораторите.
// Декоратор, който гарантира, че методът е записан и също така изисква администраторски роли
function AdminAndLoggedMethod() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Прилагане на Authorization първо (вътрешно)
Authorization(["admin"])(target, propertyKey, descriptor);
// След това прилагане на LogCall (външно)
LogCall(target, propertyKey, descriptor);
return descriptor; // Връщане на модифицирания дескриптор
};
}
class AdminPanel {
@AdminAndLoggedMethod()
deleteUserAccount(userId: string) {
console.log(`Изтриване на потребителски акаунт: ${userId}`);
return `Потребител ${userId} изтрит.`;
}
}
const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Очакван изход (приемайки администраторска роля):
[AUTH] Достъп предоставен за deleteUserAccount
[LOG] Извикване на deleteUserAccount с аргументи: [ 'user007' ]
Изтриване на потребителски акаунт: user007
[LOG] Метод deleteUserAccount върна: Потребител user007 изтрит.
*/
Тук AdminAndLoggedMethod
е фабрика, която връща декоратор, а вътре в този декоратор той прилага два други декоратора. Този модел може да капсулира сложни композиции на декоратори.
2. Използване на декоратори за миксини
Докато TypeScript предлага други начини за имплементиране на миксини, декораторите могат да се използват за инжектиране на възможности в класове по декларативен начин.
function ApplyMixins(constructors: Function[]) {
return function (derivedConstructor: Function) {
constructors.forEach(baseConstructor => {
Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {
Object.defineProperty(
derivedConstructor.prototype,
name,
Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)
);
});
});
};
}
class Disposable {
isDisposed: boolean = false;
dispose() {
this.isDisposed = true;
console.log("Обектът е изтрит.");
}
}
class Loggable {
log(message: string) {
console.log(`[Loggable] ${message}`);
}
}
@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
// Тези свойства/методи се инжектират от декоратора
isDisposed!: boolean;
dispose!: () => void;
log!: (message: string) => void;
constructor(public name: string) {
this.log(`Ресурс ${this.name} създаден.`);
}
cleanUp() {
this.dispose();
this.log(`Ресурс ${this.name} почистен.`);
}
}
const resource = new MyResource("NetworkConnection");
console.log(`Изтрит: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Изтрит: ${resource.isDisposed}`);
Този декоратор @ApplyMixins
динамично копира методи и свойства от базови конструктори към прототипа на производния клас, ефективно "миксирайки" функционалности.
Заключение: Овластяване на модерното TypeScript разработване
TypeScript декораторите са мощна и експресивна функция, която позволява нова парадигма на метаданни-ориентирано и аспектно-ориентирано програмиране. Те позволяват на разработчиците да обогатяват, модифицират и добавят декларативни поведения към класове, методи, свойства, аксесори и параметри, без да променят основната им логика. Това разделяне на отговорностите води до по-чист, по-поддържан и силно повторно използваема код.
От опростяване на инжектирането на зависимости и прилагане на стабилни системи за валидация до добавяне на кръстосано-разрязващи аспекти като записване и мониторинг на производителността, декораторите предоставят елегантно решение на много често срещани предизвикателства в разработката.
Като овладявате TypeScript декораторите, вие придобивате значителен инструмент във вашия арсенал, което ви позволява да изграждате по-стабилни, мащабируеми и интелигентни приложения. Приемайте ги отговорно, разбирайте тяхната механика и отключете ново ниво на декларативна мощ във вашите TypeScript проекти.