Slovenščina

Raziščite moč TypeScript dekoratorjev za metapodatkovno programiranje, aspektno orientirano programiranje in izboljšanje kode z deklarativnimi vzorci. Celovit vodnik za globalne razvijalce.

TypeScript dekoratorji: Obvladovanje vzorcev metapodatkovnega programiranja za robustne aplikacije

V obsežni pokrajini sodobnega razvoja programske opreme je vzdrževanje čistih, razširljivih in obvladljivih kodnih baz ključnega pomena. TypeScript s svojim zmogljivim sistemom tipov in naprednimi funkcijami razvijalcem ponuja orodja za doseganje tega. Med njegovimi najbolj zanimivimi in transformativnimi funkcijami so dekoratorji. Čeprav so v času pisanja tega članka še vedno eksperimentalna funkcija (predlog 3. stopnje za ECMAScript), se dekoratorji široko uporabljajo v ogrodjih, kot sta Angular in TypeORM, in temeljito spreminjajo naš pristop k oblikovalskim vzorcem, metapodatkovnemu programiranju in aspektno orientiranemu programiranju (AOP).

Ta celovit vodnik se bo poglobil v TypeScript dekoratorje, raziskal njihovo mehaniko, različne tipe, praktične uporabe in najboljše prakse. Ne glede na to, ali gradite velike poslovne aplikacije, mikrostoritve ali spletne vmesnike na strani odjemalca, vam bo razumevanje dekoratorjev omogočilo pisanje bolj deklarativne, vzdržljive in zmogljive kode v TypeScriptu.

Razumevanje osnovnega koncepta: Kaj je dekorator?

V svojem bistvu je dekorator posebna vrsta deklaracije, ki jo je mogoče pripeti na deklaracijo razreda, metode, dostopa (accessor), lastnosti ali parametra. Dekoratorji so funkcije, ki vrnejo novo vrednost (ali spremenijo obstoječo) za cilj, ki ga dekorirajo. Njihov primarni namen je dodajanje metapodatkov ali spreminjanje obnašanja deklaracije, na katero so pripeti, ne da bi neposredno spreminjali osnovno strukturo kode. Ta zunanji, deklarativni način dopolnjevanja kode je izjemno močan.

Predstavljajte si dekoratorje kot opombe ali oznake, ki jih dodate delom svoje kode. Te oznake lahko nato berejo ali uporabijo drugi deli vaše aplikacije ali ogrodja, pogosto med izvajanjem, za zagotavljanje dodatne funkcionalnosti ali konfiguracije.

Sintaksa dekoratorja

Dekoratorji imajo predpono v obliki simbola @, ki mu sledi ime funkcije dekoratorja. Postavljeni so neposredno pred deklaracijo, ki jo dekorirajo.

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

Omogočanje dekoratorjev v TypeScriptu

Preden lahko uporabite dekoratorje, morate v datoteki tsconfig.json omogočiti kompilatorsko opcijo experimentalDecorators. Poleg tega boste za napredne zmožnosti refleksije metapodatkov (ki jih pogosto uporabljajo ogrodja) potrebovali tudi emitDecoratorMetadata in polifil reflect-metadata.

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

Namestiti morate tudi reflect-metadata:

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

In ga uvoziti na samem vrhu vstopne točke vaše aplikacije (npr. main.ts ali app.ts):

import "reflect-metadata";
// Sledi koda vaše aplikacije

Tovarniške funkcije dekoratorjev (Decorator Factories): Prilagajanje na dosegu roke

Medtem ko je osnovni dekorator funkcija, boste pogosto morali dekoratorju posredovati argumente za konfiguracijo njegovega obnašanja. To dosežemo z uporabo tovarniške funkcije dekoratorja. Tovarniška funkcija dekoratorja je funkcija, ki vrne dejansko funkcijo dekoratorja. Ko uporabite tovarniško funkcijo dekoratorja, jo pokličete z njenimi argumenti, ta pa nato vrne funkcijo dekoratorja, ki jo TypeScript uporabi za vašo kodo.

Primer ustvarjanja enostavne tovarniške funkcije dekoratorja

Ustvarimo tovarniško funkcijo za dekorator Logger, ki lahko beleži sporočila z različnimi predponami.

function Logger(prefix: string) {
  return function (target: Function) {
    console.log(`[${prefix}] Razred ${target.name} je bil definiran.`);
  };
}

