Lietuvių

Atraskite TypeScript dekoratorių galią meta duomenų, į aspektus orientuotame programavime ir kodo tobulinime deklaratyviais modeliais. Išsamus vadovas programuotojams.

TypeScript dekoratoriai: meta duomenų programavimo modelių įvaldymas tvirtoms programoms kurti

Šiuolaikinės programinės įrangos kūrimo plačiame peizaže svarbiausia yra palaikyti švarias, keičiamo dydžio ir valdomas kodo bazes. TypeScript su savo galinga tipų sistema ir pažangiomis funkcijomis suteikia programuotojams įrankius tai pasiekti. Tarp įdomiausių ir transformuojančių funkcijų yra dekoratoriai. Nors rašymo metu tai vis dar yra eksperimentinė funkcija (3 etapo pasiūlymas ECMAScript), dekoratoriai yra plačiai naudojami tokiose sistemose kaip Angular ir TypeORM, iš esmės keičiantys mūsų požiūrį į dizaino modelius, meta duomenų programavimą ir į aspektus orientuotą programavimą (AOP).

Šis išsamus vadovas gilinsis į TypeScript dekoratorius, nagrinės jų mechaniką, įvairius tipus, praktinius pritaikymus ir geriausias praktikas. Nesvarbu, ar kuriate didelio masto verslo programas, mikroservisus ar kliento pusės žiniatinklio sąsajas, dekoratorių supratimas leis jums rašyti deklaratyvesnį, lengviau prižiūrimą ir galingesnį TypeScript kodą.

Pagrindinės koncepcijos supratimas: kas yra dekoratorius?

Iš esmės dekoratorius yra speciali deklaracijos rūšis, kurią galima pridėti prie klasės deklaracijos, metodo, prieigos metodo (accessor), savybės ar parametro. Dekoratoriai yra funkcijos, kurios grąžina naują vertę (arba modifikuoja esamą) tikslui, kurį jos dekoruoja. Jų pagrindinis tikslas yra pridėti meta duomenis arba pakeisti deklaracijos, prie kurios jie pridedami, elgseną, tiesiogiai nekeičiant pagrindinės kodo struktūros. Šis išorinis, deklaratyvus kodo papildymo būdas yra neįtikėtinai galingas.

Galvokite apie dekoratorius kaip apie anotacijas ar etiketes, kurias priskiriate savo kodo dalims. Šias etiketes vėliau gali skaityti ar veikti kitos jūsų programos dalys ar sistemos, dažnai vykdymo metu, kad suteiktų papildomą funkcionalumą ar konfigūraciją.

Dekoratoriaus sintaksė

Dekoratoriai pradedami @ simboliu, po kurio eina dekoratoriaus funkcijos pavadinimas. Jie rašomi iškart prieš deklaraciją, kurią dekoruoja.

@MyDecorator
class MyClass {
  @AnotherDecorator
  myMethod() {
    // ...
  }
}

Dekoratorių įjungimas TypeScript

Prieš pradedant naudoti dekoratorius, turite įjungti experimentalDecorators kompiliatoriaus parinktį savo tsconfig.json faile. Be to, norint naudoti pažangias meta duomenų atspindėjimo (reflection) galimybes (dažnai naudojamas sistemose), jums taip pat reikės emitDecoratorMetadata ir reflect-metadata polifilo.

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2017",
    "module": "commonjs",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Taip pat turite įdiegti reflect-metadata:

npm install reflect-metadata --save
# arba
yarn add reflect-metadata

Ir importuoti jį pačioje programos įvesties taško pradžioje (pvz., main.ts ar app.ts):

import "reflect-metadata";
// Toliau eina jūsų programos kodas

Dekoratorių gamyklos (Factories): pritaikymas ranka pasiekiamas

Nors paprastas dekoratorius yra funkcija, dažnai jums reikės perduoti argumentus dekoratoriui, kad sukonfigūruotumėte jo elgseną. Tai pasiekiama naudojant dekoratoriaus gamyklą. Dekoratoriaus gamykla yra funkcija, kuri grąžina tikrąją dekoratoriaus funkciją. Kai taikote dekoratoriaus gamyklą, jūs ją iškviečiate su argumentais, o ji grąžina dekoratoriaus funkciją, kurią TypeScript pritaiko jūsų kodui.

Paprasto dekoratoriaus gamyklos pavyzdys

Sukurkime gamyklą Logger dekoratoriui, kuris gali registruoti pranešimus su skirtingais priešdėliais.

function Logger(prefix: string) {
  return function (target: Function) {
    console.log(`[${prefix}] Klasė ${target.name} buvo apibrėžta.`);
  };
}

@Logger("APP_INIT")
class ApplicationBootstrap {
  constructor() {
    console.log("Programa pradedama...");
  }
}

const app = new ApplicationBootstrap();
// Išvestis:
// [APP_INIT] Klasė ApplicationBootstrap buvo apibrėžta.
// Programa pradedama...

Šiame pavyzdyje Logger("APP_INIT") yra dekoratoriaus gamyklos iškvietimas. Ji grąžina tikrąją dekoratoriaus funkciją, kuri priima target: Function (klasės konstruktorių) kaip savo argumentą. Tai leidžia dinamiškai konfigūruoti dekoratoriaus elgseną.

Dekoratorių tipai TypeScript

TypeScript palaiko penkis skirtingus dekoratorių tipus, kiekvienas taikomas tam tikros rūšies deklaracijai. Dekoratoriaus funkcijos signatūra skiriasi priklausomai nuo konteksto, kuriame ji taikoma.

