Eesti

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:

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:

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:

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:

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:

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:

  1. Parameetri dekooratorid rakendatakse esimesena, iga parameetri jaoks, alustades viimasest parameetrist esimeseni.
  2. Seejärel rakendatakse iga liikme jaoks meetodi, juurdepääsu või omaduse dekooratorid.
  3. 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)

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.