@Logger("APP_INIT")
class ApplicationBootstrap {
  constructor() {
    console.log("Aplikacija se zaganja...");
  }
}

const app = new ApplicationBootstrap();
// Izhod:
// [APP_INIT] Razred ApplicationBootstrap je bil definiran.
// Aplikacija se zaganja...

V tem primeru je Logger("APP_INIT") klic tovarniške funkcije dekoratorja. Vrne dejansko funkcijo dekoratorja, ki kot argument prejme target: Function (konstruktor razreda). To omogoča dinamično konfiguracijo obnašanja dekoratorja.

Tipi dekoratorjev v TypeScriptu

TypeScript podpira pet različnih tipov dekoratorjev, vsak pa se uporablja za določeno vrsto deklaracije. Podpis funkcije dekoratorja se razlikuje glede na kontekst, v katerem se uporablja.

1. Dekoratorji razredov

Dekoratorji razredov se uporabljajo za deklaracije razredov. Funkcija dekoratorja kot edini argument prejme konstruktor razreda. Dekorator razreda lahko opazuje, spreminja ali celo zamenja definicijo razreda.

Podpis:

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

Vračilna vrednost:

Če dekorator razreda vrne vrednost, bo ta zamenjala deklaracijo razreda s podano konstruktorsko funkcijo. To je močna funkcija, ki se pogosto uporablja za mixine ali razširitev razredov. Če vrednost ni vrnjena, se uporabi prvotni razred.

Primeri uporabe:

Primer dekoratorja razreda: Vbrizgavanje storitve

Predstavljajmo si preprost scenarij vbrizgavanja odvisnosti, kjer želite razred označiti kot "injectable" in mu po želji dodeliti ime v vsebniku.

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(`Registrirana storitev: ${serviceName}`);

    // Opcijsko lahko tukaj vrnete nov razred za razširitev obnašanja
    return class extends constructor {
      createdAt = new Date();
      // Dodatne lastnosti ali metode za vse vbrizgane storitve
    };
  };
}

@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("--- Storitve registrirane ---");
console.log(Array.from(InjectableServiceRegistry.keys()));

const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
  const userServiceInstance = new userServiceConstructor();
  console.log("Uporabniki:", userServiceInstance.getUsers());
  // console.log("Storitev za uporabnike ustvarjena:", userServiceInstance.createdAt); // Če se uporabi vrnjeni razred
}

Ta primer prikazuje, kako lahko dekorator razreda registrira razred in celo spremeni njegov konstruktor. Dekorator Injectable omogoča, da teoretični sistem za vbrizgavanje odvisnosti odkrije razred.

2. Dekoratorji metod

Dekoratorji metod se uporabljajo za deklaracije metod. Prejmejo tri argumente: ciljni objekt (za statične člane je to konstruktorska funkcija; za člane instance je to prototip razreda), ime metode in deskriptor lastnosti (property descriptor) metode.

Podpis:

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

Vračilna vrednost:

Dekorator metode lahko vrne nov PropertyDescriptor. Če to stori, se bo ta deskriptor uporabil za definiranje metode. To vam omogoča, da spremenite ali zamenjate izvirno implementacijo metode, kar je izjemno močno za AOP.

Primeri uporabe:

Primer dekoratorja metode: Spremljanje zmogljivosti

Ustvarimo dekorator MeasurePerformance za beleženje časa izvajanja metode.

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(`Metoda "${propertyKey}" je bila izvedena v ${duration.toFixed(2)} ms`);
    return result;
  };

  return descriptor;
}

class DataProcessor {
  @MeasurePerformance
  processData(data: number[]): number[] {
    // Simulacija kompleksne, časovno potratne operacije
    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(`Podatki za ID: ${id}`);
      }, 500);
    });
  }
}

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

Dekorator MeasurePerformance ovije izvirno metodo z logiko za merjenje časa in izpiše trajanje izvajanja, ne da bi onesnažil poslovno logiko znotraj same metode. To je klasičen primer aspektno orientiranega programiranja (AOP).

3. Dekoratorji dostopov (Accessor Decorators)

Dekoratorji dostopov se uporabljajo za deklaracije dostopov (get in set). Podobno kot dekoratorji metod prejmejo ciljni objekt, ime dostopa in njegov deskriptor lastnosti.

