Magyar

Fedezze fel a TypeScript Decoratorok erejét a metaadat programozásban, aspektusorientált programozásban és deklaratív mintákkal történő kódfejlesztésben. Átfogó útmutató globális fejlesztőknek.

TypeScript Decoratorok: Metaadat Programozási Minták Mesterei Robusztus Alkalmazásokhoz

A modern szoftverfejlesztés hatalmas tájképében a tiszta, skálázható és kezelhető kódalapok fenntartása elsődleges fontosságú. A TypeScript, erőteljes típusrendszerével és fejlett funkcióival, olyan eszközöket kínál a fejlesztőknek, amelyekkel ezt elérhetik. Az egyik leginkább lenyűgöző és átalakító funkciója a Decoratorok. Bár írás idején még kísérleti funkció (ECMAScript 3. szakasz javaslata), a decoratorokat széles körben használják olyan keretrendszerekben, mint az Angular és a TypeORM, alapvetően megváltoztatva, hogyan közelítjük meg a tervezési mintákat, a metaadat programozást és az aspektusorientált programozást (AOP).

Ez az átfogó útmutató mélyen elmerül a TypeScript decoratorok világában, feltárva azok mechanizmusait, különféle típusait, gyakorlati alkalmazásait és legjobb gyakorlatait. Függetlenül attól, hogy nagyszabású vállalati alkalmazásokat, mikroszolgáltatásokat vagy kliens-oldali webes felületeket épít, a decoratorok megértése felhatalmazza Önt arra, hogy deklaratívabb, könnyebben karbantartható és erőteljesebb TypeScript kódot írjon.

A Mag Koncepció Megértése: Mi az a Decorator?

Lényegében a decorator egy speciális deklaráció, amelyet egy osztálydeklarációhoz, metódushoz, accessorhoz, tulajdonsághoz vagy paraméterhez lehet csatolni. A decoratorok olyan függvények, amelyek új értéket adnak vissza (vagy módosítják a meglévőt) a célhoz, amelyet díszítenek. Elsődleges céljuk a metaadatok hozzáadása vagy a díszített deklaráció viselkedésének megváltoztatása anélkül, hogy közvetlenül módosítanák az alapul szolgáló kódstruktúrát. Ez a külső, deklaratív módon történő kód-kiegészítés hihetetlenül erőteljes.

Gondoljon a decoratorokra úgy, mint annotációkra vagy címkékre, amelyeket a kód részeihez alkalmaz. Ezeket a címkéket aztán más alkalmazásrészek vagy keretrendszerek olvashatják vagy feldolgozhatják, gyakran futásidőben, további funkcionalitás vagy konfiguráció biztosítása érdekében.

A Decorator Szintaxisa

A decoratorokat az @ szimbólum előzi meg, amelyet a decorator függvény neve követ. Azonnal a díszített deklaráció elé helyezik őket.

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

Decoratorok Engedélyezése TypeScriptben

Mielőtt használhatná a decoratorokat, engedélyeznie kell az experimentalDecorators fordító opciót a tsconfig.json fájljában. Ezenkívül a fejlett metaadat-reflexiós képességekhez (amelyeket gyakran használnak a keretrendszerek) szüksége lesz az emitDecoratorMetadata és a reflect-metadata polyfillra is.

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

Telepítenie kell a reflect-metadata csomagot is:

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

És importálnia kell az alkalmazás belépési pontjának legtetejére (pl. main.ts vagy app.ts):

import "reflect-metadata";
// Az Ön alkalmazás kódja következik

Decorator Gyárak: Testreszabás az Ujjak Végén

Míg az alapvető decorator egy függvény, gyakran át kell adnia argumentumokat a decoratornak a viselkedésének konfigurálásához. Ezt egy decorator gyár használatával érhetjük el. A decorator gyár egy függvény, amely visszaadja az aktuális decorator függvényt. Amikor egy decorator gyárat alkalmaz, argumentumaival hívja meg, és az visszaadja a decorator függvényt, amelyet a TypeScript a kódunkra alkalmaz.

Egyszerű Decorator Gyár Példa Létrehozása

Hozunk létre egy gyárat egy Logger decoratorhoz, amely különböző előtagokkal tud log üzeneteket naplózni.

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();
// Kimenet:
// [APP_INIT] Class ApplicationBootstrap has been defined.
// Application is starting...

Ebben a példában a Logger("APP_INIT") a decorator gyár hívása. Visszaadja az aktuális decorator függvényt, amely argumentumként a target: Function (az osztály konstruktora) kap. Ez lehetővé teszi a decorator viselkedésének dinamikus konfigurálását.

