Uuri TypeScripti dekooratorite vÔimsust metaandmete ja aspektorienteeritud programmeerimisel ning koodi tÀiustamisel deklaratiivsete mustritega. PÔhjalik juhend arendajatele.
TypeScripti dekooratorid: Metaandmete programmeerimismustrite valdamine tugevate rakenduste loomiseks
Kaasaegse tarkvaraarenduse laialdasel maastikul on puhaste, skaleeritavate ja hallatavate koodibaaside sĂ€ilitamine ĂŒlimalt oluline. TypeScript, oma vĂ”imsa tĂŒĂŒbisĂŒsteemi ja tĂ€iustatud funktsioonidega, pakub arendajatele tööriistu selle saavutamiseks. Ăhed selle kĂ”ige intrigeerivamatest ja transformatiivsematest funktsioonidest on dekooratorid. Kuigi kirjutamise hetkel on see veel eksperimentaalne funktsioon (ECMAScripti 3. etapi ettepanek), on dekooratorid laialdaselt kasutusel raamistikes nagu Angular ja TypeORM, muutes pĂ”himĂ”tteliselt seda, kuidas me lĂ€heneme disainimustritele, metaandmete programmeerimisele ja aspektorienteeritud programmeerimisele (AOP).
See pĂ”hjalik juhend sĂŒveneb TypeScripti dekooratoritesse, uurides nende mehaanikat, erinevaid tĂŒĂŒpe, praktilisi rakendusi ja parimaid tavasid. Olenemata sellest, kas loote suuremahulisi ettevĂ”tterakendusi, mikroteenuseid vĂ”i kliendipoolseid veebiliideseid, aitab dekooratorite mĂ”istmine teil kirjutada deklaratiivsemat, hooldatavamat ja vĂ”imsamat TypeScripti koodi.
PÔhimÔiste mÔistmine: Mis on dekoorator?
Oma olemuselt on dekoorator teatud tĂŒĂŒpi deklaratsioon, mida saab lisada klassi deklaratsioonile, meetodile, juurdepÀÀsule, omadusele vĂ”i parameetrile. Dekooraatorid on funktsioonid, mis tagastavad uue vÀÀrtuse (vĂ”i muudavad olemasolevat) sihtobjektile, mida nad kaunistavad. Nende peamine eesmĂ€rk on lisada metaandmeid vĂ”i muuta deklaratsiooni kĂ€itumist, millega nad on seotud, muutmata otse aluseks olevat koodistruktuuri. See vĂ€line, deklaratiivne viis koodi laiendamiseks on uskumatult vĂ”imas.
MÔelge dekooratoritele kui annotatsioonidele vÔi siltidele, mida rakendate oma koodi osadele. Neid silte saab seejÀrel lugeda vÔi nende jÀrgi tegutseda teie rakenduse teiste osade vÔi raamistike poolt, sageli kÀituse ajal, et pakkuda lisafunktsioone vÔi konfiguratsiooni.
Dekooraatori sĂŒntaks
Dekooraatoritele eelneb sĂŒmbol @, millele jĂ€rgneb dekooratori funktsiooni nimi. Need paigutatakse vahetult enne deklaratsiooni, mida nad kaunistavad.
@MyDecorator
class MyClass {
@AnotherDecorator
myMethod() {
// ...
}
}
Dekooraatorite lubamine TypeScriptis
Enne dekooratorite kasutamist peate lubama kompilaatori valiku experimentalDecorators oma tsconfig.json failis. Lisaks, tĂ€iustatud metaandmete peegeldamise vĂ”imekuse jaoks (mida sageli kasutavad raamistikud), vajate ka emitDecoratorMetadata ja reflect-metadata polĂŒfylli.
// tsconfig.json
{
\"compilerOptions\": {
\"target\": \"ES2017\",
\"module\": \"commonjs\",
\"experimentalDecorators\": true,
\"emitDecoratorMetadata\": true,
\"outDir\": \"./dist\",
\"strict\": true,
\"esModuleInterop\": true,
\"skipLibCheck\": true,
\"forceConsistentCasingInFileNames\": true
}
}
Samuti peate installima reflect-metadata:
npm install reflect-metadata --save
# or
yarn add reflect-metadata
Ja importige see oma rakenduse sisenemispunkti (nt main.ts vĂ”i app.ts) kĂ”ige ĂŒlemisse ossa:
import \"reflect-metadata\";
// Teie rakenduse kood jÀrgneb
Dekooraatori tehased: kohandamine teie kÀeulatuses
Kuigi pÔhidokument on funktsioon, peate sageli dekooratorile argumente edastama, et selle kÀitumist konfigureerida. See saavutatakse, kasutades dekooratori tehast. Dekooraatori tehas on funktsioon, mis tagastab tegeliku dekooratori funktsiooni. Dekooraatori tehase rakendamisel kutsute seda koos argumentidega ja seejÀrel tagastab see dekooratori funktsiooni, mida TypeScript teie koodile rakendab.
Lihtsa dekooratori tehase nÀite loomine
Loome tehase Logger dekooratorile, mis suudab logida sÔnumeid erinevate eesliidetega.
function Logger(prefix: string) {
return function (target: Function) {
console.log(`[${prefix}] Class ${target.name} has been defined.`);
};
}
@Logger(\"APP_INIT\")
class ApplicationBootstrap {
constructor() {
console.log(\"Application is starting...\");
}
}
const app = new ApplicationBootstrap();
// Output:
// [APP_INIT] Class ApplicationBootstrap has been defined.
// Application is starting...
Selles nĂ€ites on Logger("APP_INIT") dekooratori tehase vĂ€ljakutse. See tagastab tegeliku dekooratori funktsiooni, mis vĂ”tab oma argumendiks target: Function (klassi konstruktor). See vĂ”imaldab dekooratori kĂ€itumist dĂŒnaamiliselt konfigureerida.
Dekooraatorite tĂŒĂŒbid TypeScriptis
TypeScript toetab viit erinevat tĂŒĂŒpi dekooratorit, millest igaĂŒks on kohaldatav teatud tĂŒĂŒpi deklaratsioonile. Dekooraatori funktsiooni signatuur varieerub sĂ”ltuvalt kontekstist, milles seda rakendatakse.
1. Klassi dekooratorid
Klassi dekooratorid rakendatakse klassi deklaratsioonidele. Dekooraatori funktsioon saab ainsa argumendina klassi konstruktori. Klassi dekoorator vÔib klassi definitsiooni jÀlgida, muuta vÔi isegi asendada.
Signatuur:
function ClassDecorator(target: Function) { ... }
TagastusvÀÀrtus:
Kui klassi dekoorator tagastab vÀÀrtuse, asendab see klassi deklaratsiooni etteantud konstruktorifunktsiooniga. See on vÔimas funktsioon, mida sageli kasutatakse miksinide vÔi klassi laiendamise jaoks. Kui vÀÀrtust ei tagastata, kasutatakse algset klassi.
Kasutusjuhud:
- Klasside registreerimine sĂ”ltuvuste sĂŒstimise konteineris.
- Miksinide vÔi lisafunktsionaalsuste rakendamine klassile.
- Raamistikuspetsiifilised konfiguratsioonid (nt marsruutimine veebiraamistikus).
- ElutsĂŒkli konksude lisamine klassidele.
Klassi dekooratori nĂ€ide: teenuse sĂŒstimine
Kujutage ette lihtsat sĂ”ltuvuste sĂŒstimise stsenaariumi, kus soovite mĂ€rkida klassi "sĂŒstitavaks" ja soovi korral anda sellele konteineris nime.
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(`Registered service: ${serviceName}`);
// Optionally, you could return a new class here to augment behavior
return class extends constructor {
createdAt = new Date();
// Additional properties or methods for all injected services
};
};
}
@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(\"--- Services Registered ---\");
console.log(Array.from(InjectableServiceRegistry.keys()));
const userServiceConstructor = InjectableServiceRegistry.get(\"UserService\");
if (userServiceConstructor) {
const userServiceInstance = new userServiceConstructor();
console.log(\"Users:\", userServiceInstance.getUsers());
// console.log(\"User Service Created At:\", userServiceInstance.createdAt); // If the returned class is used
}
See nĂ€ide demonstreerib, kuidas klassi dekoorator saab klassi registreerida ja isegi selle konstruktorit muuta. Injectable dekoorator muudab klassi teoreetilise sĂ”ltuvuste sĂŒstimise sĂŒsteemi jaoks leitavaks.
2. Meetodi dekooratorid
Meetodi dekooratorid rakendatakse meetodi deklaratsioonidele. Nad saavad kolm argumenti: sihtobjekt (staatiliste liikmete puhul konstruktorifunktsioon; instantsiliikmete puhul klassi prototĂŒĂŒp), meetodi nimi ja meetodi omaduse kirjeldaja.
Signatuur:
function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
TagastusvÀÀrtus:
Meetodi dekoorator vÔib tagastada uue PropertyDescriptor objekti. Kui see nii teeb, kasutatakse seda kirjeldajat meetodi defineerimiseks. See vÔimaldab teil muuta vÔi asendada algse meetodi implementatsiooni, muutes selle AOP-i jaoks uskumatult vÔimsaks.
Kasutusjuhud:
- Meetodikutsete ja nende argumentide/tulemuste logimine.
- Meetoditulemuste vahemÀllu salvestamine jÔudluse parandamiseks.
- Autoriseerimiskontrollide rakendamine enne meetodi tÀitmist.
- Meetodi tÀitmise aja mÔÔtmine.
- Meetodikutsete debounimine vÔi piiramine.
Meetodi dekooratori nÀide: jÔudluse jÀlgimine
Loome MeasurePerformance dekooratori meetodi tÀitmisaja logimiseks.
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(`Method \"${propertyKey}\" executed in ${duration.toFixed(2)} ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@MeasurePerformance
processData(data: number[]): number[] {
// Simulate a complex, time-consuming operation
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(`Data for ID: ${id}`);
}, 500);
});
}
}
const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData(\"abc\").then(result => console.log(result));
MeasurePerformance dekoorator ĂŒmbritseb algse meetodi ajastusloogikaga, printides tĂ€itmisaja ilma meetodi Ă€riloogikat risustamata. See on klassikaline nĂ€ide aspektorienteeritud programmeerimisest (AOP).
3. JuurdepÀÀsu dekooratorid
JuurdepÀÀsu dekooratorid rakendatakse juurdepÀÀsu (get ja set) deklaratsioonidele. Sarnaselt meetodi dekooratoritele saavad nad sihtobjekti, juurdepÀÀsu nime ja selle omaduse kirjeldaja.
Signatuur:
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
TagastusvÀÀrtus:
JuurdepÀÀsu dekoorator vÔib tagastada uue PropertyDescriptor objekti, mida kasutatakse juurdepÀÀsu defineerimiseks.
Kasutusjuhud:
- Valideerimine omaduse mÀÀramisel.
- VÀÀrtuse teisendamine enne selle mÀÀramist vÔi pÀrast selle hankimist.
- JuurdepÀÀsuÔiguste kontrollimine omaduste jaoks.
JuurdepÀÀsu dekooratori nÀide: getterite vahemÀllu salvestamine
Loome dekooratori, mis vahemÀllu salvestab kalli getter-arvutuse tulemuse.
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] Computing value for ${String(propertyKey)}`);
this[cacheKey] = originalGetter.apply(this);
} else {
console.log(`[Cache Hit] Using cached value for ${String(propertyKey)}`);
}
return this[cacheKey];
};
}
return descriptor;
}
class ReportGenerator {
private data: number[];
constructor(data: number[]) {
this.data = data;
}
// Simulates an expensive computation
@CachedGetter
get expensiveSummary(): number {
console.log(\"Performing expensive summary calculation...\");
return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
}
}
const generator = new ReportGenerator([10, 20, 30, 40, 50]);
console.log(\"First access:\", generator.expensiveSummary);
console.log(\"Second access:\", generator.expensiveSummary);
console.log(\"Third access:\", generator.expensiveSummary);
See dekoorator tagab, et expensiveSummary getteri arvutus kĂ€ivitub ainult ĂŒks kord, jĂ€rgnevad kutsed tagastavad vahemĂ€llu salvestatud vÀÀrtuse. See muster on vĂ€ga kasulik jĂ”udluse optimeerimiseks, kus omadusele ligipÀÀs hĂ”lmab rasket arvutust vĂ”i vĂ€liseid kutseid.
4. Omaduse dekooratorid
Omaduse dekooratorid rakendatakse omaduse deklaratsioonidele. Nad saavad kaks argumenti: sihtobjekt (staatiliste liikmete puhul konstruktorifunktsioon; instantsiliikmete puhul klassi prototĂŒĂŒp) ja omaduse nimi.
Signatuur:
function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }
TagastusvÀÀrtus:
Omaduse dekooratorid ei saa tagastada ĂŒhtegi vÀÀrtust. Nende peamine kasutusala on omaduse kohta kĂ€ivate metaandmete registreerimine. Nad ei saa otse muuta omaduse vÀÀrtust ega selle kirjeldajat dekoorimise hetkel, kuna omaduse kirjeldaja ei ole veel tĂ€ielikult mÀÀratletud, kui omaduse dekooratorid kĂ€ivituvad.
Kasutusjuhud:
- Omaduste registreerimine serialiseerimiseks/deserialiseerimiseks.
- Valideerimisreeglite rakendamine omadustele.
- VaikevÀÀrtuste vÔi konfiguratsioonide mÀÀramine omadustele.
- ORM (Object-Relational Mapping) veergude vastendamine (nt
@Column()TypeORM-is).
Omaduse dekooratori nÀide: kohustusliku vÀlja valideerimine
Loome dekooratori omaduse mÀrkimiseks "kohustuslikuks" ja seejÀrel selle valideerimiseks kÀituse ajal.
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)} is required.`
});
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(\"User 1 validation errors:\", validate(user1)); // []
const user2 = new UserProfile(\"\", \"Smith\");
console.log(\"User 2 validation errors:\", validate(user2)); // [\"firstName is required.\"]
const user3 = new UserProfile(\"Alice\", \"\");
console.log(\"User 3 validation errors:\", validate(user3)); // [\"lastName is required.\"]
Required dekoorator lihtsalt registreerib valideerimisreegli tsentraalses validationRules kaardis. Eraldi validate funktsioon kasutab seejÀrel seda metaandmeid, et kontrollida eksemplari kÀituse ajal. See muster eraldab valideerimisloogika andmete definitsioonist, muutes selle taaskasutatavaks ja puhtaks.
5. Parameetri dekooratorid
Parameetri dekooratorid rakendatakse klassi konstruktori vĂ”i meetodi parameetritele. Nad saavad kolm argumenti: sihtobjekt (staatiliste liikmete puhul konstruktorifunktsioon; instantsiliikmete puhul klassi prototĂŒĂŒp), meetodi nimi (vĂ”i undefined konstruktori parameetrite puhul) ja parameetri jĂ€rjekorranumber funktsiooni parameetrite loendis.
Signatuur:
function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
TagastusvÀÀrtus:
Parameetri dekooratorid ei saa tagastada ĂŒhtegi vÀÀrtust. Nagu omaduse dekooratorid, on nende peamine roll lisada parameetri kohta metaandmeid.
Kasutusjuhud:
- ParameetritĂŒĂŒpide registreerimine sĂ”ltuvuste sĂŒstimiseks (nt
@Inject()Angularis). - Valideerimise vÔi teisendamise rakendamine konkreetsetele parameetritele.
- Metaandmete ekstraheerimine API-pÀringu parameetrite kohta veebiraamistikudes.
Parameetri dekooratori nĂ€ide: pĂ€ringuandmete sĂŒstimine
Simuleerime, kuidas veebiraamistik vĂ”ib kasutada parameetri dekooratoreid konkreetsete andmete sĂŒstimiseks meetodi parameetrisse, nĂ€iteks kasutaja ID pĂ€ringust.
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);
};
}
// A hypothetical framework function to invoke a method with resolved parameters
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(`Fetching user with ID: ${userId}, Token: ${authToken || \"N/A\"}`);
return { id: userId, name: \"Jane Doe\" };
}
deleteUser(@RequestParam(\"id\") userId: string) {
console.log(`Deleting user with ID: ${userId}`);
return { status: \"deleted\", id: userId };
}
}
const userController = new UserController();
// Simulate an incoming request
const mockRequest = {
id: \"user123\",
token: \"abc-123\",
someOtherProp: \"xyz\"
};
console.log("\n--- Executing getUser ---");
executeWithParams(userController, \"getUser\", mockRequest);
console.log("\n--- Executing deleteUser ---");
executeWithParams(userController, \"deleteUser\", { id: \"user456\" });
See nĂ€ide nĂ€itab, kuidas parameetri dekooratorid saavad koguda teavet vajalike meetodi parameetrite kohta. Raamistik saab seejĂ€rel kasutada seda kogutud metaandmeid, et automaatselt lahendada ja sĂŒstida sobivaid vÀÀrtusi, kui meetodit kutsutakse, lihtsustades oluliselt kontrolleri vĂ”i teenuse loogikat.
Dekooraatori kompositsioon ja tÀitmise jÀrjekord
Dekooraatoreid saab rakendada erinevates kombinatsioonides ja nende tĂ€itmise jĂ€rjekorra mĂ”istmine on kĂ€itumise ennustamisel ja ootamatute probleemide vĂ€ltimisel ĂŒlioluline.
Mitu dekooratorit ĂŒhel sihtobjektil
Kui ĂŒhele deklaratsioonile (nt klassile, meetodile vĂ”i omadusele) rakendatakse mitu dekooratorit, tĂ€ituvad need konkreetses jĂ€rjekorras: alt ĂŒles vĂ”i paremalt vasakule, nende hindamise osas. Kuid nende tulemused rakendatakse vastupidises jĂ€rjekorras.
@DecoratorA
@DecoratorB
class MyClass {
// ...
}
Siin hinnatakse esmalt DecoratorB, seejĂ€rel DecoratorA. Kui need muudavad klassi (nt tagastades uue konstruktori), siis DecoratorA muudatus ĂŒmbritseb vĂ”i rakendub DecoratorB muudatuse peale.
NĂ€ide: meetodi dekooratorite aheldamine
Vaatleme kahte meetodi dekooratorit: LogCall ja Authorization.
function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] Calling ${String(propertyKey)} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] Method ${String(propertyKey)} returned:`, 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\"]; // Simulate fetching current user roles
const authorized = roles.some(role => currentUserRoles.includes(role));
if (!authorized) {
console.warn(`[AUTH] Access denied for ${String(propertyKey)}. Required roles: ${roles.join(\", \")}`);
throw new Error(\"Unauthorized access\");
}
console.log(`[AUTH] Access granted for ${String(propertyKey)}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class SecureService {
@LogCall
@Authorization([\"admin\"])
deleteSensitiveData(id: string) {
console.log(`Deleting sensitive data for ID: ${id}`);
return `Data ID ${id} deleted.`;
}
@Authorization([\"user\"])
@LogCall // Order changed here
fetchPublicData(query: string) {
console.log(`Fetching public data with query: ${query}`);
return `Public data for query: ${query}`;
}
}
const service = new SecureService();
try {
console.log("\n--- Calling deleteSensitiveData (Admin User) ---");
service.deleteSensitiveData(\"record123\");
} catch (error: any) {
console.error(error.message);
}
try {
console.log("\n--- Calling fetchPublicData (Non-Admin User) ---");
// Simulate a non-admin user trying to access fetchPublicData which requires 'user' role
const mockUserRoles = [\"guest\"]; // This will fail auth
// To make this dynamic, you'd need a DI system or static context for current user roles.
// For simplicity, we assume the Authorization decorator has access to current user context.
// Let's adjust Authorization decorator to always assume 'admin' for demo purposes,
// so the first call succeeds and second fails to show different paths.
// Re-run with user role for fetchPublicData to succeed.
// Imagine currentUserRoles in Authorization becomes: ['user']
// For this example, let's keep it simple and show the order effect.
service.fetchPublicData(\"search term\"); // This will execute Auth -> Log
} catch (error: any) {
console.error(error.message);
}
/* Expected output for deleteSensitiveData:
[AUTH] Access granted for deleteSensitiveData
[LOG] Calling deleteSensitiveData with args: [ 'record123' ]
Deleting sensitive data for ID: record123
[LOG] Method deleteSensitiveData returned: Data ID record123 deleted.
*/
/* Expected output for fetchPublicData (if user has 'user' role):
[LOG] Calling fetchPublicData with args: [ 'search term' ]
[AUTH] Access granted for fetchPublicData
Fetching public data with query: search term
[LOG] Method fetchPublicData returned: Public data for query: search term
*/
Pange tĂ€hele jĂ€rjekorda: deleteSensitiveData puhul kĂ€ivitub esmalt Authorization (alt), seejĂ€rel ĂŒmbritseb selle LogCall (ĂŒlevalt). Authorization sisemine loogika tĂ€itub esimesena. fetchPublicData puhul kĂ€ivitub esmalt LogCall (alt), seejĂ€rel ĂŒmbritseb selle Authorization (ĂŒlevalt). See tĂ€hendab, et LogCall aspekt jÀÀb Authorization aspektist vĂ€ljapoole. See erinevus on kriitiline lĂ€bilĂ”ikavate murede (nagu logimine vĂ”i veakĂ€sitlus) puhul, kus tĂ€itmise jĂ€rjekord vĂ”ib kĂ€itumist oluliselt mĂ”jutada.
TÀitmise jÀrjekord erinevate sihtmÀrkide puhul
Kui klassil, selle liikmetel ja parameetritel on kÔigil dekooratorid, on tÀitmise jÀrjekord hÀsti mÀÀratletud:
- Parameetri dekooratorid rakendatakse esimesena, iga parameetri jaoks, alustades viimasest parameetrist esimeseni.
- SeejÀrel rakendatakse iga liikme jaoks meetodi, juurdepÀÀsu vÔi omaduse dekooratorid.
- LÔpuks rakendatakse klassile endale klassi dekooratorid.
Igas kategoorias rakendatakse sama sihtmĂ€rgi mitut dekooratorit alt ĂŒles (vĂ”i paremalt vasakule).
NÀide: tÀielik tÀitmise jÀrjekord
function log(message: string) {
return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
if (typeof descriptorOrIndex === 'number') {
console.log(`Param Decorator: ${message} on parameter #${descriptorOrIndex} of ${String(propertyKey || \"constructor\")}`);
} else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
console.log(`Method/Accessor Decorator: ${message} on ${String(propertyKey)}`);
} else {
console.log(`Property Decorator: ${message} on ${String(propertyKey)}`);
}
} else {
console.log(`Class Decorator: ${message} on ${target.name}`);
}
return descriptorOrIndex; // Return descriptor for method/accessor, undefined for others
};
}
@log(\"Class Level D\")
@log(\"Class Level C\")
class MyDecoratedClass {
@log(\"Static Property A\")
static staticProp: string = \"\";
@log(\"Instance Property B\")
instanceProp: number = 0;
@log(\"Method D\")
@log(\"Method C\")
myMethod(
@log(\"Parameter Z\") paramZ: string,
@log(\"Parameter Y\") paramY: number
) {
console.log(\"Method myMethod executed.\");
}
@log(\"Getter/Setter F\")
get myAccessor() {
return \"\";
}
set myAccessor(value: string) {
//...
}
constructor() {
console.log(\"Constructor executed.\");
}
}
new MyDecoratedClass();
// Call method to trigger method decorator
new MyDecoratedClass().myMethod(\"hello\", 123);
/* Predicted Output Order (approximate, depending on specific TypeScript version and compilation):
Param Decorator: Parameter Y on parameter #1 of myMethod
Param Decorator: Parameter Z on parameter #0 of myMethod
Property Decorator: Static Property A on staticProp
Property Decorator: Instance Property B on instanceProp
Method/Accessor Decorator: Getter/Setter F on myAccessor
Method/Accessor Decorator: Method C on myMethod
Method/Accessor Decorator: Method D on myMethod
Class Decorator: Class Level C on MyDecoratedClass
Class Decorator: Class Level D on MyDecoratedClass
Constructor executed.
Method myMethod executed.
*/
TĂ€pne konsooli logimise ajastus vĂ”ib pisut erineda sĂ”ltuvalt sellest, millal konstruktorit vĂ”i meetodit kutsutakse, kuid dekooratori funktsioonide endi tĂ€itmise jĂ€rjekord (ja seega ka nende kĂ”rvalmĂ”jud vĂ”i tagastatud vÀÀrtused) jĂ€rgib ĂŒlaltoodud reegleid.
Praktilised rakendused ja disainimustrid dekooratoritega
Dekooraatorid, eriti koos reflect-metadata polĂŒfilliga, avavad uue metaandmepĂ”hise programmeerimise valdkonna. See vĂ”imaldab vĂ”imsaid disainimustreid, mis abstraheerivad korduvat koodi ja lĂ€bilĂ”ikavaid muresid.
1. SĂ”ltuvuste sĂŒstimine (DI)
Ăks silmapaistvamaid dekooratorite kasutusviise on sĂ”ltuvuste sĂŒstimise raamistikes (nagu Angulari @Injectable(), @Component() jne, vĂ”i NestJS-i laialdane DI kasutus). Dekooraatorid vĂ”imaldavad deklareerida sĂ”ltuvusi otse konstruktoritel vĂ”i omadustel, vĂ”imaldades raamistikul automaatselt Ă”igeid teenuseid instanseerida ja pakkuda.
NĂ€ide: lihtsustatud teenuse sĂŒstimine
import \"reflect-metadata\"; // Essential for 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(`Class ${target.name} is not marked as @Injectable.`);
}
// Get constructor parameters' types (requires emitDecoratorMetadata)
const paramTypes: any[] = Reflect.getMetadata(\"design:paramtypes\", target) || [];
const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
const dependencies = paramTypes.map((paramType, index) => {
// Use explicit @Inject token if provided, otherwise infer type
const token = explicitInjections[index] || paramType;
if (token === undefined) {
throw new Error(`Cannot resolve parameter at index ${index} for ${target.name}. It might be a circular dependency or primitive type without explicit @Inject.`);
}
return Container.resolve(token);
});
const instance = new target(...dependencies);
Container.instances.set(target, instance);
return instance;
}
}
// Define services
@Injectable()
class DatabaseService {
connect() {
console.log(\"Connecting to database...\");
return \"DB Connection\";
}
}
@Injectable()
class AuthService {
private db: DatabaseService;
constructor(db: DatabaseService) {
this.db = db;
}
login() {
console.log(`AuthService: Authenticating using ${this.db.connect()}`);
return \"User logged in\";
}
}
@Injectable()
class UserService {
private authService: AuthService;
private dbService: DatabaseService; // Example of injecting via property using a custom decorator or framework feature
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: Fetching user profile...\");
return { id: 1, name: \"Global User\" };
}
}
// Resolve the main service
console.log(\"--- Resolving UserService ---\");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());
console.log("\n--- Resolving AuthService (should be cached) ---");
const authService = Container.resolve(AuthService);
authService.login();
See pĂ”hjalik nĂ€ide demonstreerib, kuidas @Injectable ja @Inject dekooratorid koos reflect-metadata abil vĂ”imaldavad kohandatud Container-il automaatselt sĂ”ltuvusi lahendada ja pakkuda. TypeScripti poolt automaatselt vĂ€ljastatud design:paramtypes metaandmed (kui emitDecoratorMetadata on tĂ”ene) on siin ĂŒliolulised.
2. Aspektorienteeritud programmeerimine (AOP)
AOP keskendub lÀbilÔikavate murede (nt logimine, turvalisus, tehingud) modulariseerimisele, mis lÀbivad mitut klassi ja moodulit. Dekooraatorid sobivad suurepÀraselt AOP-kontseptsioonide rakendamiseks TypeScriptis.
NĂ€ide: logimine meetodi dekooratoriga
Naastes LogCall dekooratori juurde, on see tÀiuslik nÀide AOP-st. See lisab logimiskÀitumise mis tahes meetodile, muutmata meetodi algset koodi. See eraldab "mida teha" (Àriloogika) sellest, "kuidas seda teha" (logimine, jÔudluse jÀlgimine jne).
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG AOP] Entering method: ${String(propertyKey)} with args:`, args);
try {
const result = originalMethod.apply(this, args);
console.log(`[LOG AOP] Exiting method: ${String(propertyKey)} with result:`, result);
return result;
} catch (error: any) {
console.error(`[LOG AOP] Error in method ${String(propertyKey)}:`, error.message);
throw error;
}
};
return descriptor;
}
class PaymentProcessor {
@LogMethod
processPayment(amount: number, currency: string) {
if (amount <= 0) {
throw new Error(\"Payment amount must be positive.\");
}
console.log(`Processing payment of ${amount} ${currency}...`);
return `Payment of ${amount} ${currency} processed successfully.`;
}
@LogMethod
refundPayment(transactionId: string) {
console.log(`Refunding payment for transaction ID: ${transactionId}...`);
return `Refund initiated for ${transactionId}.`;
}
}
const processor = new PaymentProcessor();
processor.processPayment(100, \"USD\");
try {
processor.processPayment(-50, \"EUR\");
} catch (error: any) {
console.error(\"Caught error:\", error.message);
}
See lÀhenemine hoiab PaymentProcessor klassi keskendununa puhtalt makseloogikale, samal ajal kui LogMethod dekoorator tegeleb logimisega seotud lÀbilÔikava murega.
3. Valideerimine ja teisendamine
Dekooraatorid on uskumatult kasulikud valideerimisreeglite otse omadustele defineerimiseks vÔi andmete teisendamiseks serialiseerimise/deserialiseerimise kÀigus.
NĂ€ide: andmete valideerimine omaduse dekooratoritega
Varasem @Required nĂ€ide demonstreeris seda juba. Siin on veel ĂŒks nĂ€ide numbrilise vahemiku valideerimisega.
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)} must be a positive number.`);
}
function MaxLength(maxLength: number) {
return function (target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} must be at most ${maxLength} characters long.`);
};
}
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(\"Product 1 errors:\", Product.validate(product1)); // []
const product2 = new Product(\"Very long product name that exceeds fifty characters limit for testing purpose\", 50);
console.log(\"Product 2 errors:\", Product.validate(product2)); // [\"name must be at most 50 characters long.\"]
const product3 = new Product(\"Book\", -10);
console.log(\"Product 3 errors:\", Product.validate(product3)); // [\"price must be a positive number.\"]
See seadistus vÔimaldab teil deklaratiivselt defineerida valideerimisreegleid oma mudeli omadustel, muutes teie andmemudelid oma piirangute osas isekirjeldavateks.
Parimad tavad ja kaalutlused
Kuigi dekooratorid on vÔimsad, tuleks neid kasutada arukalt. Nende vÀÀrkasutamine vÔib viia koodini, mida on raskem siluda vÔi mÔista.
Millal dekooratoreid kasutada (ja millal mitte)
- Kasuta neid:
- LÀbilÔikavate murede puhul: logimine, vahemÀllu salvestamine, autoriseerimine, tehingute haldamine.
- Metaandmete deklareerimiseks: skeemi defineerimine ORM-idele, valideerimisreeglid, DI konfiguratsioon.
- Raamistiku integreerimiseks: raamistike loomisel vÔi kasutamisel, mis kasutavad metaandmeid.
- Korduva koodi vÀhendamiseks: korduvate koodimustrite abstraheerimiseks.
- VĂ€ldi neid:
- Lihtsate funktsioonikutsete puhul: kui tavaline funktsioonikutse suudab sama tulemuse selgelt saavutada, eelista seda.
- Ăriloogika puhul: dekooratorid peaksid Ă€riloogikat tĂ€iendama, mitte defineerima.
- Ălekomplitseerimise puhul: kui dekooratori kasutamine muudab koodi vĂ€hem loetavaks vĂ”i raskemini testitavaks, mĂ”tle uuesti.
JÔudluse tagajÀrjed
Dekooraatorid tÀituvad kompileerimisajal (vÔi JavaScripti kÀituse ajal, kui transpileeritud). Teisendamine vÔi metaandmete kogumine toimub klassi/meetodi defineerimisel, mitte igal kutsungil. SeetÔttu on dekooratorite *rakendamise* kÀitusaegne jÔudluse mÔju minimaalne. Kuid teie dekooratorite *sees olev loogika* vÔib jÔudlust mÔjutada, eriti kui nad teevad iga meetodi kutsungi puhul kalleid operatsioone (nt keerulisi arvutusi meetodi dekooratori sees).
Hooldatavus ja loetavus
Dekooraatorid, Ôigesti kasutamisel, vÔivad oluliselt parandada loetavust, viies korduva koodi peamisest loogikast vÀlja. Kui nad aga teostavad keerulisi, varjatud teisendusi, vÔib silumine muutuda keeruliseks. Veenduge, et teie dekooratorid on hÀsti dokumenteeritud ja nende kÀitumine on ennustatav.
Dekooraatorite eksperimentaalne staatus ja tulevik
Oluline on korrata, et TypeScripti dekooratorid pÔhinevad TC39 3. etapi ettepanekul. See tÀhendab, et spetsifikatsioon on suures osas stabiilne, kuid vÔib enne ametliku ECMAScripti standardi osaks saamist siiski lÀbi teha vÀiksemaid muudatusi. Raamistikud nagu Angular on need omaks vÔtnud, panustades nende lÔplikule standardiseerimisele. See hÔlmab teatud riskitaset, kuigi nende laialdase leviku tÔttu on olulised murrangulised muutused ebatÔenÀolised.
TC39 ettepanek on arenenud. TypeScripti praegune implementatsioon pĂ”hineb ettepaneku vanemal versioonil. Eksisteerib "pĂ€randdekooratorite" ja "standarddekooratorite" eristus. Kui ametlik standard jĂ”ustub, uuendab TypeScript tĂ”enĂ€oliselt oma implementatsiooni. Enamiku raamistike kasutavate arendajate jaoks haldab seda ĂŒleminekut raamistik ise. Teegiautorite jaoks vĂ”ib osutuda vajalikuks mĂ”ista pĂ€rand- ja tulevaste standarddekooratorite peeneid erinevusi.
Kompilaatori valik emitDecoratorMetadata
See valik, kui see on tsconfig.json failis seatud vÀÀrtusele true, annab TypeScripti kompilaatorile korralduse vĂ€ljastada teatud disainiaegset tĂŒĂŒbi metaandmeid kompileeritud JavaScripti. See metaandmed sisaldavad konstruktori parameetrite tĂŒĂŒpi (design:paramtypes), meetodite tagastustĂŒĂŒpi (design:returntype) ja omaduste tĂŒĂŒpi (design:type).
See vĂ€ljastatud metaandmed ei kuulu standardse JavaScripti kĂ€itusaega. Tavaliselt tarbib seda reflect-metadata polĂŒfill, mis seejĂ€rel teeb selle kĂ€ttesaadavaks Reflect.getMetadata() funktsioonide kaudu. See on absoluutselt kriitiline tĂ€iustatud mustrite (nagu sĂ”ltuvuste sĂŒstimine) puhul, kus konteiner peab teadma klassi nĂ”utavate sĂ”ltuvuste tĂŒĂŒpe ilma selgesĂ”nalise konfiguratsioonita.
TĂ€iustatud mustrid dekooratoritega
Dekooraatoreid saab kombineerida ja laiendada veelgi keerukamate mustrite loomiseks.
1. Dekooraatorite dekoorimine (kÔrgema jÀrgu dekooratorid)
Saate luua dekooratoreid, mis muudavad vÔi komponeerivad teisi dekooratoreid. See on harvem, kuid demonstreerib dekooratorite funktsionaalset olemust.
// A decorator that ensures a method is logged and also requires admin roles
function AdminAndLoggedMethod() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Apply Authorization first (inner)
Authorization([\"admin\"])(target, propertyKey, descriptor);
// Then apply LogCall (outer)
LogCall(target, propertyKey, descriptor);
return descriptor; // Return the modified descriptor
};
}
class AdminPanel {
@AdminAndLoggedMethod()
deleteUserAccount(userId: string) {
console.log(`Deleting user account: ${userId}`);
return `User ${userId} deleted.`;
}
}
const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount(\"user007\");
/* Expected Output (assuming admin role):
[AUTH] Access granted for deleteUserAccount
[LOG] Calling deleteUserAccount with args: [ 'user007' ]
Deleting user account: user007
[LOG] Method deleteUserAccount returned: User user007 deleted.
*/
Siin, AdminAndLoggedMethod on tehas, mis tagastab dekooratori, ja selle dekooratori sees rakendab see kahte teist dekooratorit. See muster vÔib kapseldada keerukaid dekooratorite kompositsioone.
2. Dekooraatorite kasutamine miksinide jaoks
Kuigi TypeScript pakub miksinide implementeerimiseks ka teisi viise, saab dekooratoreid kasutada vĂ”imete sĂŒstimiseks klassidesse deklaratiivsel viisil.
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(\"Object disposed.\");
}
}
class Loggable {
log(message: string) {
console.log(`[Loggable] ${message}`);
}
}
@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
// These properties/methods are injected by the decorator
isDisposed!: boolean;
dispose!: () => void;
log!: (message: string) => void;
constructor(public name: string) {
this.log(`Resource ${this.name} created.`);
}
cleanUp() {
this.dispose();
this.log(`Resource ${this.name} cleaned up.`);
}
}
const resource = new MyResource(\"NetworkConnection\");
console.log(`Is disposed: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Is disposed: ${resource.isDisposed}`);
See @ApplyMixins dekoorator kopeerib dĂŒnaamiliselt meetodeid ja omadusi baaskonstruktoritest tuletatud klassi prototĂŒĂŒpi, "segades" tĂ”husalt sisse funktsionaalsuseid.
KokkuvÔte: kaasaegse TypeScripti arenduse vÔimustamine
TypeScripti dekooratorid on vÔimas ja ekspressiivne funktsioon, mis vÔimaldab uut metaandmepÔhise ja aspektorienteeritud programmeerimise paradigmat. Need vÔimaldavad arendajatel tÀiustada, muuta ja lisada deklaratiivseid kÀitumisi klassidele, meetoditele, omadustele, juurdepÀÀsudele ja parameetritele, muutmata nende pÔhilogikat. Selline murede eraldamine viib puhtama, hooldatavama ja korduvkasutatavama koodini.
Alates sĂ”ltuvuste sĂŒstimise lihtsustamisest ja robustsete valideerimissĂŒsteemide juurutamisest kuni lĂ€bilĂ”ikavate murede (nagu logimine ja jĂ”udluse jĂ€lgimine) lisamiseni pakuvad dekooratorid elegantset lahendust paljudele tavalistele arendusprobleemidele. Kuigi nende eksperimentaalne staatus nĂ”uab teadlikkust, nĂ€itab nende laialdane kasutuselevĂ”tt suurtes raamistikes nende praktilist vÀÀrtust ja tulevast asjakohasust.
TypeScripti dekooratorite valdamisega saate oma arsenali olulise tööriista, mis vÔimaldab teil luua robustsemaid, skaleeritavamaid ja intelligentsemaid rakendusi. VÔtke need vastutustundlikult omaks, mÔistke nende mehaanikat ja avage oma TypeScripti projektides uus deklaratiivse vÔimsuse tase.