Podpis:

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

Vračilna vrednost:

Dekorator dostopa lahko vrne nov PropertyDescriptor, ki se bo uporabil za definiranje dostopa.

Primeri uporabe:

Primer dekoratorja dostopa: Predpomnjenje getterjev

Ustvarimo dekorator, ki predpomni rezultat dragega izračuna v getterju.

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] Izračunavanje vrednosti za ${String(propertyKey)}`);
        this[cacheKey] = originalGetter.apply(this);
      } else {
        console.log(`[Cache Hit] Uporaba predpomnjene vrednosti za ${String(propertyKey)}`);
      }
      return this[cacheKey];
    };
  }
  return descriptor;
}

class ReportGenerator {
  private data: number[];

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

  // Simulira drag izračun
  @CachedGetter
  get expensiveSummary(): number {
    console.log("Izvajanje dragega izračuna povzetka...");
    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
  }
}

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

console.log("Prvi dostop:", generator.expensiveSummary);
console.log("Drugi dostop:", generator.expensiveSummary);
console.log("Tretji dostop:", generator.expensiveSummary);

Ta dekorator zagotavlja, da se izračun v getterju expensiveSummary izvede samo enkrat, nadaljnji klici pa vrnejo predpomnjeno vrednost. Ta vzorec je zelo uporaben za optimizacijo zmogljivosti, kjer dostop do lastnosti vključuje težke izračune ali zunanje klice.

4. Dekoratorji lastnosti

Dekoratorji lastnosti se uporabljajo za deklaracije lastnosti. Prejmejo dva argumenta: ciljni objekt (za statične člane je to konstruktorska funkcija; za člane instance je to prototip razreda) in ime lastnosti.

Podpis:

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

Vračilna vrednost:

Dekoratorji lastnosti ne morejo vrniti nobene vrednosti. Njihova primarna uporaba je registracija metapodatkov o lastnosti. Ne morejo neposredno spremeniti vrednosti lastnosti ali njenega deskriptorja v času dekoracije, saj deskriptor za lastnost še ni v celoti definiran, ko se izvajajo dekoratorji lastnosti.

Primeri uporabe:

Primer dekoratorja lastnosti: Validacija obveznega polja

Ustvarimo dekorator, ki označi lastnost kot "obvezno" in jo nato preveri med izvajanjem.

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)} je obvezno polje.`
  });
  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("Napake pri validaciji uporabnika 1:", validate(user1)); // []

const user2 = new UserProfile("", "Smith");
console.log("Napake pri validaciji uporabnika 2:", validate(user2)); // ["firstName je obvezno polje."]

const user3 = new UserProfile("Alice", "");
console.log("Napake pri validaciji uporabnika 3:", validate(user3)); // ["lastName je obvezno polje."]

Dekorator Required preprosto registrira validacijsko pravilo v centralni mapi validationRules. Ločena funkcija validate nato uporabi te metapodatke za preverjanje instance med izvajanjem. Ta vzorec ločuje validacijsko logiko od definicije podatkov, kar jo naredi ponovno uporabno in čisto.

5. Dekoratorji parametrov

Dekoratorji parametrov se uporabljajo za parametre znotraj konstruktorja razreda ali metode. Prejmejo tri argumente: ciljni objekt (za statične člane je to konstruktorska funkcija; za člane instance je to prototip razreda), ime metode (ali undefined za parametre konstruktorja) in vrstni red (indeks) parametra na seznamu parametrov funkcije.

Podpis:

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

Vračilna vrednost:

Dekoratorji parametrov ne morejo vrniti nobene vrednosti. Podobno kot dekoratorji lastnosti je njihova primarna vloga dodajanje metapodatkov o parametru.

Primeri uporabe:

Primer dekoratorja parametra: Vbrizgavanje podatkov iz zahtevka

Simulirajmo, kako bi spletno ogrodje lahko uporabilo dekoratorje parametrov za vbrizgavanje določenih podatkov v parameter metode, na primer ID uporabnika iz zahtevka.

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);
  };
}

// Hipotetična funkcija ogrodja za klic metode z razrešenimi parametri
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(`Pridobivanje uporabnika z ID: ${userId}, žeton: ${authToken || "N/A"}`);
    return { id: userId, name: "Jane Doe" };
  }

  deleteUser(@RequestParam("id") userId: string) {
    console.log(`Brisanje uporabnika z ID: ${userId}`);
    return { status: "deleted", id: userId };
  }
}