1. Klasių dekoratoriai

Klasių dekoratoriai taikomi klasių deklaracijoms. Dekoratoriaus funkcija gauna klasės konstruktorių kaip vienintelį argumentą. Klasės dekoratorius gali stebėti, modifikuoti ar net pakeisti klasės apibrėžimą.

Signatūra:

function ClassDecorator(target: Function) { ... }

Grąžinama vertė:

Jei klasės dekoratorius grąžina vertę, ji pakeis klasės deklaraciją pateikta konstruktoriaus funkcija. Tai galinga funkcija, dažnai naudojama priemaišoms (mixins) ar klasės išplėtimui. Jei vertė negrąžinama, naudojama originali klasė.

Panaudojimo atvejai:

Klasės dekoratoriaus pavyzdys: paslaugos įtraukimas (Injecting a Service)

Įsivaizduokime paprastą priklausomybių įtraukimo scenarijų, kuriame norite pažymėti klasę kaip „įtraukiamą“ (injectable) ir pasirinktinai nurodyti jos pavadinimą konteineryje.

const InjectableServiceRegistry = new Map<string, Function>();

function Injectable(name?: string) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    const serviceName = name || constructor.name;
    InjectableServiceRegistry.set(serviceName, constructor);
    console.log(`Užregistruota paslauga: ${serviceName}`);

    // Pasirinktinai, čia galite grąžinti naują klasę, kad išplėstumėte elgseną
    return class extends constructor {
      createdAt = new Date();
      // Papildomos savybės ar metodai visoms įtrauktoms paslaugoms
    };
  };
}

@Injectable("UserService")
class UserDataService {
  getUsers() {
    return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
  }
}

@Injectable()
class ProductDataService {
  getProducts() {
    return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];
  }
}

console.log("--- Paslaugos užregistruotos ---");
console.log(Array.from(InjectableServiceRegistry.keys()));

const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
  const userServiceInstance = new userServiceConstructor();
  console.log("Vartotojai:", userServiceInstance.getUsers());
  // console.log("Vartotojų paslaugos sukūrimo laikas:", userServiceInstance.createdAt); // Jei naudojama grąžinta klasė
}

Šis pavyzdys parodo, kaip klasės dekoratorius gali užregistruoti klasę ir netgi modifikuoti jos konstruktorių. Injectable dekoratorius padaro klasę atrandamą teorinei priklausomybių įtraukimo sistemai.

2. Metodų dekoratoriai

Metodų dekoratoriai taikomi metodų deklaracijoms. Jie gauna tris argumentus: tikslinį objektą (statiniams nariams – konstruktoriaus funkciją; egzemplioriaus nariams – klasės prototipą), metodo pavadinimą ir metodo savybės aprašą (property descriptor).

Signatūra:

function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Grąžinama vertė:

Metodo dekoratorius gali grąžinti naują PropertyDescriptor. Jei jis tai padaro, šis aprašas bus naudojamas metodui apibrėžti. Tai leidžia jums modifikuoti arba pakeisti originalaus metodo įgyvendinimą, todėl tai yra neįtikėtinai galinga priemonė AOP.

Panaudojimo atvejai:

Metodo dekoratoriaus pavyzdys: našumo stebėjimas

Sukurkime MeasurePerformance dekoratorių, kuris registruotų metodo vykdymo laiką.

function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    const start = process.hrtime.bigint();
    const result = originalMethod.apply(this, args);
    const end = process.hrtime.bigint();
    const duration = Number(end - start) / 1_000_000;
    console.log(`Metodas "${propertyKey}" įvykdytas per ${duration.toFixed(2)} ms`);
    return result;
  };

  return descriptor;
}

class DataProcessor {
  @MeasurePerformance
  processData(data: number[]): number[] {
    // Imituojama sudėtinga, daug laiko reikalaujanti operacija
    for (let i = 0; i < 1_000_000; i++) {
      Math.sin(i);
    }
    return data.map(n => n * 2);
  }

  @MeasurePerformance
  fetchRemoteData(id: string): Promise<string> {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(`Duomenys ID: ${id}`);
      }, 500);
    });
  }
}

const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));

MeasurePerformance dekoratorius apgaubia originalų metodą laiko matavimo logika, išspausdindamas vykdymo trukmę, neperkraunant verslo logikos pačiame metode. Tai klasikinis į aspektus orientuoto programavimo (AOP) pavyzdys.

3. Prieigos metodų (Accessor) dekoratoriai

Prieigos metodų dekoratoriai taikomi prieigos metodų (get ir set) deklaracijoms. Panašiai kaip metodų dekoratoriai, jie gauna tikslinį objektą, prieigos metodo pavadinimą ir jo savybės aprašą.

Signatūra:

function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Grąžinama vertė:

Prieigos metodo dekoratorius gali grąžinti naują PropertyDescriptor, kuris bus naudojamas prieigos metodui apibrėžti.

Panaudojimo atvejai:

Prieigos metodo dekoratoriaus pavyzdys: geterių kaupimas (Caching)

Sukurkime dekoratorių, kuris kaupia brangiai apskaičiuojamo geterio rezultatą.