TypeScript Decoratorok Típusai

A TypeScript öt különálló decorator típust támogat, amelyek mindegyike egy adott deklarációhoz alkalmazható. A decorator függvény aláírása attól függően változik, hogy milyen kontextusban alkalmazzák.

1. Osztály Decoratorok

Az osztály decoratorokat az osztálydeklarációkhoz alkalmazzák. A decorator függvény az osztály konstruktorát kapja meg egyetlen argumentumként. Az osztály decorator megfigyelheti, módosíthatja vagy akár helyettesítheti az osztály definícióját.

Aláírás:

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

Visszaadott Érték:

Ha az osztály decorator értéket ad vissza, akkor az osztálydeklarációt a megadott konstruktor függvénnyel helyettesíti. Ez egy hatékony funkció, amelyet gyakran használnak mixinek vagy osztály-kiegészítésekhez. Ha nem ad vissza értéket, az eredeti osztályt használja.

Használati Esetek:

Osztály Decorator Példa: Szolgáltatás Injektálása

Képzeljen el egy egyszerű függőségi injektálási forgatókönyvet, ahol egy osztályt "injektálhatónak" szeretne megjelölni, és opcionálisan nevet adni neki egy konténerben.

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

    // Opcionálisan itt visszaadhat egy új osztályt a viselkedés kiegészítéséhez
    return class extends constructor {
      createdAt = new Date();
      // További tulajdonságok vagy metódusok minden injektált szolgáltatáshoz
    };
  };
}

@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); // Ha a visszaadott osztályt használják
}

Ez a példa bemutatja, hogyan regisztrálhat egy osztályt egy osztály decorator, és akár módosíthatja a konstruktorát is. Az Injectable decorator az osztályt felfedezhetővé teszi egy elméleti függőségi injektáló rendszer számára.

2. Metódus Decoratorok

A metódus decoratorokat a metódusdeklarációkhoz alkalmazzák. Három argumentumot kapnak: a célobjektumot (statikus tagok esetén a konstruktor függvényt; példány tagok esetén az osztály prototípusát), a metódus nevét és a metódus tulajdonságdeszkriptorát.

Aláírás:

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

Visszaadott Érték:

Egy metódus decorator egy új PropertyDescriptor-t adhat vissza. Ha megtartja, ezt a deszkriptort használják a metódus definiálásához. Ez lehetővé teszi az eredeti metódus implementációjának módosítását vagy helyettesítését, ami hihetetlenül erőteljessé teszi az AOP számára.

Használati Esetek:

Metódus Decorator Példa: Teljesítménymérés

Hozunk létre egy MeasurePerformance decoratorot, amely naplózza egy metódus végrehajtási idejét.

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[] {
    // Szimuláljon egy összetett, időigényes műveletet
    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));

A MeasurePerformance decorator időzítési logikával csomagolja be az eredeti metódust, megjelenítve a végrehajtási időtartamot anélkül, hogy magában a metódusban belerondítana az üzleti logikával. Ez egy klasszikus példa az Aspektusorientált Programozásra (AOP).

3. Accessor Decoratorok

Az accessor decoratorokat az accessor (get és set) deklarációkhoz alkalmazzák. Hasonlóan a metódus decoratorokhoz, ezek is megkapják a célobjektumot, az accessor nevét és a tulajdonságdeszkriptorát.

Aláírás:

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

Visszaadott Érték:

Egy accessor decorator visszaadhat egy új PropertyDescriptor-t, amelyet az accessor definiálásához használnak.

Használati Esetek:

Accessor Decorator Példa: Gyorsítótárazott Getter-ek

Hozunk létre egy decoratorot, amely gyorsítótárazza egy drága getter számítás eredményét.

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

  // Szimuláljon egy drága számítást
  @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);

Ez a decorator biztosítja, hogy az expensiveSummary getter számítása csak egyszer fusson le, a későbbi hívások a gyorsítótárazott értéket adják vissza. Ez a minta nagyon hasznos a teljesítmény optimalizálásához, ahol a tulajdonság-hozzáférés nehéz számításokat vagy külső hívásokat foglal magában.

4. Tulajdonság Decoratorok

A tulajdonság decoratorokat a tulajdonságdeklarációkhoz alkalmazzák. Két argumentumot kapnak: a célobjektumot (statikus tagok esetén a konstruktor függvényt; példány tagok esetén az osztály prototípusát), és a tulajdonság nevét.