const userController = new UserController();

// Simulacija dohodnega zahtevka
const mockRequest = {
  id: "user123",
  token: "abc-123",
  someOtherProp: "xyz"
};

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

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

Ta primer prikazuje, kako lahko dekoratorji parametrov zbirajo informacije o zahtevanih parametrih metode. Ogrodje lahko nato uporabi te zbrane metapodatke za samodejno razreševanje in vbrizgavanje ustreznih vrednosti ob klicu metode, kar znatno poenostavi logiko kontrolerja ali storitve.

Sestavljanje dekoratorjev in vrstni red izvajanja

Dekoratorje je mogoče uporabiti v različnih kombinacijah, in razumevanje njihovega vrstnega reda izvajanja je ključno za predvidevanje obnašanja in izogibanje nepričakovanim težavam.

Več dekoratorjev na enem cilju

Ko se na eno deklaracijo (npr. razred, metodo ali lastnost) uporabi več dekoratorjev, se ti izvajajo v določenem vrstnem redu: od spodaj navzgor ali od desne proti levi, za njihovo evalvacijo. Vendar pa se njihovi rezultati uporabijo v obratnem vrstnem redu.

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

Tukaj se bo najprej evalviral DecoratorB, nato DecoratorA. Če spreminjajo razred (npr. z vračanjem novega konstruktorja), bo sprememba iz DecoratorA ovila ali se uporabila nad spremembo iz DecoratorB.

Primer: Veriženje dekoratorjev metod

Razmislimo o dveh dekoratorjih metod: LogCall in Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] Klicanje ${String(propertyKey)} z argumenti:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] Metoda ${String(propertyKey)} je vrnila:`, 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"]; // Simulacija pridobivanja vlog trenutnega uporabnika
      const authorized = roles.some(role => currentUserRoles.includes(role));
      if (!authorized) {
        console.warn(`[AUTH] Dostop zavrnjen za ${String(propertyKey)}. Zahtevane vloge: ${roles.join(", ")}`);
        throw new Error("Nepooblaščen dostop");
      }
      console.log(`[AUTH] Dostop dovoljen za ${String(propertyKey)}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class SecureService {
  @LogCall
  @Authorization(["admin"])
  deleteSensitiveData(id: string) {
    console.log(`Brisanje občutljivih podatkov za ID: ${id}`);
    return `Podatki ID ${id} izbrisani.`;
  }

  @Authorization(["user"])
  @LogCall // Vrstni red je tukaj spremenjen
  fetchPublicData(query: string) {
    console.log(`Pridobivanje javnih podatkov s poizvedbo: ${query}`);
    return `Javni podatki za poizvedbo: ${query}`; 
  }
}

const service = new SecureService();

try {
  console.log("\n--- Klicanje deleteSensitiveData (Admin uporabnik) ---");
  service.deleteSensitiveData("record123");
} catch (error: any) {
  console.error(error.message);
}