function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalGetter = descriptor.get;
  const cacheKey = `_cached_${String(propertyKey)}`;

  if (originalGetter) {
    descriptor.get = function() {
      if (this[cacheKey] === undefined) {
        console.log(`[Cache Miss] Apskaičiuojama vertė savybei ${String(propertyKey)}`);
        this[cacheKey] = originalGetter.apply(this);
      } else {
        console.log(`[Cache Hit] Naudojama kešuota vertė savybei ${String(propertyKey)}`);
      }
      return this[cacheKey];
    };
  }
  return descriptor;
}

class ReportGenerator {
  private data: number[];

  constructor(data: number[]) {
    this.data = data;
  }

  // Imituoja brangų skaičiavimą
  @CachedGetter
  get expensiveSummary(): number {
    console.log("Vykdomas brangus suvestinės skaičiavimas...");
    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
  }
}

const generator = new ReportGenerator([10, 20, 30, 40, 50]);

console.log("Pirmas kreipimasis:", generator.expensiveSummary);
console.log("Antras kreipimasis:", generator.expensiveSummary);
console.log("Trečias kreipimasis:", generator.expensiveSummary);

Šis dekoratorius užtikrina, kad expensiveSummary geterio skaičiavimas bus atliktas tik vieną kartą, o vėlesni iškvietimai grąžins kešuotą vertę. Šis modelis yra labai naudingas optimizuojant našumą, kai savybės prieiga apima sudėtingus skaičiavimus ar išorinius iškvietimus.

4. Savybių (Property) dekoratoriai

Savybių dekoratoriai taikomi savybių deklaracijoms. Jie gauna du argumentus: tikslinį objektą (statiniams nariams – konstruktoriaus funkciją; egzemplioriaus nariams – klasės prototipą) ir savybės pavadinimą.

Signatūra:

function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }

Grąžinama vertė:

Savybių dekoratoriai negali grąžinti jokios vertės. Jų pagrindinė paskirtis – registruoti meta duomenis apie savybę. Jie negali tiesiogiai pakeisti savybės vertės ar jos aprašo dekoravimo metu, nes savybės aprašas dar nėra visiškai apibrėžtas, kai vykdomi savybių dekoratoriai.

Panaudojimo atvejai:

Savybės dekoratoriaus pavyzdys: privalomo lauko validacija

Sukurkime dekoratorių, kuris pažymėtų savybę kaip „privalomą“ ir vėliau ją patikrintų vykdymo metu.

interface ValidationRule {
  property: string | symbol;
  validate: (value: any) => boolean;
  message: string;
}

const validationRules: Map<Function, ValidationRule[]> = new Map();

function Required(target: Object, propertyKey: string | symbol) {
  const rules = validationRules.get(target.constructor) || [];
  rules.push({
    property: propertyKey,
    validate: (value: any) => value !== null && value !== undefined && value !== "",
    message: `${String(propertyKey)} yra privalomas.`
  });
  validationRules.set(target.constructor, rules);
}

function validate(instance: any): string[] {
  const classRules = validationRules.get(instance.constructor) || [];
  const errors: string[] = [];

  for (const rule of classRules) {
    if (!rule.validate(instance[rule.property])) {
      errors.push(rule.message);
    }
  }
  return errors;
}

class UserProfile {
  @Required
  firstName: string;

  @Required
  lastName: string;

  age?: number;

  constructor(firstName: string, lastName: string, age?: number) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
}

const user1 = new UserProfile("John", "Doe", 30);
console.log("1 vartotojo validacijos klaidos:", validate(user1)); // []

const user2 = new UserProfile("", "Smith");
console.log("2 vartotojo validacijos klaidos:", validate(user2)); // ["firstName yra privalomas."]

const user3 = new UserProfile("Alice", "");
console.log("3 vartotojo validacijos klaidos:", validate(user3)); // ["lastName yra privalomas."]

Required dekoratorius paprasčiausiai užregistruoja validacijos taisyklę centriniame validationRules žemėlapyje. Atskira validate funkcija vėliau naudoja šiuos meta duomenis, kad patikrintų egzempliorių vykdymo metu. Šis modelis atskiria validacijos logiką nuo duomenų apibrėžimo, todėl ji tampa pakartotinai naudojama ir švari.

5. Parametrų dekoratoriai

Parametrų dekoratoriai taikomi parametrams klasės konstruktoriuje arba metode. Jie gauna tris argumentus: tikslinį objektą (statiniams nariams – konstruktoriaus funkciją; egzemplioriaus nariams – klasės prototipą), metodo pavadinimą (arba undefined konstruktoriaus parametrams) ir parametro eilės indeksą funkcijos parametrų sąraše.

Signatūra:

function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }

Grąžinama vertė:

Parametrų dekoratoriai negali grąžinti jokios vertės. Kaip ir savybių dekoratoriai, jų pagrindinis vaidmuo yra pridėti meta duomenis apie parametrą.

Panaudojimo atvejai:

Parametro dekoratoriaus pavyzdys: užklausos duomenų įtraukimas

Imituokime, kaip žiniatinklio sistema galėtų naudoti parametrų dekoratorius, kad įtrauktų konkrečius duomenis į metodo parametrą, pvz., vartotojo ID iš užklausos.

interface ParameterMetadata {
  index: number;
  key: string | symbol;
  resolver: (request: any) => any;
}

const parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();

function RequestParam(paramName: string) {
  return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
    const targetKey = propertyKey || "constructor";
    let methodResolvers = parameterResolvers.get(target.constructor);
    if (!methodResolvers) {
      methodResolvers = new Map();
      parameterResolvers.set(target.constructor, methodResolvers);
    }
    const paramMetadata = methodResolvers.get(targetKey) || [];
    paramMetadata.push({
      index: parameterIndex,
      key: targetKey,
      resolver: (request: any) => request[paramName]
    });
    methodResolvers.set(targetKey, paramMetadata);
  };
}