Aláírás:

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

Visszaadott Érték:

A tulajdonság decoratorok nem adhatnak vissza semmilyen értéket. Elsődleges használatuk a tulajdonságra vonatkozó metaadatok regisztrálása. Nem tudják közvetlenül megváltoztatni a tulajdonság értékét vagy deszkriptorát a díszítés pillanatában, mivel a tulajdonság deszkriptora még nem teljesen definiált a tulajdonság decoratorok futásakor.

Használati Esetek:

Tulajdonság Decorator Példa: Kötelező Mező Érvényesítése

Hozunk létre egy decoratorot egy tulajdonság "kötelező" megjelölésére, majd futásidőben érvényesítsük.

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."]

A Required decorator egyszerűen regisztrálja az érvényesítési szabályt egy központi validationRules térképen. Egy külön validate függvény ezután ezt a metaadatot használja az példány futásidőben történő ellenőrzésére. Ez a minta elválasztja az érvényesítési logikát az adatdefiníciótól, újrafelhasználhatóvá és tisztává téve.

5. Paraméter Decoratorok

A paraméter decoratorokat egy osztály konstruktorának vagy metódusának paramétereihez alkalmazzák. Három argumentumot kapnak: a célobjektumot (statikus tagok esetén a konstruktor függvényt; példány tagok esetén az osztály prototípusát), a metódus nevét (vagy undefined a konstruktor paraméterei esetén), és a paraméter indexét a függvény paraméterlistájában.

Aláírás:

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

Visszaadott Érték:

A paraméter decoratorok nem adhatnak vissza semmilyen értéket. A tulajdonság decoratorokhoz hasonlóan elsődleges szerepük a paraméterrel kapcsolatos metaadatok hozzáadása.

Használati Esetek:

Paraméter Decorator Példa: Kérés Adatainak Injektálása

Szimuláljuk, hogyan használhat egy webes keretrendszer paraméter decoratorokat specifikus adatok injektálásához egy metódus paraméterébe, például egy felhasználói azonosítót egy kérésből.

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

// Egy hipotetikus keretrendszeri függvény egy metódus meghívásához feloldott paraméterekkel
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();

// Szimuláljon egy bejövő kérést
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" });

Ez a példa bemutatja, hogyan gyűjthetnek össze a paraméter decoratorok információt a szükséges metódus paraméterekről. Egy keretrendszer ezután felhasználhatja ezt az összegyűjtött metaadatot a megfelelő értékek automatikus feloldására és injektálására, amikor a metódus meghívásra kerül, jelentősen leegyszerűsítve a vezérlő vagy szolgáltatási logikát.

Decorator Összetétel és Végrehajtási Sorrend

A decoratorokat különféle kombinációkban lehet alkalmazni, és a végrehajtási sorrendjük megértése kulcsfontosságú a viselkedés előrejelzéséhez és a váratlan problémák elkerüléséhez.

Több Decorator Egy Célon

Amikor több decoratorot alkalmaznak egyetlen deklarációhoz (pl. egy osztályhoz, metódushoz vagy tulajdonsághoz), akkor azok egy specifikus sorrendben hajtódnak végre: alulról felfelé, vagy jobbról balra az értékelésük során. Azonban az eredményeik fordított sorrendben kerülnek alkalmazásra.

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

Itt először a DecoratorB értékelődik ki, majd a DecoratorA. Ha módosítják az osztályt (pl. egy új konstruktor visszaadásával), akkor a DecoratorA által végzett módosítás a DecoratorB módosítását fogja becsomagolni vagy ráalkalmazni.

Példa: Metódus Decoratorok Láncolása

Tekintsünk két metódus decoratorot: LogCall és 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"]; // Szimuláljon felhasználói szerepkörök lekérését
      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 // Itt megváltozott a sorrend
  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) ---");
  // Szimuláljon egy nem admin felhasználót, aki megpróbál hozzáférni a fetchPublicData-hoz, amihez 'user' szerepkör szükséges
  const mockUserRoles = ["guest"]; // Ez hibával jár az auth-nál
  // Ahhoz, hogy ez dinamikus legyen, szükség lenne egy DI rendszerre vagy statikus kontextusra a jelenlegi felhasználói szerepkörökhöz.
  // Egyszerűség kedvéért feltételezzük, hogy az Authorization decorator hozzáfér a jelenlegi felhasználói kontextushoz.
  // Kezeljük ezt úgy, hogy az Authorization decorator mindig "admin"-nak tekintse magát a demó kedvéért, 
  // így az első hívás sikeres lesz, a második pedig hibázik, hogy megmutassuk a különböző utakat.
  
  // Újrafuttatás a fetchPublicData 'user' szerepkörével a siker érdekében.
  // Képzelje el, hogy az Authorization decoratorban a currentUserRoles a következő lesz: ['user']
  // Ebben a példában egyszerűen tartsuk meg, és mutassuk meg a sorrend hatását.
  service.fetchPublicData("search term"); // Ez végrehajtja az Auth -> Log műveleteket
} catch (error: any) {
  console.error(error.message);
}