try {
  console.log("\n--- Klicanje fetchPublicData (Ne-Admin uporabnik) ---");
  // Simulacija ne-admin uporabnika, ki poskuša dostopiti do fetchPublicData, ki zahteva vlogo 'user'
  const mockUserRoles = ["guest"]; // To bo neuspešno pri avtorizaciji
  // Za dinamičnost bi potrebovali sistem DI ali statični kontekst za vloge trenutnega uporabnika.
  // Za poenostavitev predpostavljamo, da ima dekorator Authorization dostop do konteksta trenutnega uporabnika.
  // Za demo namene prilagodimo dekorator Authorization, da vedno predpostavlja 'admin',
  // tako da prvi klic uspe, drugi pa ne, da pokažemo različne poti.
  
  // Ponovni zagon z vlogo 'user', da fetchPublicData uspe.
  // Predstavljajte si, da currentUserRoles v Authorization postane: ['user']
  // V tem primeru pustimo enostavno in pokažemo učinek vrstnega reda.
  service.fetchPublicData("iskalni izraz"); // To bo izvedlo Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* Pričakovan izhod za deleteSensitiveData:
[AUTH] Dostop dovoljen za deleteSensitiveData
[LOG] Klicanje deleteSensitiveData z argumenti: [ 'record123' ]
Brisanje občutljivih podatkov za ID: record123
[LOG] Metoda deleteSensitiveData je vrnila: Podatki ID record123 izbrisani.
*/

/* Pričakovan izhod za fetchPublicData (če ima uporabnik vlogo 'user'):
[LOG] Klicanje fetchPublicData z argumenti: [ 'iskalni izraz' ]
[AUTH] Dostop dovoljen za fetchPublicData
Pridobivanje javnih podatkov s poizvedbo: iskalni izraz
[LOG] Metoda fetchPublicData je vrnila: Javni podatki za poizvedbo: iskalni izraz
*/

Opazite vrstni red: za deleteSensitiveData se najprej izvede Authorization (spodaj), nato pa ga LogCall (zgoraj) ovije. Notranja logika Authorization se izvede prva. Za fetchPublicData se najprej izvede LogCall (spodaj), nato pa ga Authorization (zgoraj) ovije. To pomeni, da bo aspekt LogCall zunaj aspekta Authorization. Ta razlika je ključna za presečne zadeve (cross-cutting concerns), kot so beleženje ali obravnavanje napak, kjer lahko vrstni red izvajanja znatno vpliva na obnašanje.

Vrstni red izvajanja za različne cilje

Kadar imajo razred, njegovi člani in parametri vsi dekoratorje, je vrstni red izvajanja natančno določen:

  1. Dekoratorji parametrov se uporabijo prvi, za vsak parameter, od zadnjega parametra do prvega.
  2. Nato se za vsakega člana uporabijo dekoratorji metod, dostopov ali lastnosti.
  3. Na koncu se na sam razred uporabijo dekoratorji razredov.

Znotraj vsake kategorije se več dekoratorjev na istem cilju uporabi od spodaj navzgor (ali od desne proti levi).

Primer: Celoten vrstni red izvajanja

function log(message: string) {
  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
    if (typeof descriptorOrIndex === 'number') {
      console.log(`Dekorator parametra: ${message} na parametru #${descriptorOrIndex} od ${String(propertyKey || "constructor")}`);
    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
        console.log(`Dekorator metode/dostopa: ${message} na ${String(propertyKey)}`);
      } else {
        console.log(`Dekorator lastnosti: ${message} na ${String(propertyKey)}`);
      }
    } else {
      console.log(`Dekorator razreda: ${message} na ${target.name}`);
    }
    return descriptorOrIndex; // Vrne deskriptor za metodo/dostop, undefined za druge
  };
}

@log("Nivo razreda D")
@log("Nivo razreda C")
class MyDecoratedClass {
  @log("Statična lastnost A")
  static staticProp: string = "";

  @log("Lastnost instance B")
  instanceProp: number = 0;

  @log("Metoda D")
  @log("Metoda C")
  myMethod(
    @log("Parameter Z") paramZ: string,
    @log("Parameter Y") paramY: number
  ) {
    console.log("Metoda myMethod izvedena.");
  }

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

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

  constructor() {
    console.log("Konstruktor izveden.");
  }
}

new MyDecoratedClass();
// Klic metode za sprožitev dekoratorja metode
new MyDecoratedClass().myMethod("hello", 123);

/* Predviden vrstni red izhoda (približen, odvisno od specifične različice TypeScripta in kompilacije):
Dekorator parametra: Parameter Y na parametru #1 od myMethod
Dekorator parametra: Parameter Z na parametru #0 od myMethod
Dekorator lastnosti: Statična lastnost A na staticProp
Dekorator lastnosti: Lastnost instance B na instanceProp
Dekorator metode/dostopa: Getter/Setter F na myAccessor
Dekorator metode/dostopa: Metoda C na myMethod
Dekorator metode/dostopa: Metoda D na myMethod
Dekorator razreda: Nivo razreda C na MyDecoratedClass
Dekorator razreda: Nivo razreda D na MyDecoratedClass
Konstruktor izveden.
Metoda myMethod izvedena.
*/

Točen čas izpisa na konzolo se lahko nekoliko razlikuje glede na to, kdaj se pokliče konstruktor ali metoda, vendar vrstni red, v katerem se izvedejo same funkcije dekoratorjev (in s tem uporabijo njihovi stranski učinki ali vrnjene vrednosti), sledi zgornjim pravilom.