// Hipotetinė sistemos funkcija, skirta iškviesti metodą su išspręstais parametrais
function executeWithParams(instance: any, methodName: string, request: any) {
  const classResolvers = parameterResolvers.get(instance.constructor);
  if (!classResolvers) {
    return (instance[methodName] as Function).apply(instance, []);
  }
  const methodParamMetadata = classResolvers.get(methodName);
  if (!methodParamMetadata) {
    return (instance[methodName] as Function).apply(instance, []);
  }

  const args: any[] = Array(methodParamMetadata.length);
  for (const meta of methodParamMetadata) {
    args[meta.index] = meta.resolver(request);
  }
  return (instance[methodName] as Function).apply(instance, args);
}

class UserController {
  getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {
    console.log(`Ieškomas vartotojas su ID: ${userId}, Žetonas: ${authToken || "N/A"}`);
    return { id: userId, name: "Jane Doe" };
  }

  deleteUser(@RequestParam("id") userId: string) {
    console.log(`Trinamas vartotojas su ID: ${userId}`);
    return { status: "deleted", id: userId };
  }
}

const userController = new UserController();

// Imituojama gaunama užklausa
const mockRequest = {
  id: "user123",
  token: "abc-123",
  someOtherProp: "xyz"
};

console.log("\n--- Vykdomas getUser ---");
executeWithParams(userController, "getUser", mockRequest);

console.log("\n--- Vykdomas deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });

Šis pavyzdys parodo, kaip parametrų dekoratoriai gali rinkti informaciją apie reikiamus metodo parametrus. Sistema vėliau gali naudoti šiuos surinktus meta duomenis, kad automatiškai išspręstų ir įtrauktų atitinkamas vertes, kai metodas yra iškviečiamas, žymiai supaprastinant valdiklio ar paslaugos logiką.

Dekoratorių kompozicija ir vykdymo tvarka

Dekoratoriai gali būti taikomi įvairiomis kombinacijomis, o jų vykdymo tvarkos supratimas yra labai svarbus norint numatyti elgseną ir išvengti netikėtų problemų.

Keli dekoratoriai vienam tikslui

Kai vienai deklaracijai (pvz., klasei, metodui ar savybei) taikomi keli dekoratoriai, jie vykdomi tam tikra tvarka: iš apačios į viršų, arba iš dešinės į kairę, vertinimo metu. Tačiau jų rezultatai taikomi atvirkštine tvarka.

@DecoratorA
@DecoratorB
class MyClass {
  // ...
}

Čia pirmiausia bus įvertintas DecoratorB, o po to DecoratorA. Jei jie modifikuoja klasę (pvz., grąžindami naują konstruktorių), DecoratorA modifikacija apgaubs arba bus pritaikyta virš DecoratorB modifikacijos.

Pavyzdys: metodų dekoratorių grandinė

Apsvarstykime du metodų dekoratorius: LogCall ir Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] Kviečiamas ${String(propertyKey)} su argumentais:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] Metodas ${String(propertyKey)} grąžino:`, result);
    return result;
  };
  return descriptor;
}

function Authorization(roles: string[]) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const currentUserRoles = ["admin"]; // Imituojamas dabartinio vartotojo rolių gavimas
      const authorized = roles.some(role => currentUserRoles.includes(role));
      if (!authorized) {
        console.warn(`[AUTH] Prieiga prie ${String(propertyKey)} uždrausta. Reikalingos rolės: ${roles.join(", ")}`);
        throw new Error("Neautorizuota prieiga");
      }
      console.log(`[AUTH] Prieiga prie ${String(propertyKey)} suteikta`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class SecureService {
  @LogCall
  @Authorization(["admin"])
  deleteSensitiveData(id: string) {
    console.log(`Trinami jautrūs duomenys ID: ${id}`);
    return `Duomenų ID ${id} ištrintas.`;
  }

  @Authorization(["user"])
  @LogCall // Tvarka čia pakeista
  fetchPublicData(query: string) {
    console.log(`Ieškomi vieši duomenys pagal užklausą: ${query}`);
    return `Vieši duomenys užklausai: ${query}`; 
  }
}

const service = new SecureService();

try {
  console.log("\n--- Kviečiamas deleteSensitiveData (Admin vartotojas) ---");
  service.deleteSensitiveData("record123");
} catch (error: any) {
  console.error(error.message);
}

try {
  console.log("\n--- Kviečiamas fetchPublicData (Ne Admin vartotojas) ---");
  // Imituojamas ne administratoriaus vartotojas, bandantis pasiekti fetchPublicData, kuriam reikalinga 'user' rolė
  const mockUserRoles = ["guest"]; // Tai nepraeis autorizacijos
  // Kad tai būtų dinamiška, jums reikėtų DI sistemos ar statinio konteksto dabartinio vartotojo rolėms.
  // Paprastumo dėlei, tarkime, kad Authorization dekoratorius turi prieigą prie dabartinio vartotojo konteksto.
  // Pakeiskime Authorization dekoratorių, kad jis visada tarkime 'admin' demonstraciniais tikslais,
  // kad pirmas iškvietimas pavyktų, o antras nepavyktų, parodydamas skirtingus kelius.
  
  // Paleiskime iš naujo su vartotojo role, kad fetchPublicData pavyktų.
  // Įsivaizduokite, kad currentUserRoles Authorization tampa: ['user']
  // Šiam pavyzdžiui, palikime paprastai ir parodykime tvarkos poveikį.
  service.fetchPublicData("search term"); // Tai vykdys Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* Numatoma išvestis deleteSensitiveData:
[AUTH] Prieiga prie deleteSensitiveData suteikta
[LOG] Kviečiamas deleteSensitiveData su argumentais: [ 'record123' ]
Trinami jautrūs duomenys ID: record123
[LOG] Metodas deleteSensitiveData grąžino: Duomenų ID record123 ištrintas.
*/

/* Numatoma išvestis fetchPublicData (jei vartotojas turi 'user' rolę):
[LOG] Kviečiamas fetchPublicData su argumentais: [ 'search term' ]
[AUTH] Prieiga prie fetchPublicData suteikta
Ieškomi vieši duomenys pagal užklausą: search term
[LOG] Metodas fetchPublicData grąžino: Vieši duomenys užklausai: search term
*/

Atkreipkite dėmesį į tvarką: deleteSensitiveData atveju pirmiausia vykdomas Authorization (apačioje), tada jį apgaubia LogCall (viršuje). Vidinė Authorization logika įvykdoma pirma. fetchPublicData atveju pirmiausia vykdomas LogCall (apačioje), tada jį apgaubia Authorization (viršuje). Tai reiškia, kad LogCall aspektas bus už Authorization aspekto ribų. Šis skirtumas yra kritiškai svarbus skersiniams aspektams (cross-cutting concerns), tokiems kaip registravimas ar klaidų apdorojimas, kur vykdymo tvarka gali ženkliai paveikti elgseną.

Vykdymo tvarka skirtingiems tikslams

Kai klasė, jos nariai ir parametrai visi turi dekoratorius, vykdymo tvarka yra aiškiai apibrėžta:

  1. Parametrų dekoratoriai taikomi pirmiausia, kiekvienam parametrui, pradedant nuo paskutinio parametro iki pirmojo.
  2. Tada metodų, prieigos metodų ar savybių dekoratoriai taikomi kiekvienam nariui.
  3. Galiausiai, klasių dekoratoriai taikomi pačiai klasei.

Kiekvienoje kategorijoje keli dekoratoriai tam pačiam tikslui taikomi iš apačios į viršų (arba iš dešinės į kairę).

Pavyzdys: pilna vykdymo tvarka

function log(message: string) {
  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
    if (typeof descriptorOrIndex === 'number') {
      console.log(`Parametro dekoratorius: ${message} parametrui #${descriptorOrIndex} iš ${String(propertyKey || "konstruktorius")}`);
    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
        console.log(`Metodo/Prieigos metodo dekoratorius: ${message} ant ${String(propertyKey)}`);
      } else {
        console.log(`Savybės dekoratorius: ${message} ant ${String(propertyKey)}`);
      }
    } else {
      console.log(`Klasės dekoratorius: ${message} ant ${target.name}`);
    }
    return descriptorOrIndex; // Grąžinamas aprašas metodui/prieigos metodui, undefined kitiems
  };
}

