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.