Praktične uporabe in oblikovalski vzorci z dekoratorji

Dekoratorji, zlasti v povezavi s polifilom reflect-metadata, odpirajo novo področje metapodatkovno vodenega programiranja. To omogoča močne oblikovalske vzorce, ki abstrahirajo odvečno kodo in presečne zadeve.

1. Vbrizgavanje odvisnosti (DI)

Ena najvidnejših uporab dekoratorjev je v ogrodjih za vbrizgavanje odvisnosti (kot so Angularjev @Injectable(), @Component() itd. ali obsežna uporaba DI v NestJS). Dekoratorji vam omogočajo, da deklarirate odvisnosti neposredno na konstruktorjih ali lastnostih, kar ogrodju omogoča samodejno instanciranje in zagotavljanje pravilnih storitev.

Primer: Poenostavljeno vbrizgavanje storitev

import "reflect-metadata"; // Nujno za 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(`Razred ${target.name} ni označen kot @Injectable.`);
    }

    // Pridobi tipe parametrov konstruktorja (zahteva emitDecoratorMetadata)
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // Uporabi ekspliciten @Inject žeton, če je podan, sicer ugotovi tip
      const token = explicitInjections[index] || paramType;
      if (token === undefined) {
        throw new Error(`Ni mogoče razrešiti parametra na indeksu ${index} za ${target.name}. Morda gre za krožno odvisnost ali primitivni tip brez eksplicitnega @Inject.`);
      }
      return Container.resolve(token);
    });

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

// Definiraj storitve
@Injectable()
class DatabaseService {
  connect() {
    console.log("Povezovanje z bazo podatkov...");
    return "Povezava z bazo";
  }
}

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

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

  login() {
    console.log(`AuthService: Avtentikacija z uporabo ${this.db.connect()}`);
    return "Uporabnik prijavljen";
  }
}

@Injectable()
class UserService {
  private authService: AuthService;
  private dbService: DatabaseService; // Primer vbrizgavanja preko lastnosti z uporabo custom dekoratorja ali funkcije ogrodja

  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: Pridobivanje profila uporabnika...");
    return { id: 1, name: "Global User" };
  }
}

// Razreši glavno storitev
console.log("--- Razreševanje UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("\n--- Razreševanje AuthService (bi morala biti predpomnjena) ---");
const authService = Container.resolve(AuthService);
authService.login();

Ta zapleten primer prikazuje, kako dekoratorji @Injectable in @Inject, v kombinaciji z reflect-metadata, omogočajo, da custom Container samodejno razreši in zagotovi odvisnosti. Metapodatki design:paramtypes, ki jih TypeScript samodejno odda (ko je emitDecoratorMetadata nastavljen na true), so tukaj ključni.

2. Aspektno orientirano programiranje (AOP)

AOP se osredotoča na modularizacijo presečnih zadev (npr. beleženje, varnost, transakcije), ki prečkajo več razredov in modulov. Dekoratorji so odlično orodje za implementacijo konceptov AOP v TypeScriptu.

Primer: Beleženje z dekoratorjem metode

Če se vrnemo k dekoratorju LogCall, je to popoln primer AOP. Dodaja obnašanje beleženja kateri koli metodi, ne da bi spreminjal izvirno kodo metode. To ločuje "kaj narediti" (poslovna logika) od "kako to narediti" (beleženje, spremljanje zmogljivosti itd.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG AOP] Vstop v metodo: ${String(propertyKey)} z argumenti:`, args);
    try {
      const result = originalMethod.apply(this, args);
      console.log(`[LOG AOP] Izhod iz metode: ${String(propertyKey)} z rezultatom:`, result);
      return result;
    } catch (error: any) {
      console.error(`[LOG AOP] Napaka v metodi ${String(propertyKey)}:`, error.message);
      throw error;
    }
  };
  return descriptor;
}

class PaymentProcessor {
  @LogMethod
  processPayment(amount: number, currency: string) {
    if (amount <= 0) {
      throw new Error("Znesek plačila mora biti pozitiven.");
    }
    console.log(`Obdelava plačila v znesku ${amount} ${currency}...`);
    return `Plačilo v znesku ${amount} ${currency} uspešno obdelano.`;
  }

  @LogMethod
  refundPayment(transactionId: string) {
    console.log(`Vračilo plačila za transakcijo ID: ${transactionId}...`);
    return `Vračilo sproženo za ${transactionId}.`;
  }
}

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

Ta pristop ohranja razred PaymentProcessor osredotočen izključno na logiko plačil, medtem ko dekorator LogMethod skrbi za presečno zadevo beleženja.

3. Validacija in transformacija

Dekoratorji so izjemno uporabni za definiranje validacijskih pravil neposredno na lastnostih ali za transformacijo podatkov med serializacijo/deserializacijo.

Primer: Validacija podatkov z dekoratorji lastnosti

Primer @Required je to že prikazal. Tukaj je še en primer z validacijo numeričnega obsega.

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)} mora biti pozitivno število.`);
}