@log("Klasės lygis D")
@log("Klasės lygis C")
class MyDecoratedClass {
  @log("Statinė savybė A")
  static staticProp: string = "";

  @log("Egzemplioriaus savybė B")
  instanceProp: number = 0;

  @log("Metodas D")
  @log("Metodas C")
  myMethod(
    @log("Parametras Z") paramZ: string,
    @log("Parametras Y") paramY: number
  ) {
    console.log("Metodas myMethod įvykdytas.");
  }

  @log("Getter/Setter F")
  get myAccessor() {
    return "";
  }

  set myAccessor(value: string) {
    //...
  }

  constructor() {
    console.log("Konstruktorius įvykdytas.");
  }
}

new MyDecoratedClass();
// Iškviečiame metodą, kad suaktyvintume metodo dekoratorių
new MyDecoratedClass().myMethod("hello", 123);

/* Numatoma išvesties tvarka (apytikslė, priklausomai nuo konkrečios TypeScript versijos ir kompiliacijos):
Parametro dekoratorius: Parametras Y parametrui #1 iš myMethod
Parametro dekoratorius: Parametras Z parametrui #0 iš myMethod
Savybės dekoratorius: Statinė savybė A ant staticProp
Savybės dekoratorius: Egzemplioriaus savybė B ant instanceProp
Metodo/Prieigos metodo dekoratorius: Getter/Setter F ant myAccessor
Metodo/Prieigos metodo dekoratorius: Metodas C ant myMethod
Metodo/Prieigos metodo dekoratorius: Metodas D ant myMethod
Klasės dekoratorius: Klasės lygis C ant MyDecoratedClass
Klasės dekoratorius: Klasės lygis D ant MyDecoratedClass
Konstruktorius įvykdytas.
Metodas myMethod įvykdytas.
*/

Tiksli konsolės išvesties laiko eiga gali šiek tiek skirtis priklausomai nuo to, kada iškviečiamas konstruktorius ar metodas, tačiau tvarka, kuria pačios dekoratorių funkcijos yra vykdomos (ir taip taikomi jų šalutiniai poveikiai ar grąžinamos vertės), atitinka aukščiau pateiktas taisykles.

Praktiniai pritaikymai ir dizaino modeliai su dekoratoriais

