Atraskite TypeScript dekoratorių galią meta duomenų, į aspektus orientuotame programavime ir kodo tobulinime deklaratyviais modeliais. Išsamus vadovas programuotojams.
TypeScript dekoratoriai: meta duomenų programavimo modelių įvaldymas tvirtoms programoms kurti
Šiuolaikinės programinės įrangos kūrimo plačiame peizaže svarbiausia yra palaikyti švarias, keičiamo dydžio ir valdomas kodo bazes. TypeScript su savo galinga tipų sistema ir pažangiomis funkcijomis suteikia programuotojams įrankius tai pasiekti. Tarp įdomiausių ir transformuojančių funkcijų yra dekoratoriai. Nors rašymo metu tai vis dar yra eksperimentinė funkcija (3 etapo pasiūlymas ECMAScript), dekoratoriai yra plačiai naudojami tokiose sistemose kaip Angular ir TypeORM, iš esmės keičiantys mūsų požiūrį į dizaino modelius, meta duomenų programavimą ir į aspektus orientuotą programavimą (AOP).
Šis išsamus vadovas gilinsis į TypeScript dekoratorius, nagrinės jų mechaniką, įvairius tipus, praktinius pritaikymus ir geriausias praktikas. Nesvarbu, ar kuriate didelio masto verslo programas, mikroservisus ar kliento pusės žiniatinklio sąsajas, dekoratorių supratimas leis jums rašyti deklaratyvesnį, lengviau prižiūrimą ir galingesnį TypeScript kodą.
Pagrindinės koncepcijos supratimas: kas yra dekoratorius?
Iš esmės dekoratorius yra speciali deklaracijos rūšis, kurią galima pridėti prie klasės deklaracijos, metodo, prieigos metodo (accessor), savybės ar parametro. Dekoratoriai yra funkcijos, kurios grąžina naują vertę (arba modifikuoja esamą) tikslui, kurį jos dekoruoja. Jų pagrindinis tikslas yra pridėti meta duomenis arba pakeisti deklaracijos, prie kurios jie pridedami, elgseną, tiesiogiai nekeičiant pagrindinės kodo struktūros. Šis išorinis, deklaratyvus kodo papildymo būdas yra neįtikėtinai galingas.
Galvokite apie dekoratorius kaip apie anotacijas ar etiketes, kurias priskiriate savo kodo dalims. Šias etiketes vėliau gali skaityti ar veikti kitos jūsų programos dalys ar sistemos, dažnai vykdymo metu, kad suteiktų papildomą funkcionalumą ar konfigūraciją.
Dekoratoriaus sintaksė
Dekoratoriai pradedami @
simboliu, po kurio eina dekoratoriaus funkcijos pavadinimas. Jie rašomi iškart prieš deklaraciją, kurią dekoruoja.
@MyDecorator
class MyClass {
@AnotherDecorator
myMethod() {
// ...
}
}
Dekoratorių įjungimas TypeScript
Prieš pradedant naudoti dekoratorius, turite įjungti experimentalDecorators
kompiliatoriaus parinktį savo tsconfig.json
faile. Be to, norint naudoti pažangias meta duomenų atspindėjimo (reflection) galimybes (dažnai naudojamas sistemose), jums taip pat reikės emitDecoratorMetadata
ir reflect-metadata
polifilo.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Taip pat turite įdiegti reflect-metadata
:
npm install reflect-metadata --save
# arba
yarn add reflect-metadata
Ir importuoti jį pačioje programos įvesties taško pradžioje (pvz., main.ts
ar app.ts
):
import "reflect-metadata";
// Toliau eina jūsų programos kodas
Dekoratorių gamyklos (Factories): pritaikymas ranka pasiekiamas
Nors paprastas dekoratorius yra funkcija, dažnai jums reikės perduoti argumentus dekoratoriui, kad sukonfigūruotumėte jo elgseną. Tai pasiekiama naudojant dekoratoriaus gamyklą. Dekoratoriaus gamykla yra funkcija, kuri grąžina tikrąją dekoratoriaus funkciją. Kai taikote dekoratoriaus gamyklą, jūs ją iškviečiate su argumentais, o ji grąžina dekoratoriaus funkciją, kurią TypeScript pritaiko jūsų kodui.
Paprasto dekoratoriaus gamyklos pavyzdys
Sukurkime gamyklą Logger
dekoratoriui, kuris gali registruoti pranešimus su skirtingais priešdėliais.
function Logger(prefix: string) {
return function (target: Function) {
console.log(`[${prefix}] Klasė ${target.name} buvo apibrėžta.`);
};
}
@Logger("APP_INIT")
class ApplicationBootstrap {
constructor() {
console.log("Programa pradedama...");
}
}
const app = new ApplicationBootstrap();
// Išvestis:
// [APP_INIT] Klasė ApplicationBootstrap buvo apibrėžta.
// Programa pradedama...
Šiame pavyzdyje Logger("APP_INIT")
yra dekoratoriaus gamyklos iškvietimas. Ji grąžina tikrąją dekoratoriaus funkciją, kuri priima target: Function
(klasės konstruktorių) kaip savo argumentą. Tai leidžia dinamiškai konfigūruoti dekoratoriaus elgseną.
Dekoratorių tipai TypeScript
TypeScript palaiko penkis skirtingus dekoratorių tipus, kiekvienas taikomas tam tikros rūšies deklaracijai. Dekoratoriaus funkcijos signatūra skiriasi priklausomai nuo konteksto, kuriame ji taikoma.
1. Klasių dekoratoriai
Klasių dekoratoriai taikomi klasių deklaracijoms. Dekoratoriaus funkcija gauna klasės konstruktorių kaip vienintelį argumentą. Klasės dekoratorius gali stebėti, modifikuoti ar net pakeisti klasės apibrėžimą.
Signatūra:
function ClassDecorator(target: Function) { ... }
Grąžinama vertė:
Jei klasės dekoratorius grąžina vertę, ji pakeis klasės deklaraciją pateikta konstruktoriaus funkcija. Tai galinga funkcija, dažnai naudojama priemaišoms (mixins) ar klasės išplėtimui. Jei vertė negrąžinama, naudojama originali klasė.
Panaudojimo atvejai:
- Klasių registravimas priklausomybių įtraukimo (dependency injection) konteineryje.
- Priemaišų (mixins) ar papildomų funkcionalumų taikymas klasei.
- Sistemai specifinės konfigūracijos (pvz., maršrutizavimas žiniatinklio sistemoje).
- Gyvavimo ciklo kabliukų (lifecycle hooks) pridėjimas klasėms.
Klasės dekoratoriaus pavyzdys: paslaugos įtraukimas (Injecting a Service)
Įsivaizduokime paprastą priklausomybių įtraukimo scenarijų, kuriame norite pažymėti klasę kaip „įtraukiamą“ (injectable) ir pasirinktinai nurodyti jos pavadinimą konteineryje.
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(`Užregistruota paslauga: ${serviceName}`);
// Pasirinktinai, čia galite grąžinti naują klasę, kad išplėstumėte elgseną
return class extends constructor {
createdAt = new Date();
// Papildomos savybės ar metodai visoms įtrauktoms paslaugoms
};
};
}
@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("--- Paslaugos užregistruotos ---");
console.log(Array.from(InjectableServiceRegistry.keys()));
const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
const userServiceInstance = new userServiceConstructor();
console.log("Vartotojai:", userServiceInstance.getUsers());
// console.log("Vartotojų paslaugos sukūrimo laikas:", userServiceInstance.createdAt); // Jei naudojama grąžinta klasė
}
Šis pavyzdys parodo, kaip klasės dekoratorius gali užregistruoti klasę ir netgi modifikuoti jos konstruktorių. Injectable
dekoratorius padaro klasę atrandamą teorinei priklausomybių įtraukimo sistemai.
2. Metodų dekoratoriai
Metodų dekoratoriai taikomi metodų deklaracijoms. Jie gauna tris argumentus: tikslinį objektą (statiniams nariams – konstruktoriaus funkciją; egzemplioriaus nariams – klasės prototipą), metodo pavadinimą ir metodo savybės aprašą (property descriptor).
Signatūra:
function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Grąžinama vertė:
Metodo dekoratorius gali grąžinti naują PropertyDescriptor
. Jei jis tai padaro, šis aprašas bus naudojamas metodui apibrėžti. Tai leidžia jums modifikuoti arba pakeisti originalaus metodo įgyvendinimą, todėl tai yra neįtikėtinai galinga priemonė AOP.
Panaudojimo atvejai:
- Metodų iškvietimų ir jų argumentų/rezultatų registravimas.
- Metodų rezultatų kaupimas (caching) siekiant pagerinti našumą.
- Autorizacijos patikrų taikymas prieš metodo vykdymą.
- Metodo vykdymo laiko matavimas.
- Metodų iškvietimų atidėjimas (debouncing) ar ribojimas (throttling).
Metodo dekoratoriaus pavyzdys: našumo stebėjimas
Sukurkime MeasurePerformance
dekoratorių, kuris registruotų metodo vykdymo laiką.
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(`Metodas "${propertyKey}" įvykdytas per ${duration.toFixed(2)} ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@MeasurePerformance
processData(data: number[]): number[] {
// Imituojama sudėtinga, daug laiko reikalaujanti operacija
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(`Duomenys ID: ${id}`);
}, 500);
});
}
}
const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));
MeasurePerformance
dekoratorius apgaubia originalų metodą laiko matavimo logika, išspausdindamas vykdymo trukmę, neperkraunant verslo logikos pačiame metode. Tai klasikinis į aspektus orientuoto programavimo (AOP) pavyzdys.
3. Prieigos metodų (Accessor) dekoratoriai
Prieigos metodų dekoratoriai taikomi prieigos metodų (get
ir set
) deklaracijoms. Panašiai kaip metodų dekoratoriai, jie gauna tikslinį objektą, prieigos metodo pavadinimą ir jo savybės aprašą.
Signatūra:
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Grąžinama vertė:
Prieigos metodo dekoratorius gali grąžinti naują PropertyDescriptor
, kuris bus naudojamas prieigos metodui apibrėžti.
Panaudojimo atvejai:
- Validacija nustatant savybės vertę.
- Vertės transformavimas prieš ją nustatant arba po jos gavimo.
- Savybių prieigos teisių valdymas.
Prieigos metodo dekoratoriaus pavyzdys: geterių kaupimas (Caching)
Sukurkime dekoratorių, kuris kaupia brangiai apskaičiuojamo geterio rezultatą.
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] Apskaičiuojama vertė savybei ${String(propertyKey)}`);
this[cacheKey] = originalGetter.apply(this);
} else {
console.log(`[Cache Hit] Naudojama kešuota vertė savybei ${String(propertyKey)}`);
}
return this[cacheKey];
};
}
return descriptor;
}
class ReportGenerator {
private data: number[];
constructor(data: number[]) {
this.data = data;
}
// Imituoja brangų skaičiavimą
@CachedGetter
get expensiveSummary(): number {
console.log("Vykdomas brangus suvestinės skaičiavimas...");
return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
}
}
const generator = new ReportGenerator([10, 20, 30, 40, 50]);
console.log("Pirmas kreipimasis:", generator.expensiveSummary);
console.log("Antras kreipimasis:", generator.expensiveSummary);
console.log("Trečias kreipimasis:", generator.expensiveSummary);
Šis dekoratorius užtikrina, kad expensiveSummary
geterio skaičiavimas bus atliktas tik vieną kartą, o vėlesni iškvietimai grąžins kešuotą vertę. Šis modelis yra labai naudingas optimizuojant našumą, kai savybės prieiga apima sudėtingus skaičiavimus ar išorinius iškvietimus.
4. Savybių (Property) dekoratoriai
Savybių dekoratoriai taikomi savybių deklaracijoms. Jie gauna du argumentus: tikslinį objektą (statiniams nariams – konstruktoriaus funkciją; egzemplioriaus nariams – klasės prototipą) ir savybės pavadinimą.
Signatūra:
function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }
Grąžinama vertė:
Savybių dekoratoriai negali grąžinti jokios vertės. Jų pagrindinė paskirtis – registruoti meta duomenis apie savybę. Jie negali tiesiogiai pakeisti savybės vertės ar jos aprašo dekoravimo metu, nes savybės aprašas dar nėra visiškai apibrėžtas, kai vykdomi savybių dekoratoriai.
Panaudojimo atvejai:
- Savybių registravimas serializavimui/deserializavimui.
- Validacijos taisyklių taikymas savybėms.
- Numatytųjų verčių ar konfigūracijų nustatymas savybėms.
- ORM (Object-Relational Mapping) stulpelių susiejimas (pvz.,
@Column()
TypeORM).
Savybės dekoratoriaus pavyzdys: privalomo lauko validacija
Sukurkime dekoratorių, kuris pažymėtų savybę kaip „privalomą“ ir vėliau ją patikrintų vykdymo metu.
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)} yra privalomas.`
});
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("1 vartotojo validacijos klaidos:", validate(user1)); // []
const user2 = new UserProfile("", "Smith");
console.log("2 vartotojo validacijos klaidos:", validate(user2)); // ["firstName yra privalomas."]
const user3 = new UserProfile("Alice", "");
console.log("3 vartotojo validacijos klaidos:", validate(user3)); // ["lastName yra privalomas."]
Required
dekoratorius paprasčiausiai užregistruoja validacijos taisyklę centriniame validationRules
žemėlapyje. Atskira validate
funkcija vėliau naudoja šiuos meta duomenis, kad patikrintų egzempliorių vykdymo metu. Šis modelis atskiria validacijos logiką nuo duomenų apibrėžimo, todėl ji tampa pakartotinai naudojama ir švari.
5. Parametrų dekoratoriai
Parametrų dekoratoriai taikomi parametrams klasės konstruktoriuje arba metode. Jie gauna tris argumentus: tikslinį objektą (statiniams nariams – konstruktoriaus funkciją; egzemplioriaus nariams – klasės prototipą), metodo pavadinimą (arba undefined
konstruktoriaus parametrams) ir parametro eilės indeksą funkcijos parametrų sąraše.
Signatūra:
function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
Grąžinama vertė:
Parametrų dekoratoriai negali grąžinti jokios vertės. Kaip ir savybių dekoratoriai, jų pagrindinis vaidmuo yra pridėti meta duomenis apie parametrą.
Panaudojimo atvejai:
- Parametrų tipų registravimas priklausomybių įtraukimui (pvz.,
@Inject()
Angular). - Validacijos ar transformacijos taikymas konkretiems parametrams.
- Meta duomenų apie API užklausos parametrus išgavimas žiniatinklio sistemose.
Parametro dekoratoriaus pavyzdys: užklausos duomenų įtraukimas
Imituokime, kaip žiniatinklio sistema galėtų naudoti parametrų dekoratorius, kad įtrauktų konkrečius duomenis į metodo parametrą, pvz., vartotojo ID iš užklausos.
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);
};
}
// Hipotetinė sistemos funkcija, skirta iškviesti metodą su išspręstais parametrais
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(`Ieškomas vartotojas su ID: ${userId}, Žetonas: ${authToken || "N/A"}`);
return { id: userId, name: "Jane Doe" };
}
deleteUser(@RequestParam("id") userId: string) {
console.log(`Trinamas vartotojas su ID: ${userId}`);
return { status: "deleted", id: userId };
}
}
const userController = new UserController();
// Imituojama gaunama užklausa
const mockRequest = {
id: "user123",
token: "abc-123",
someOtherProp: "xyz"
};
console.log("\n--- Vykdomas getUser ---");
executeWithParams(userController, "getUser", mockRequest);
console.log("\n--- Vykdomas deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });
Šis pavyzdys parodo, kaip parametrų dekoratoriai gali rinkti informaciją apie reikiamus metodo parametrus. Sistema vėliau gali naudoti šiuos surinktus meta duomenis, kad automatiškai išspręstų ir įtrauktų atitinkamas vertes, kai metodas yra iškviečiamas, žymiai supaprastinant valdiklio ar paslaugos logiką.
Dekoratorių kompozicija ir vykdymo tvarka
Dekoratoriai gali būti taikomi įvairiomis kombinacijomis, o jų vykdymo tvarkos supratimas yra labai svarbus norint numatyti elgseną ir išvengti netikėtų problemų.
Keli dekoratoriai vienam tikslui
Kai vienai deklaracijai (pvz., klasei, metodui ar savybei) taikomi keli dekoratoriai, jie vykdomi tam tikra tvarka: iš apačios į viršų, arba iš dešinės į kairę, vertinimo metu. Tačiau jų rezultatai taikomi atvirkštine tvarka.
@DecoratorA
@DecoratorB
class MyClass {
// ...
}
Čia pirmiausia bus įvertintas DecoratorB
, o po to DecoratorA
. Jei jie modifikuoja klasę (pvz., grąžindami naują konstruktorių), DecoratorA
modifikacija apgaubs arba bus pritaikyta virš DecoratorB
modifikacijos.
Pavyzdys: metodų dekoratorių grandinė
Apsvarstykime du metodų dekoratorius: LogCall
ir Authorization
.
function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] Kviečiamas ${String(propertyKey)} su argumentais:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] Metodas ${String(propertyKey)} grąžino:`, 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"]; // Imituojamas dabartinio vartotojo rolių gavimas
const authorized = roles.some(role => currentUserRoles.includes(role));
if (!authorized) {
console.warn(`[AUTH] Prieiga prie ${String(propertyKey)} uždrausta. Reikalingos rolės: ${roles.join(", ")}`);
throw new Error("Neautorizuota prieiga");
}
console.log(`[AUTH] Prieiga prie ${String(propertyKey)} suteikta`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class SecureService {
@LogCall
@Authorization(["admin"])
deleteSensitiveData(id: string) {
console.log(`Trinami jautrūs duomenys ID: ${id}`);
return `Duomenų ID ${id} ištrintas.`;
}
@Authorization(["user"])
@LogCall // Tvarka čia pakeista
fetchPublicData(query: string) {
console.log(`Ieškomi vieši duomenys pagal užklausą: ${query}`);
return `Vieši duomenys užklausai: ${query}`;
}
}
const service = new SecureService();
try {
console.log("\n--- Kviečiamas deleteSensitiveData (Admin vartotojas) ---");
service.deleteSensitiveData("record123");
} catch (error: any) {
console.error(error.message);
}
try {
console.log("\n--- Kviečiamas fetchPublicData (Ne Admin vartotojas) ---");
// Imituojamas ne administratoriaus vartotojas, bandantis pasiekti fetchPublicData, kuriam reikalinga 'user' rolė
const mockUserRoles = ["guest"]; // Tai nepraeis autorizacijos
// Kad tai būtų dinamiška, jums reikėtų DI sistemos ar statinio konteksto dabartinio vartotojo rolėms.
// Paprastumo dėlei, tarkime, kad Authorization dekoratorius turi prieigą prie dabartinio vartotojo konteksto.
// Pakeiskime Authorization dekoratorių, kad jis visada tarkime 'admin' demonstraciniais tikslais,
// kad pirmas iškvietimas pavyktų, o antras nepavyktų, parodydamas skirtingus kelius.
// Paleiskime iš naujo su vartotojo role, kad fetchPublicData pavyktų.
// Įsivaizduokite, kad currentUserRoles Authorization tampa: ['user']
// Šiam pavyzdžiui, palikime paprastai ir parodykime tvarkos poveikį.
service.fetchPublicData("search term"); // Tai vykdys Auth -> Log
} catch (error: any) {
console.error(error.message);
}
/* Numatoma išvestis deleteSensitiveData:
[AUTH] Prieiga prie deleteSensitiveData suteikta
[LOG] Kviečiamas deleteSensitiveData su argumentais: [ 'record123' ]
Trinami jautrūs duomenys ID: record123
[LOG] Metodas deleteSensitiveData grąžino: Duomenų ID record123 ištrintas.
*/
/* Numatoma išvestis fetchPublicData (jei vartotojas turi 'user' rolę):
[LOG] Kviečiamas fetchPublicData su argumentais: [ 'search term' ]
[AUTH] Prieiga prie fetchPublicData suteikta
Ieškomi vieši duomenys pagal užklausą: search term
[LOG] Metodas fetchPublicData grąžino: Vieši duomenys užklausai: search term
*/
Atkreipkite dėmesį į tvarką: deleteSensitiveData
atveju pirmiausia vykdomas Authorization
(apačioje), tada jį apgaubia LogCall
(viršuje). Vidinė Authorization
logika įvykdoma pirma. fetchPublicData
atveju pirmiausia vykdomas LogCall
(apačioje), tada jį apgaubia Authorization
(viršuje). Tai reiškia, kad LogCall
aspektas bus už Authorization
aspekto ribų. Šis skirtumas yra kritiškai svarbus skersiniams aspektams (cross-cutting concerns), tokiems kaip registravimas ar klaidų apdorojimas, kur vykdymo tvarka gali ženkliai paveikti elgseną.
Vykdymo tvarka skirtingiems tikslams
Kai klasė, jos nariai ir parametrai visi turi dekoratorius, vykdymo tvarka yra aiškiai apibrėžta:
- Parametrų dekoratoriai taikomi pirmiausia, kiekvienam parametrui, pradedant nuo paskutinio parametro iki pirmojo.
- Tada metodų, prieigos metodų ar savybių dekoratoriai taikomi kiekvienam nariui.
- Galiausiai, klasių dekoratoriai taikomi pačiai klasei.
Kiekvienoje kategorijoje keli dekoratoriai tam pačiam tikslui taikomi iš apačios į viršų (arba iš dešinės į kairę).
Pavyzdys: pilna vykdymo tvarka
function log(message: string) {
return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
if (typeof descriptorOrIndex === 'number') {
console.log(`Parametro dekoratorius: ${message} parametrui #${descriptorOrIndex} iš ${String(propertyKey || "konstruktorius")}`);
} else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
console.log(`Metodo/Prieigos metodo dekoratorius: ${message} ant ${String(propertyKey)}`);
} else {
console.log(`Savybės dekoratorius: ${message} ant ${String(propertyKey)}`);
}
} else {
console.log(`Klasės dekoratorius: ${message} ant ${target.name}`);
}
return descriptorOrIndex; // Grąžinamas aprašas metodui/prieigos metodui, undefined kitiems
};
}
@log("Klasės lygis D")
@log("Klasės lygis C")
class MyDecoratedClass {
@log("Statinė savybė A")
static staticProp: string = "";
@log("Egzemplioriaus savybė B")
instanceProp: number = 0;
@log("Metodas D")
@log("Metodas C")
myMethod(
@log("Parametras Z") paramZ: string,
@log("Parametras Y") paramY: number
) {
console.log("Metodas myMethod įvykdytas.");
}
@log("Getter/Setter F")
get myAccessor() {
return "";
}
set myAccessor(value: string) {
//...
}
constructor() {
console.log("Konstruktorius įvykdytas.");
}
}
new MyDecoratedClass();
// Iškviečiame metodą, kad suaktyvintume metodo dekoratorių
new MyDecoratedClass().myMethod("hello", 123);
/* Numatoma išvesties tvarka (apytikslė, priklausomai nuo konkrečios TypeScript versijos ir kompiliacijos):
Parametro dekoratorius: Parametras Y parametrui #1 iš myMethod
Parametro dekoratorius: Parametras Z parametrui #0 iš myMethod
Savybės dekoratorius: Statinė savybė A ant staticProp
Savybės dekoratorius: Egzemplioriaus savybė B ant instanceProp
Metodo/Prieigos metodo dekoratorius: Getter/Setter F ant myAccessor
Metodo/Prieigos metodo dekoratorius: Metodas C ant myMethod
Metodo/Prieigos metodo dekoratorius: Metodas D ant myMethod
Klasės dekoratorius: Klasės lygis C ant MyDecoratedClass
Klasės dekoratorius: Klasės lygis D ant MyDecoratedClass
Konstruktorius įvykdytas.
Metodas myMethod įvykdytas.
*/
Tiksli konsolės išvesties laiko eiga gali šiek tiek skirtis priklausomai nuo to, kada iškviečiamas konstruktorius ar metodas, tačiau tvarka, kuria pačios dekoratorių funkcijos yra vykdomos (ir taip taikomi jų šalutiniai poveikiai ar grąžinamos vertės), atitinka aukščiau pateiktas taisykles.
Praktiniai pritaikymai ir dizaino modeliai su dekoratoriais
Dekoratoriai, ypač kartu su reflect-metadata
polifilu, atveria naują meta duomenimis pagrįsto programavimo sritį. Tai leidžia naudoti galingus dizaino modelius, kurie abstrahuoja šabloninį kodą ir skersinius aspektus.
1. Priklausomybių įtraukimas (DI)
Vienas iš ryškiausių dekoratorių panaudojimo būdų yra priklausomybių įtraukimo sistemose (pvz., Angular @Injectable()
, @Component()
ir kt., arba NestJS plačiai naudojamas DI). Dekoratoriai leidžia deklaruoti priklausomybes tiesiogiai konstruktoriuose ar savybėse, suteikiant sistemai galimybę automatiškai sukurti ir pateikti teisingas paslaugas.
Pavyzdys: supaprastintas paslaugų įtraukimas
import "reflect-metadata"; // Būtina 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(`Klasė ${target.name} nėra pažymėta kaip @Injectable.`);
}
// Gaunami konstruktoriaus parametrų tipai (reikalingas emitDecoratorMetadata)
const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
const dependencies = paramTypes.map((paramType, index) => {
// Naudojamas aiškus @Inject žetonas, jei pateiktas, kitu atveju nustatomas tipas
const token = explicitInjections[index] || paramType;
if (token === undefined) {
throw new Error(`Nepavyko išspręsti parametro indekse ${index} klasei ${target.name}. Tai gali būti ciklinė priklausomybė arba primityvus tipas be aiškaus @Inject.`);
}
return Container.resolve(token);
});
const instance = new target(...dependencies);
Container.instances.set(target, instance);
return instance;
}
}
// Apibrėžiamos paslaugos
@Injectable()
class DatabaseService {
connect() {
console.log("Jungiamasi prie duomenų bazės...");
return "DB prisijungimas";
}
}
@Injectable()
class AuthService {
private db: DatabaseService;
constructor(db: DatabaseService) {
this.db = db;
}
login() {
console.log(`AuthService: Autentifikuojama naudojant ${this.db.connect()}`);
return "Vartotojas prisijungė";
}
}
@Injectable()
class UserService {
private authService: AuthService;
private dbService: DatabaseService; // Pavyzdys, kaip įtraukti per savybę naudojant pasirinktinį dekoratorių ar sistemos funkciją
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: Gaunamas vartotojo profilis...");
return { id: 1, name: "Global User" };
}
}
// Išsprendžiama pagrindinė paslauga
console.log("--- Išsprendžiamas UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());
console.log("\n--- Išsprendžiamas AuthService (turėtų būti kešuotas) ---");
const authService = Container.resolve(AuthService);
authService.login();
Šis sudėtingas pavyzdys parodo, kaip @Injectable
ir @Inject
dekoratoriai, kartu su reflect-metadata
, leidžia pasirinktiniam Container
automatiškai išspręsti ir pateikti priklausomybes. design:paramtypes
meta duomenys, automatiškai išspinduliuojami TypeScript (kai emitDecoratorMetadata
yra tiesa), čia yra kritiškai svarbūs.
2. Į aspektus orientuotas programavimas (AOP)
AOP sutelkia dėmesį į skersinių aspektų (pvz., registravimo, saugumo, transakcijų), kurie kerta kelias klases ir modulius, modularizavimą. Dekoratoriai puikiai tinka AOP koncepcijoms įgyvendinti TypeScript.
Pavyzdys: registravimas su metodo dekoratoriumi
Grįžtant prie LogCall
dekoratoriaus, tai yra puikus AOP pavyzdys. Jis prideda registravimo elgseną bet kuriam metodui, nekeičiant originalaus metodo kodo. Tai atskiria „ką daryti“ (verslo logiką) nuo „kaip tai daryti“ (registravimas, našumo stebėjimas ir kt.).
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG AOP] Įeinama į metodą: ${String(propertyKey)} su argumentais:`, args);
try {
const result = originalMethod.apply(this, args);
console.log(`[LOG AOP] Išeinama iš metodo: ${String(propertyKey)} su rezultatu:`, result);
return result;
} catch (error: any) {
console.error(`[LOG AOP] Klaida metode ${String(propertyKey)}:`, error.message);
throw error;
}
};
return descriptor;
}
class PaymentProcessor {
@LogMethod
processPayment(amount: number, currency: string) {
if (amount <= 0) {
throw new Error("Mokėjimo suma turi būti teigiama.");
}
console.log(`Apdorojamas mokėjimas ${amount} ${currency}...`);
return `Mokėjimas ${amount} ${currency} sėkmingai apdorotas.`;
}
@LogMethod
refundPayment(transactionId: string) {
console.log(`Grąžinamas mokėjimas transakcijos ID: ${transactionId}...`);
return `Grąžinimas inicijuotas ${transactionId}.`;
}
}
const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
processor.processPayment(-50, "EUR");
} catch (error: any) {
console.error("Pagauta klaida:", error.message);
}
Šis požiūris leidžia PaymentProcessor
klasei sutelkti dėmesį tik į mokėjimų logiką, o LogMethod
dekoratorius tvarko skersinį registravimo aspektą.
3. Validacija ir transformacija
Dekoratoriai yra neįtikėtinai naudingi apibrėžiant validacijos taisykles tiesiogiai savybėse arba transformuojant duomenis serializacijos/deserializacijos metu.
Pavyzdys: duomenų validacija su savybių dekoratoriais
@Required
pavyzdys anksčiau jau tai demonstravo. Štai dar vienas pavyzdys su skaitinio diapazono validacija.
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)} turi būti teigiamas skaičius.`);
}
function MaxLength(maxLength: number) {
return function (target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} turi būti ne ilgesnis nei ${maxLength} simbolių.`);
};
}
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("1 produkto klaidos:", Product.validate(product1)); // []
const product2 = new Product("Labai ilgas produkto pavadinimas, kuris viršija penkiasdešimties simbolių limitą testavimo tikslais", 50);
console.log("2 produkto klaidos:", Product.validate(product2)); // ["name turi būti ne ilgesnis nei 50 simbolių."]
const product3 = new Product("Book", -10);
console.log("3 produkto klaidos:", Product.validate(product3)); // ["price turi būti teigiamas skaičius."]
Ši sąranka leidžia deklaratyviai apibrėžti validacijos taisykles savo modelio savybėse, todėl jūsų duomenų modeliai tampa savaime aprašantys savo apribojimus.
Geriausios praktikos ir svarstymai
Nors dekoratoriai yra galingi, juos reikėtų naudoti apgalvotai. Netinkamas jų naudojimas gali lemti kodą, kurį sunkiau derinti ar suprasti.
Kada naudoti dekoratorius (ir kada ne)
- Naudokite juos:
- Skersiniams aspektams: registravimui, kešavimui, autorizacijai, transakcijų valdymui.
- Meta duomenų deklaravimui: ORM schemų apibrėžimui, validacijos taisyklėms, DI konfigūracijai.
- Sistemos integracijai: kuriant ar naudojant sistemas, kurios naudoja meta duomenis.
- Šabloninio kodo mažinimui: abstrahuojant pasikartojančius kodo modelius.
- Venkite jų:
- Paprastiems funkcijų iškvietimams: jei paprastas funkcijos iškvietimas gali pasiekti tą patį rezultatą aiškiai, rinkitės jį.
- Verslo logikai: dekoratoriai turėtų papildyti, o ne apibrėžti pagrindinę verslo logiką.
- Perdėtam sudėtingumui: jei dekoratoriaus naudojimas daro kodą mažiau skaitomą ar sunkiau testuojamą, apsvarstykite iš naujo.
Našumo pasekmės
Dekoratoriai vykdomi kompiliavimo metu (arba apibrėžimo metu JavaScript vykdymo aplinkoje, jei transpiliuoti). Transformacija ar meta duomenų rinkimas vyksta, kai klasė/metodas yra apibrėžiamas, o ne kiekvieno iškvietimo metu. Todėl vykdymo laiko našumo poveikis *taikant* dekoratorius yra minimalus. Tačiau *logika viduje* jūsų dekoratorių gali turėti našumo poveikį, ypač jei jie atlieka brangias operacijas kiekvieno metodo iškvietimo metu (pvz., sudėtingi skaičiavimai metodo dekoratoriuje).
Priežiūra ir skaitomumas
Dekoratoriai, kai naudojami teisingai, gali žymiai pagerinti skaitomumą, perkeldami šabloninį kodą iš pagrindinės logikos. Tačiau jei jie atlieka sudėtingas, paslėptas transformacijas, derinimas gali tapti sudėtingas. Užtikrinkite, kad jūsų dekoratoriai būtų gerai dokumentuoti, o jų elgsena būtų nuspėjama.
Eksperimentinis statusas ir dekoratorių ateitis
Svarbu pakartoti, kad TypeScript dekoratoriai yra pagrįsti 3 etapo TC39 pasiūlymu. Tai reiškia, kad specifikacija yra daugiausia stabili, bet vis dar gali būti nežymių pakeitimų, kol taps oficialia ECMAScript standarto dalimi. Tokios sistemos kaip Angular juos priėmė, lažindamosi dėl jų galutinio standartizavimo. Tai reiškia tam tikrą rizikos lygį, nors, atsižvelgiant į jų platų pritaikymą, reikšmingi ardomieji pakeitimai yra mažai tikėtini.
TC39 pasiūlymas evoliucionavo. Dabartinis TypeScript įgyvendinimas yra pagrįstas senesne pasiūlymo versija. Yra „senųjų dekoratorių“ (Legacy Decorators) ir „standartinių dekoratorių“ (Standard Decorators) skirtumas. Kai bus priimtas oficialus standartas, TypeScript tikriausiai atnaujins savo įgyvendinimą. Daugumai programuotojų, naudojančių sistemas, šį perėjimą valdys pati sistema. Bibliotekų autoriams gali tekti suprasti subtilius skirtumus tarp senųjų ir būsimų standartinių dekoratorių.
emitDecoratorMetadata
kompiliatoriaus parinktis
Ši parinktis, kai nustatyta į true
tsconfig.json
, nurodo TypeScript kompiliatoriui išspinduliuoti tam tikrus projektavimo metu esančius tipų meta duomenis į sukompiliuotą JavaScript. Šie meta duomenys apima konstruktoriaus parametrų tipą (design:paramtypes
), metodų grąžinimo tipą (design:returntype
) ir savybių tipą (design:type
).
Šie išspinduliuoti meta duomenys nėra standartinės JavaScript vykdymo aplinkos dalis. Juos paprastai naudoja reflect-metadata
polifilas, kuris tada padaro juos prieinamus per Reflect.getMetadata()
funkcijas. Tai yra absoliučiai būtina pažangiems modeliams, tokiems kaip priklausomybių įtraukimas, kur konteineris turi žinoti priklausomybių tipus, kurių klasei reikia be aiškios konfigūracijos.
Pažangūs modeliai su dekoratoriais
Dekoratorius galima derinti ir plėsti, kad būtų sukurti dar sudėtingesni modeliai.
1. Dekoratorių dekoravimas (aukštesnės eilės dekoratoriai)
Galite kurti dekoratorius, kurie modifikuoja ar sudaro kitus dekoratorius. Tai yra rečiau pasitaikantis atvejis, bet parodo funkcinę dekoratorių prigimtį.
// Dekoratorius, užtikrinantis, kad metodas yra registruojamas ir taip pat reikalauja administratoriaus teisių
function AdminAndLoggedMethod() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Pirmiausia taikomas Authorization (vidinis)
Authorization(["admin"])(target, propertyKey, descriptor);
// Tada taikomas LogCall (išorinis)
LogCall(target, propertyKey, descriptor);
return descriptor; // Grąžinamas modifikuotas aprašas
};
}
class AdminPanel {
@AdminAndLoggedMethod()
deleteUserAccount(userId: string) {
console.log(`Trinama vartotojo paskyra: ${userId}`);
return `Vartotojas ${userId} ištrintas.`;
}
}
const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Numatoma išvestis (darant prielaidą, kad vartotojas turi administratoriaus teises):
[AUTH] Prieiga prie deleteUserAccount suteikta
[LOG] Kviečiamas deleteUserAccount su argumentais: [ 'user007' ]
Trinama vartotojo paskyra: user007
[LOG] Metodas deleteUserAccount grąžino: Vartotojas user007 ištrintas.
*/
Čia AdminAndLoggedMethod
yra gamykla, kuri grąžina dekoratorių, o to dekoratoriaus viduje ji taiko du kitus dekoratorius. Šis modelis gali apimti sudėtingas dekoratorių kompozicijas.
2. Dekoratorių naudojimas priemaišoms (Mixins)
Nors TypeScript siūlo kitus būdus įgyvendinti priemaišas, dekoratoriai gali būti naudojami norint deklaratyviai įtraukti galimybes į klases.
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("Objektas sunaikintas.");
}
}
class Loggable {
log(message: string) {
console.log(`[Loggable] ${message}`);
}
}
@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
// Šios savybės/metodai yra įtraukiami dekoratoriaus
isDisposed!: boolean;
dispose!: () => void;
log!: (message: string) => void;
constructor(public name: string) {
this.log(`Resursas ${this.name} sukurtas.`);
}
cleanUp() {
this.dispose();
this.log(`Resursas ${this.name} išvalytas.`);
}
}
const resource = new MyResource("NetworkConnection");
console.log(`Ar sunaikintas: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Ar sunaikintas: ${resource.isDisposed}`);
Šis @ApplyMixins
dekoratorius dinamiškai kopijuoja metodus ir savybes iš bazių konstruktorių į išvestinės klasės prototipą, efektyviai „įmaišydamas“ funkcionalumus.
Išvada: modernaus TypeScript kūrimo įgalinimas
TypeScript dekoratoriai yra galinga ir išraiškinga funkcija, kuri įgalina naują meta duomenimis pagrįsto ir į aspektus orientuoto programavimo paradigmą. Jie leidžia programuotojams tobulinti, modifikuoti ir pridėti deklaratyvią elgseną klasėms, metodams, savybėms, prieigos metodams ir parametrams, nekeičiant jų pagrindinės logikos. Šis atsakomybių atskyrimas lemia švaresnį, lengviau prižiūrimą ir labai pakartotinai naudojamą kodą.
Nuo priklausomybių įtraukimo supaprastinimo ir tvirtų validacijos sistemų įgyvendinimo iki skersinių aspektų, tokių kaip registravimas ir našumo stebėjimas, pridėjimo, dekoratoriai siūlo elegantišką sprendimą daugeliui įprastų kūrimo iššūkių. Nors jų eksperimentinis statusas reikalauja atsargumo, jų platus pritaikymas pagrindinėse sistemose rodo jų praktinę vertę ir ateities aktualumą.
Įvaldę TypeScript dekoratorius, jūs įgyjate reikšmingą įrankį savo arsenale, leidžiantį kurti tvirtesnes, keičiamo dydžio ir išmanesnes programas. Naudokite juos atsakingai, supraskite jų mechaniką ir atrakinkite naują deklaratyvios galios lygį savo TypeScript projektuose.