/* Várható kimenet a deleteSensitiveData esetében:
[AUTH] Access granted for deleteUserAccount
[LOG] Calling deleteUserAccount with args: [ 'record123' ]
Deleting sensitive data for ID: record123
[LOG] Method deleteUserAccount returned: Data ID record123 deleted.
*/

/* Várható kimenet a fetchPublicData esetében (ha a felhasználó rendelkezik 'user' szerepkörrel):
[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
*/

Figyelje meg a sorrendet: a deleteSensitiveData esetében először az Authorization (alul) fut le, majd a LogCall (felül) csomagolja be. Az Authorization belső logikája hajtódik végre először. A fetchPublicData esetében először a LogCall (alul) fut le, majd a Authorization (felül) csomagolja be. Ez azt jelenti, hogy a LogCall aspektus a Authorization aspektuson kívül lesz. Ez a különbség kritikus a keresztirányú aggályok, mint a naplózás vagy a hibakezelés esetében, ahol a végrehajtási sorrend jelentősen befolyásolhatja a viselkedést.

Végrehajtási Sorrend Különböző Célok Esetén

Amikor egy osztály, annak tagjai és paraméterei mind rendelkeznek decoratorokkal, akkor a végrehajtási sorrend jól definiált:

  1. Paraméter Decoratorok kerülnek először alkalmazásra, minden paraméterre, az utolsó paramétertől az elsőig.
  2. Ezután a Metódus, Accessor, vagy Tulajdonság Decoratorok kerülnek alkalmazásra minden tagnál.
  3. Végül az Osztály Decoratorok kerülnek alkalmazásra magára az osztályra.

Minden kategórián belül, az azonos célon lévő több decorator alulról felfelé (vagy jobbról balra) kerül alkalmazásra.

Példa: Teljes Végrehajtási Sorrend

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; // Visszaadja a deszkriptort a metódushoz/accessorhoz, másokhoz undefined-ot
  };
}

@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();
// Hívja meg a metódust a metódus decorator aktiválásához
new MyDecoratedClass().myMethod("hello", 123);

/* Várható kimeneti sorrend (megközelítőleg, a specifikus TypeScript verziótól és a fordítástól függően):
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.
*/

A pontos konzol log időzítés kissé eltérhet attól függően, hogy mikor hívják meg a konstruktort vagy a metódust, de maga a decorator függvények végrehajtási sorrendje (és így mellékhatásaik vagy visszaadott értékeik alkalmazása) a fenti szabályokat követi.

Gyakorlati Alkalmazások és Tervezési Minták Decoratorokkal

A decoratorok, különösen a reflect-metadata polifill-lel kombinálva, a metaadat-vezérelt programozás új birodalmát nyitják meg. Ez lehetővé teszi az erőteljes tervezési mintákat, amelyek elvonatkoztatnak a vázlati kódoktól és a keresztirányú aggályoktól.

1. Függőség Injektálás (DI)

A decoratorok egyik legkiemelkedőbb felhasználása a Függőség Injektálás keretrendszerekben (mint az Angular @Injectable(), @Component() stb., vagy a NestJS kiterjedt DI használata). A decoratorok lehetővé teszik a függőségek közvetlen deklarálását konstruktorokon vagy tulajdonságokon, lehetővé téve a keretrendszer számára a megfelelő szolgáltatások automatikus példányosítását és biztosítását.

Példa: Egyszerűsített Szolgáltatás Injektálás

import "reflect-metadata"; // Elengedhetetlen az emitDecoratorMetadata funkcióhoz

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

    // Konstruktor paraméterek típusainak lekérése (emitDecoratorMetadata szükséges)
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
    const explicitInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // Használjon explicit @Inject token-t, ha meg van adva, különben kövesse a típust
      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;
  }
}