Dekoratoriai, ypač kartu su reflect-metadata polifilu, atveria naują meta duomenimis pagrįsto programavimo sritį. Tai leidžia naudoti galingus dizaino modelius, kurie abstrahuoja šabloninį kodą ir skersinius aspektus.

1. Priklausomybių įtraukimas (DI)

Vienas iš ryškiausių dekoratorių panaudojimo būdų yra priklausomybių įtraukimo sistemose (pvz., Angular @Injectable(), @Component() ir kt., arba NestJS plačiai naudojamas DI). Dekoratoriai leidžia deklaruoti priklausomybes tiesiogiai konstruktoriuose ar savybėse, suteikiant sistemai galimybę automatiškai sukurti ir pateikti teisingas paslaugas.

Pavyzdys: supaprastintas paslaugų įtraukimas

import "reflect-metadata"; // Būtina emitDecoratorMetadata

const INJECTABLE_METADATA_KEY = Symbol("injectable");
const INJECT_METADATA_KEY = Symbol("inject");

function Injectable() {
  return function (target: Function) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
  };
}

function Inject(token: any) {
  return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
    const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];
    existingInjections[parameterIndex] = token;
    Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);
  };
}

class Container {
  private static instances = new Map<any, any>();

  static resolve<T>(target: { new (...args: any[]): T }): T {
    if (Container.instances.has(target)) {
      return Container.instances.get(target);
    }

    const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);
    if (!isInjectable) {
      throw new Error(`Klasė ${target.name} nėra pažymėta kaip @Injectable.`);
    }

    // Gaunami konstruktoriaus parametrų tipai (reikalingas emitDecoratorMetadata)
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // Naudojamas aiškus @Inject žetonas, jei pateiktas, kitu atveju nustatomas tipas
      const token = explicitInjections[index] || paramType;
      if (token === undefined) {
        throw new Error(`Nepavyko išspręsti parametro indekse ${index} klasei ${target.name}. Tai gali būti ciklinė priklausomybė arba primityvus tipas be aiškaus @Inject.`);
      }
      return Container.resolve(token);
    });

    const instance = new target(...dependencies);
    Container.instances.set(target, instance);
    return instance;
  }
}

// Apibrėžiamos paslaugos
@Injectable()
class DatabaseService {
  connect() {
    console.log("Jungiamasi prie duomenų bazės...");
    return "DB prisijungimas";
  }
}

@Injectable()
class AuthService {
  private db: DatabaseService;

  constructor(db: DatabaseService) {
    this.db = db;
  }

  login() {
    console.log(`AuthService: Autentifikuojama naudojant ${this.db.connect()}`);
    return "Vartotojas prisijungė";
  }
}

@Injectable()
class UserService {
  private authService: AuthService;
  private dbService: DatabaseService; // Pavyzdys, kaip įtraukti per savybę naudojant pasirinktinį dekoratorių ar sistemos funkciją

  constructor(@Inject(AuthService) authService: AuthService,
              @Inject(DatabaseService) dbService: DatabaseService) {
    this.authService = authService;
    this.dbService = dbService;
  }

  getUserProfile() {
    this.authService.login();
    this.dbService.connect();
    console.log("UserService: Gaunamas vartotojo profilis...");
    return { id: 1, name: "Global User" };
  }
}

// Išsprendžiama pagrindinė paslauga
console.log("--- Išsprendžiamas UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("\n--- Išsprendžiamas AuthService (turėtų būti kešuotas) ---");
const authService = Container.resolve(AuthService);
authService.login();

Šis sudėtingas pavyzdys parodo, kaip @Injectable ir @Inject dekoratoriai, kartu su reflect-metadata, leidžia pasirinktiniam Container automatiškai išspręsti ir pateikti priklausomybes. design:paramtypes meta duomenys, automatiškai išspinduliuojami TypeScript (kai emitDecoratorMetadata yra tiesa), čia yra kritiškai svarbūs.

2. Į aspektus orientuotas programavimas (AOP)

AOP sutelkia dėmesį į skersinių aspektų (pvz., registravimo, saugumo, transakcijų), kurie kerta kelias klases ir modulius, modularizavimą. Dekoratoriai puikiai tinka AOP koncepcijoms įgyvendinti TypeScript.

Pavyzdys: registravimas su metodo dekoratoriumi

Grįžtant prie LogCall dekoratoriaus, tai yra puikus AOP pavyzdys. Jis prideda registravimo elgseną bet kuriam metodui, nekeičiant originalaus metodo kodo. Tai atskiria „ką daryti“ (verslo logiką) nuo „kaip tai daryti“ (registravimas, našumo stebėjimas ir kt.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG AOP] Įeinama į metodą: ${String(propertyKey)} su argumentais:`, args);
    try {
      const result = originalMethod.apply(this, args);
      console.log(`[LOG AOP] Išeinama iš metodo: ${String(propertyKey)} su rezultatu:`, result);
      return result;
    } catch (error: any) {
      console.error(`[LOG AOP] Klaida metode ${String(propertyKey)}:`, error.message);
      throw error;
    }
  };
  return descriptor;
}

class PaymentProcessor {
  @LogMethod
  processPayment(amount: number, currency: string) {
    if (amount <= 0) {
      throw new Error("Mokėjimo suma turi būti teigiama.");
    }
    console.log(`Apdorojamas mokėjimas ${amount} ${currency}...`);
    return `Mokėjimas ${amount} ${currency} sėkmingai apdorotas.`;
  }

  @LogMethod
  refundPayment(transactionId: string) {
    console.log(`Grąžinamas mokėjimas transakcijos ID: ${transactionId}...`);
    return `Grąžinimas inicijuotas ${transactionId}.`;
  }
}

const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
  processor.processPayment(-50, "EUR");
} catch (error: any) {
  console.error("Pagauta klaida:", error.message);
}

Šis požiūris leidžia PaymentProcessor klasei sutelkti dėmesį tik į mokėjimų logiką, o LogMethod dekoratorius tvarko skersinį registravimo aspektą.

3. Validacija ir transformacija

Dekoratoriai yra neįtikėtinai naudingi apibrėžiant validacijos taisykles tiesiogiai savybėse arba transformuojant duomenis serializacijos/deserializacijos metu.

Pavyzdys: duomenų validacija su savybių dekoratoriais

@Required pavyzdys anksčiau jau tai demonstravo. Štai dar vienas pavyzdys su skaitinio diapazono validacija.

interface FieldValidationRule {
  property: string | symbol;
  validator: (value: any) => boolean;
  message: string;
}

const fieldValidationRules = new Map<Function, FieldValidationRule[]>();

function addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {
  const rules = fieldValidationRules.get(target.constructor) || [];
  rules.push({ property: propertyKey, validator, message });
  fieldValidationRules.set(target.constructor, rules);
}

function IsPositive(target: Object, propertyKey: string | symbol) {
  addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} turi būti teigiamas skaičius.`);
}

