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ályok regisztrálása egy függőségi injektáló konténerben.
- Mixinek vagy további funkciók alkalmazása egy osztályhoz.
- Keretrendszer-specifikus konfigurációk (pl. útvonalvezetés egy webes keretrendszerben).
- Életciklus kampók hozzáadása osztályokhoz.
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ódushívások és azok argumentumainak/eredményeinek naplózása.
- Metóduseredmények gyorsítótárazása a teljesítmény javítása érdekében.
- Engedélyezési ellenőrzések alkalmazása a metódus végrehajtása előtt.
- Metódus végrehajtási idő mérése.
- Metódushívások csökkentése vagy szabályozása.
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:
- Érték érvényesítése egy tulajdonság beállítása során.
- Egy érték átalakítása, mielőtt beállítják, vagy miután lekérdezték.
- Hozzáférési engedélyek szabályozása a tulajdonságokhoz.
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ágok regisztrálása szerializáláshoz/deszerializáláshoz.
- Érvényesítési szabályok alkalmazása a tulajdonságokhoz.
- Alapértelmezett értékek vagy konfigurációk beállítása a tulajdonságokhoz.
- ORM (Object-Relational Mapping) oszlop-hozzárendelés (pl.
@Column()
a TypeORM-ban).
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étertípusok regisztrálása a függőségi injektáláshoz (pl.
@Inject()
az Angularban). - Érvényesítés vagy átalakítás alkalmazása specifikus paraméterekhez.
- Metaadatok kinyerése API kérés paraméterekről webes keretrendszerekben.
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:
- Paraméter Decoratorok kerülnek először alkalmazásra, minden paraméterre, az utolsó paramétertől az elsőig.
- Ezután a Metódus, Accessor, vagy Tulajdonság Decoratorok kerülnek alkalmazásra minden tagnál.
- 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)
- Használja őket a következőkre:
- Keresztirányú aggályok: Naplózás, gyorsítótárazás, engedélyezés, tranzakciós kezelés.
- Metaadat deklaráció: Sémák definiálása ORM-ekhez, érvényesítési szabályok, DI konfiguráció.
- Keretrendszer integráció: Amikor olyan keretrendszereket építünk vagy használunk, amelyek kihasználják a metaadatokat.
- Vázlati kód csökkentése: Ismétlődő kódminták elvonatkoztatása.
- Kerülje őket a következőkre:
- Egyszerű függvényhívások: Ha egy egyszerű függvényhívás ugyanazt az eredményt világosan el tudja érni, részesítse előnyben azt.
- Üzleti logika: A decoratoroknak ki kell egészíteniük, nem pedig meghatározniuk az alapvető üzleti logikát.
- Túlkomplikálás: Ha egy decorator használata kevésbé olvashatóvá vagy nehezebben tesztelhetővé teszi a kódot, gondolja át újra.
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.