function MaxLength(maxLength: number) {
  return function (target: Object, propertyKey: string | symbol) {
    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} mora imeti največ ${maxLength} znakov.`);
  };
}

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("Prenosnik", 1200);
console.log("Napake izdelka 1:", Product.validate(product1)); // []

const product2 = new Product("Zelo dolgo ime izdelka ki presega omejitev petdesetih znakov za namen testiranja", 50);
console.log("Napake izdelka 2:", Product.validate(product2)); // ["name mora imeti največ 50 znakov."]

const product3 = new Product("Knjiga", -10);
console.log("Napake izdelka 3:", Product.validate(product3)); // ["price mora biti pozitivno število."]

Ta postavitev vam omogoča deklarativno definiranje validacijskih pravil na lastnostih vašega modela, kar naredi vaše podatkovne modele samoopisne glede na njihove omejitve.

Najboljše prakse in premisleki

Čeprav so dekoratorji močni, jih je treba uporabljati preudarno. Zloraba lahko vodi do kode, ki jo je težje odpravljati ali razumeti.

Kdaj uporabiti dekoratorje (in kdaj ne)

Vpliv na zmogljivost

Dekoratorji se izvedejo ob prevajanju (ali v času definicije v JavaScript okolju, če so transpilani). Transformacija ali zbiranje metapodatkov se zgodi, ko je razred/metoda definirana, ne ob vsakem klicu. Zato je vpliv *uporabe* dekoratorjev na zmogljivost med izvajanjem minimalen. Vendar pa lahko *logika znotraj* vaših dekoratorjev vpliva na zmogljivost, zlasti če izvajajo drage operacije ob vsakem klicu metode (npr. kompleksni izračuni znotraj dekoratorja metode).

Vzdržljivost in berljivost

Dekoratorji, če se uporabljajo pravilno, lahko znatno izboljšajo berljivost s premikanjem odvečne kode iz glavne logike. Vendar, če izvajajo kompleksne, skrite transformacije, lahko odpravljanje napak postane zahtevno. Poskrbite, da so vaši dekoratorji dobro dokumentirani in da je njihovo obnašanje predvidljivo.

Eksperimentalni status in prihodnost dekoratorjev

Pomembno je ponoviti, da TypeScript dekoratorji temeljijo na predlogu TC39 3. stopnje. To pomeni, da je specifikacija večinoma stabilna, vendar bi se lahko pred uradnim vključitvijo v standard ECMAScript še vedno zgodile manjše spremembe. Ogrodja, kot je Angular, so jih sprejela in stavijo na njihovo končno standardizacijo. To pomeni določeno stopnjo tveganja, čeprav so glede na njihovo široko uporabo pomembne prelomne spremembe malo verjetne.

Predlog TC39 se je razvijal. Trenutna implementacija TypeScripta temelji na starejši različici predloga. Obstaja razlika med "Legacy Decorators" in "Standard Decorators". Ko bo uradni standard sprejet, bo TypeScript verjetno posodobil svojo implementacijo. Za večino razvijalcev, ki uporabljajo ogrodja, bo ta prehod upravljalo samo ogrodje. Za avtorje knjižnic pa bo morda postalo potrebno razumevanje subtilnih razlik med starimi in prihodnjimi standardnimi dekoratorji.

Kompilatorska opcija emitDecoratorMetadata

Ta opcija, ko je v tsconfig.json nastavljena na true, naroči prevajalniku TypeScripta, da v prevedeni JavaScript odda določene metapodatke o tipih iz časa načrtovanja. Ti metapodatki vključujejo tip parametrov konstruktorja (design:paramtypes), povratni tip metod (design:returntype) in tip lastnosti (design:type).

Ti oddani metapodatki niso del standardnega izvajalskega okolja JavaScripta. Običajno jih porabi polifil reflect-metadata, ki jih nato naredi dostopne preko funkcij Reflect.getMetadata(). To je absolutno ključno za napredne vzorce, kot je vbrizgavanje odvisnosti, kjer mora vsebnik poznati tipe odvisnosti, ki jih razred potrebuje, brez eksplicitne konfiguracije.

Napredni vzorci z dekoratorji

Dekoratorje je mogoče kombinirati in razširiti za gradnjo še bolj sofisticiranih vzorcev.

1. Dekoriranje dekoratorjev (dekoratorji višjega reda)

Ustvarite lahko dekoratorje, ki spreminjajo ali sestavljajo druge dekoratorje. To je manj pogosto, vendar prikazuje funkcionalno naravo dekoratorjev.

// Dekorator, ki zagotavlja, da je metoda beležena in zahteva tudi administratorske vloge
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // Najprej uporabi Authorization (notranji)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // Nato uporabi LogCall (zunanji)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // Vrne spremenjen deskriptor
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`Brisanje uporabniškega računa: ${userId}`);
    return `Uporabnik ${userId} izbrisan.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Pričakovan izhod (ob predpostavki administratorske vloge):
[AUTH] Dostop dovoljen za deleteUserAccount
[LOG] Klicanje deleteUserAccount z argumenti: [ 'user007' ]
Brisanje uporabniškega računa: user007
[LOG] Metoda deleteUserAccount je vrnila: Uporabnik user007 izbrisan.
*/

Tukaj je AdminAndLoggedMethod tovarniška funkcija, ki vrne dekorator, znotraj katerega uporabi dva druga dekoratorja. Ta vzorec lahko inkapsulira kompleksne sestave dekoratorjev.

2. Uporaba dekoratorjev za Mixine

Čeprav TypeScript ponuja druge načine za implementacijo mixinov, se lahko dekoratorji uporabijo za vbrizgavanje zmožnosti v razrede na deklarativen način.

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("Objekt zavržen.");
  }
}

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