// Szolgáltatások definiálása
@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; // Példa tulajdonságon keresztüli injektálásra egyedi decorator vagy keretrendszeri funkció használatával

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

// A fő szolgáltatás feloldása
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();

Ez az elmélyült példa bemutatja, hogyan teszik lehetővé az @Injectable és @Inject decoratorok, valamint a reflect-metadata egy egyedi Container számára a függőségek automatikus feloldását és biztosítását. A TypeScript által automatikusan kibocsátott design:paramtypes metaadat (ha az emitDecoratorMetadata be van kapcsolva) itt elengedhetetlen.

2. Aspektus-Orientált Programozás (AOP)

Az AOP arra összpontosít, hogy modulárisan kezelje a keresztirányú aggályokat (pl. naplózás, biztonság, tranzakciók), amelyek több osztályon és modulon átívelnek. A decoratorok kiválóan alkalmasak az AOP fogalmak TypeScriptben történő megvalósítására.

Példa: Naplózás Metódus Decoratorral

Visszatérve a LogCall decoratorra, ez egy tökéletes példa az AOP-ra. Naplózási viselkedést ad hozzá minden olyan metódushoz, amely nem módosítja a metódus eredeti kódját. Ez elválasztja a "mit kell tenni" (üzleti logika) a "hogyan kell csinálni" (naplózás, teljesítménymérés stb.) elemtől.

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

Ez a megközelítés biztosítja, hogy a PaymentProcessor osztály tisztán a fizetési logikára összpontosítson, míg a LogMethod decorator kezeli a naplózás keresztirányú aggályát.

3. Érvényesítés és Átalakítás

A decoratorok hihetetlenül hasznosak az érvényesítési szabályok közvetlenül a tulajdonságokon történő definiálásához, vagy az adatok átalakításához szerializálás/deszerializálás során.

Példa: Adat Érvényesítés Tulajdonság Decoratorokkal

A fent említett @Required példa már bemutatta ezt. Íme egy másik példa numerikus tartomány érvényesítéssel.

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."]

Ez a beállítás lehetővé teszi az érvényesítési szabályok deklaratív definiálását a modell tulajdonságain, így az adatmodellek önleírják magukat a korlátaikat illetően.

Legjobb Gyakorlatok és Megfontolások

Bár a decoratorok erőteljesek, megfontoltan kell őket használni. Helytelen használatuk olyan kódot eredményezhet, amelyet nehezebb hibakeresni vagy megérteni.

Mikor Használjunk Decoratorokat (és Mikor Ne)

Teljesítmény Hatásai

A decoratorok fordítási időben (vagy JavaScript futásidőben, ha átfordítva) futnak. A transzformáció vagy metaadatgyűjtés akkor történik, amikor az osztály/metódus definiálva van, nem minden hívásnál. Ezért a decoratorok *alkalmazásának* futásidejű teljesítményhatása minimális. Azonban az Ön decoratorain belüli *logika* teljesítményhatással lehet, különösen, ha minden metódushívásnál költséges műveleteket végeznek (pl. bonyolult számítások egy metódus decoratoron belül).

Karbantarthatóság és Olvashatóság

A decoratorok, ha helyesen használják őket, jelentősen javíthatják az olvashatóságot azáltal, hogy a vázlati kódot a fő logikán kívülre helyezik. Azonban, ha bonyolult, rejtett átalakításokat végeznek, a hibakeresés kihívást jelenthet. Biztosítsa, hogy decoratorjai jól dokumentáltak legyenek, és viselkedésük kiszámítható legyen.

Kísérleti Állapot és a Decoratorok Jövője

Fontos megismételni, hogy a TypeScript decoratorok egy 3. szakaszban lévő TC39 javaslaton alapulnak. Ez azt jelenti, hogy a specifikáció nagyrészt stabil, de még kisebb változásokon mehet keresztül, mielőtt a hivatalos ECMAScript szabvány részévé válna. Olyan keretrendszerek, mint az Angular, magukévá tették őket, fogadva a végső szabványosítást. Ez bizonyos szintű kockázatot jelent, bár széleskörű elterjedésük miatt jelentős, bontó változások valószínűtlenek.

A TC39 javaslat fejlődött. A TypeScript jelenlegi megvalósítása az egyik korábbi javaslat verzión alapul. Van egy "Legacy Decorators" vs. "Standard Decorators" megkülönböztetés. Amikor a hivatalos szabvány megérkezik, a TypeScript valószínűleg frissíti a megvalósítását. A legtöbb fejlesztő számára, aki keretrendszereket használ, ezt az átmenetet maga a keretrendszer fogja kezelni. A könyvtár szerzők számára a régi és a jövőbeli standard decoratorok közötti finom különbségek megértése szükségessé válhat.