function MaxLength(maxLength: number) {
  return function (target: Object, propertyKey: string | symbol) {
    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} turi būti ne ilgesnis nei ${maxLength} simbolių.`);
  };
}

class Product {
  @MaxLength(50)
  name: string;

  @IsPositive
  price: number;

  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }

  static validate(instance: any): string[] {
    const errors: string[] = [];
    const rules = fieldValidationRules.get(instance.constructor) || [];
    for (const rule of rules) {
      if (!rule.validator(instance[rule.property])) {
        errors.push(rule.message);
      }
    }
    return errors;
  }
}

const product1 = new Product("Laptop", 1200);
console.log("1 produkto klaidos:", Product.validate(product1)); // []

const product2 = new Product("Labai ilgas produkto pavadinimas, kuris viršija penkiasdešimties simbolių limitą testavimo tikslais", 50);
console.log("2 produkto klaidos:", Product.validate(product2)); // ["name turi būti ne ilgesnis nei 50 simbolių."]

const product3 = new Product("Book", -10);
console.log("3 produkto klaidos:", Product.validate(product3)); // ["price turi būti teigiamas skaičius."]

Ši sąranka leidžia deklaratyviai apibrėžti validacijos taisykles savo modelio savybėse, todėl jūsų duomenų modeliai tampa savaime aprašantys savo apribojimus.

Geriausios praktikos ir svarstymai

Nors dekoratoriai yra galingi, juos reikėtų naudoti apgalvotai. Netinkamas jų naudojimas gali lemti kodą, kurį sunkiau derinti ar suprasti.

Kada naudoti dekoratorius (ir kada ne)

Našumo pasekmės

Dekoratoriai vykdomi kompiliavimo metu (arba apibrėžimo metu JavaScript vykdymo aplinkoje, jei transpiliuoti). Transformacija ar meta duomenų rinkimas vyksta, kai klasė/metodas yra apibrėžiamas, o ne kiekvieno iškvietimo metu. Todėl vykdymo laiko našumo poveikis *taikant* dekoratorius yra minimalus. Tačiau *logika viduje* jūsų dekoratorių gali turėti našumo poveikį, ypač jei jie atlieka brangias operacijas kiekvieno metodo iškvietimo metu (pvz., sudėtingi skaičiavimai metodo dekoratoriuje).

Priežiūra ir skaitomumas

Dekoratoriai, kai naudojami teisingai, gali žymiai pagerinti skaitomumą, perkeldami šabloninį kodą iš pagrindinės logikos. Tačiau jei jie atlieka sudėtingas, paslėptas transformacijas, derinimas gali tapti sudėtingas. Užtikrinkite, kad jūsų dekoratoriai būtų gerai dokumentuoti, o jų elgsena būtų nuspėjama.

Eksperimentinis statusas ir dekoratorių ateitis

Svarbu pakartoti, kad TypeScript dekoratoriai yra pagrįsti 3 etapo TC39 pasiūlymu. Tai reiškia, kad specifikacija yra daugiausia stabili, bet vis dar gali būti nežymių pakeitimų, kol taps oficialia ECMAScript standarto dalimi. Tokios sistemos kaip Angular juos priėmė, lažindamosi dėl jų galutinio standartizavimo. Tai reiškia tam tikrą rizikos lygį, nors, atsižvelgiant į jų platų pritaikymą, reikšmingi ardomieji pakeitimai yra mažai tikėtini.

TC39 pasiūlymas evoliucionavo. Dabartinis TypeScript įgyvendinimas yra pagrįstas senesne pasiūlymo versija. Yra „senųjų dekoratorių“ (Legacy Decorators) ir „standartinių dekoratorių“ (Standard Decorators) skirtumas. Kai bus priimtas oficialus standartas, TypeScript tikriausiai atnaujins savo įgyvendinimą. Daugumai programuotojų, naudojančių sistemas, šį perėjimą valdys pati sistema. Bibliotekų autoriams gali tekti suprasti subtilius skirtumus tarp senųjų ir būsimų standartinių dekoratorių.

emitDecoratorMetadata kompiliatoriaus parinktis

Ši parinktis, kai nustatyta į true tsconfig.json, nurodo TypeScript kompiliatoriui išspinduliuoti tam tikrus projektavimo metu esančius tipų meta duomenis į sukompiliuotą JavaScript. Šie meta duomenys apima konstruktoriaus parametrų tipą (design:paramtypes), metodų grąžinimo tipą (design:returntype) ir savybių tipą (design:type).

Šie išspinduliuoti meta duomenys nėra standartinės JavaScript vykdymo aplinkos dalis. Juos paprastai naudoja reflect-metadata polifilas, kuris tada padaro juos prieinamus per Reflect.getMetadata() funkcijas. Tai yra absoliučiai būtina pažangiems modeliams, tokiems kaip priklausomybių įtraukimas, kur konteineris turi žinoti priklausomybių tipus, kurių klasei reikia be aiškios konfigūracijos.

Pažangūs modeliai su dekoratoriais

Dekoratorius galima derinti ir plėsti, kad būtų sukurti dar sudėtingesni modeliai.

1. Dekoratorių dekoravimas (aukštesnės eilės dekoratoriai)

Galite kurti dekoratorius, kurie modifikuoja ar sudaro kitus dekoratorius. Tai yra rečiau pasitaikantis atvejis, bet parodo funkcinę dekoratorių prigimtį.

// Dekoratorius, užtikrinantis, kad metodas yra registruojamas ir taip pat reikalauja administratoriaus teisių
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // Pirmiausia taikomas Authorization (vidinis)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // Tada taikomas LogCall (išorinis)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // Grąžinamas modifikuotas aprašas
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`Trinama vartotojo paskyra: ${userId}`);
    return `Vartotojas ${userId} ištrintas.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Numatoma išvestis (darant prielaidą, kad vartotojas turi administratoriaus teises):
[AUTH] Prieiga prie deleteUserAccount suteikta
[LOG] Kviečiamas deleteUserAccount su argumentais: [ 'user007' ]
Trinama vartotojo paskyra: user007
[LOG] Metodas deleteUserAccount grąžino: Vartotojas user007 ištrintas.
*/

Čia AdminAndLoggedMethod yra gamykla, kuri grąžina dekoratorių, o to dekoratoriaus viduje ji taiko du kitus dekoratorius. Šis modelis gali apimti sudėtingas dekoratorių kompozicijas.

2. Dekoratorių naudojimas priemaišoms (Mixins)

Nors TypeScript siūlo kitus būdus įgyvendinti priemaišas, dekoratoriai gali būti naudojami norint deklaratyviai įtraukti galimybes į klases.

function ApplyMixins(constructors: Function[]) {
  return function (derivedConstructor: Function) {
    constructors.forEach(baseConstructor => {
      Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {
        Object.defineProperty(
          derivedConstructor.prototype,
          name,
          Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)
        );
      });
    });
  };
}