@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
  // Te lastnosti/metode so vbrizgane s strani dekoratorja
  isDisposed!: boolean;
  dispose!: () => void;
  log!: (message: string) => void;

  constructor(public name: string) {
    this.log(`Vir ${this.name} ustvarjen.`);
  }

  cleanUp() {
    this.dispose();
    this.log(`Vir ${this.name} očiščen.`);
  }
}

const resource = new MyResource("NetworkConnection");
console.log(`Je zavržen: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Je zavržen: ${resource.isDisposed}`);

Ta dekorator @ApplyMixins dinamično kopira metode in lastnosti iz osnovnih konstruktorjev v prototip izpeljanega razreda, s čimer učinkovito "primeša" funkcionalnosti.

Zaključek: Opolnomočenje sodobnega razvoja v TypeScriptu

TypeScript dekoratorji so močna in izrazna funkcija, ki omogoča novo paradigmo metapodatkovno vodenega in aspektno orientiranega programiranja. Razvijalcem omogočajo izboljšanje, spreminjanje in dodajanje deklarativnih obnašanj razredom, metodam, lastnostim, dostopom in parametrom, ne da bi spreminjali njihovo osrednjo logiko. Ta ločitev zadev vodi do čistejše, bolj vzdržljive in visoko ponovno uporabne kode.

Od poenostavljanja vbrizgavanja odvisnosti in implementacije robustnih validacijskih sistemov do dodajanja presečnih zadev, kot sta beleženje in spremljanje zmogljivosti, dekoratorji ponujajo elegantno rešitev za mnoge pogoste razvojne izzive. Medtem ko njihov eksperimentalni status zahteva zavedanje, njihova široka uporaba v večjih ogrodjih kaže na njihovo praktično vrednost in prihodnjo relevantnost.

Z obvladovanjem TypeScript dekoratorjev pridobite pomembno orodje v svojem arzenalu, ki vam omogoča gradnjo bolj robustnih, razširljivih in inteligentnih aplikacij. Uporabljajte jih odgovorno, razumite njihovo mehaniko in odklenite novo raven deklarativne moči v svojih TypeScript projektih.