Az emitDecoratorMetadata Fordító Opció

Ez az opció, ha true-ra van állítva a tsconfig.json fájlban, arra utasítja a TypeScript fordítót, hogy bizonyos tervezési idejű típus metaadatokat bocsásson ki a lefordított JavaScriptbe. Ezek a metaadatok magukban foglalják a konstruktor paraméterek típusát (design:paramtypes), a metódusok visszatérési típusát (design:returntype), és a tulajdonságok típusát (design:type).

Ez a kibocsátott metaadat nem része a standard JavaScript futásidőnek. Általában a reflect-metadata polifill fogyasztja, amely aztán elérhetővé teszi a Reflect.getMetadata() függvények segítségével. Ez abszolút kritikus a fejlett minták, mint a Függőség Injektálás esetében, ahol egy konténernek tudnia kell egy osztály által igényelt függőségek típusait explicit konfiguráció nélkül.

Fejlettebb Minták Decoratorokkal

A decoratorok kombinálhatók és kiterjeszthetők még kifinomultabb minták felépítéséhez.

1. Decoratorok Decorálása (Magasabb Rangu Decoratorok)

Létrehozhat decoratorokat, amelyek más decoratorokat módosítanak vagy összekapcsolnak. Ez ritkább, de bemutatja a decoratorok funkcionális jellegét.

// Egy decorator, amely biztosítja, hogy egy metódus naplózásra kerüljön, és admin szerepköröket is igényeljen
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // Először az Authorization alkalmazása (belül)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // Majd a LogCall alkalmazása (kívül)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // A módosított deszkriptor visszaadása
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`Deleting user account: ${userId}`);
    return `User ${userId} deleted.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Várható kimenet (admin szerepkör feltételezésével):
[AUTH] Access granted for deleteUserAccount
[LOG] Calling deleteUserAccount with args: [ 'user007' ]
Deleting user account: user007
[LOG] Method deleteUserAccount returned: User user007 deleted.
*/

Itt az AdminAndLoggedMethod egy gyár, amely visszaad egy decoratorot, és azon belül két másik decoratorot alkalmaz. Ez a minta bonyolult decorator-összetételeket képes elzárni.

2. Decoratorok Használata Mixinekhez

Míg a TypeScript más módokat is kínál a mixinek implementálására, a decoratorok felhasználhatók képességek deklaratív módon történő injektálására az osztályokba.

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 {
  // Ezek a tulajdonságok/metódusok a decorator által vannak injektálva
  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}`);

Ez az @ApplyMixins decorator dinamikusan másolja a metódusokat és tulajdonságokat az alap konstruktorokból a származtatott osztály prototípusára, hatékonyan "mix-in" funkciókat biztosítva.

Következtetés: Modern TypeScript Fejlesztés Felhatalmazása

A TypeScript decoratorok egy erőteljes és kifejező funkció, amely lehetővé teszi a metaadat-vezérelt és aspektusorientált programozás új paradigmáját. Lehetővé teszik a fejlesztők számára az osztályok, metódusok, tulajdonságok, accesszorok és paraméterek kiegészítését, módosítását és deklaratív viselkedések hozzáadását azok alapvető logikájának megváltoztatása nélkül. Ez a felelősség szétválasztása tisztább, könnyebben karbantartható és rendkívül újrafelhasználható kódot eredményez.

Az egyszerű függőség-injektálás, robusztus érvényesítési rendszerek megvalósítása, valamint a naplózás és a teljesítménymérés mint keresztirányú aggályok hozzáadása terén a decoratorok elegáns megoldást kínálnak számos gyakori fejlesztési kihívásra. Bár kísérleti státuszuk tudatosságot igényel, széleskörű elterjedésük a főbb keretrendszerekben jelzi gyakorlati értéküket és jövőbeli relevanciájukat.

A TypeScript decoratorok elsajátításával jelentős eszközt szerez a rendelkezésére, amely lehetővé teszi robusztusabb, skálázhatóbb és intelligensebb alkalmazások felépítését. Fogadja el őket felelősségteljesen, értse meg mechanizmusukat, és engedje szabadjára a deklaratív erő új szintjét TypeScript projektjeiben.

TypeScript Decoratorok: Metaadat Programozási Minták Mesterei Robusztus Alkalmazásokhoz | MLOG