class Disposable {
  isDisposed: boolean = false;
  dispose() {
    this.isDisposed = true;
    console.log("Objektas sunaikintas.");
  }
}

class Loggable {
  log(message: string) {
    console.log(`[Loggable] ${message}`);
  }
}

@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
  // Šios savybės/metodai yra įtraukiami dekoratoriaus
  isDisposed!: boolean;
  dispose!: () => void;
  log!: (message: string) => void;

  constructor(public name: string) {
    this.log(`Resursas ${this.name} sukurtas.`);
  }

  cleanUp() {
    this.dispose();
    this.log(`Resursas ${this.name} išvalytas.`);
  }
}

const resource = new MyResource("NetworkConnection");
console.log(`Ar sunaikintas: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Ar sunaikintas: ${resource.isDisposed}`);

Šis @ApplyMixins dekoratorius dinamiškai kopijuoja metodus ir savybes iš bazių konstruktorių į išvestinės klasės prototipą, efektyviai „įmaišydamas“ funkcionalumus.

Išvada: modernaus TypeScript kūrimo įgalinimas

TypeScript dekoratoriai yra galinga ir išraiškinga funkcija, kuri įgalina naują meta duomenimis pagrįsto ir į aspektus orientuoto programavimo paradigmą. Jie leidžia programuotojams tobulinti, modifikuoti ir pridėti deklaratyvią elgseną klasėms, metodams, savybėms, prieigos metodams ir parametrams, nekeičiant jų pagrindinės logikos. Šis atsakomybių atskyrimas lemia švaresnį, lengviau prižiūrimą ir labai pakartotinai naudojamą kodą.

Nuo priklausomybių įtraukimo supaprastinimo ir tvirtų validacijos sistemų įgyvendinimo iki skersinių aspektų, tokių kaip registravimas ir našumo stebėjimas, pridėjimo, dekoratoriai siūlo elegantišką sprendimą daugeliui įprastų kūrimo iššūkių. Nors jų eksperimentinis statusas reikalauja atsargumo, jų platus pritaikymas pagrindinėse sistemose rodo jų praktinę vertę ir ateities aktualumą.

Įvaldę TypeScript dekoratorius, jūs įgyjate reikšmingą įrankį savo arsenale, leidžiantį kurti tvirtesnes, keičiamo dydžio ir išmanesnes programas. Naudokite juos atsakingai, supraskite jų mechaniką ir atrakinkite naują deklaratyvios galios lygį savo TypeScript projektuose.