Istražite moć TypeScript dekoratora za metaprogramiranje, aspektno orijentirano programiranje i poboljšanje koda deklarativnim obrascima. Sveobuhvatan vodič za globalne programere.
TypeScript dekoratori: Ovladavanje obrascima metaprogramiranja za robusne aplikacije
U golemom krajoliku modernog razvoja softvera, održavanje čistih, skalabilnih i upravljivih kodnih baza je od presudne važnosti. TypeScript, sa svojim moćnim sustavom tipova i naprednim značajkama, pruža programerima alate za postizanje toga. Među njegovim najintrigantnijim i najtransformativnijim značajkama su Dekoratori. Iako su u vrijeme pisanja ovog teksta još uvijek eksperimentalna značajka (prijedlog Faze 3 za ECMAScript), dekoratori se široko koriste u okvirima poput Angulara i TypeORM-a, temeljito mijenjajući način na koji pristupamo obrascima dizajna, metaprogramiranju i aspektno orijentiranom programiranju (AOP).
Ovaj sveobuhvatni vodič duboko će zaroniti u TypeScript dekoratore, istražujući njihovu mehaniku, različite vrste, praktične primjene i najbolje prakse. Bez obzira gradite li velike poslovne aplikacije, mikroservise ili klijentska web sučelja, razumijevanje dekoratora osnažit će vas da pišete deklarativniji, održiviji i moćniji TypeScript kod.
Razumijevanje temeljnog koncepta: Što je dekorator?
U svojoj suštini, dekorator je posebna vrsta deklaracije koja se može pridružiti deklaraciji klase, metodi, pristupniku, svojstvu ili parametru. Dekoratori su funkcije koje vraćaju novu vrijednost (ili mijenjaju postojeću) za cilj koji dekoriraju. Njihova primarna svrha je dodavanje metapodataka ili promjena ponašanja deklaracije kojoj su pridruženi, bez izravnog mijenjanja temeljne strukture koda. Ovaj vanjski, deklarativni način proširivanja koda je nevjerojatno moćan.
Zamislite dekoratore kao anotacije ili oznake koje primjenjujete na dijelove svog koda. Te oznake zatim mogu biti pročitane ili korištene od strane drugih dijelova vaše aplikacije ili okvira, često u vrijeme izvođenja, kako bi se pružila dodatna funkcionalnost ili konfiguracija.
Sintaksa dekoratora
Dekoratori imaju prefiks simbola @
, nakon kojeg slijedi naziv funkcije dekoratora. Postavljaju se neposredno ispred deklaracije koju dekoriraju.
@MyDecorator
class MyClass {
@AnotherDecorator
myMethod() {
// ...
}
}
Omogućavanje dekoratora u TypeScriptu
Prije nego što možete koristiti dekoratore, morate omogućiti opciju kompajlera experimentalDecorators
u vašoj tsconfig.json
datoteci. Dodatno, za napredne mogućnosti refleksije metapodataka (koje često koriste okviri), trebat će vam i emitDecoratorMetadata
te reflect-metadata
polyfill.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Također trebate instalirati reflect-metadata
:
npm install reflect-metadata --save
# or
yarn add reflect-metadata
I uvezite ga na samom vrhu ulazne točke vaše aplikacije (npr. main.ts
ili app.ts
):
import "reflect-metadata";
// Slijedi kod vaše aplikacije
Tvornice dekoratora: Prilagodba na dohvat ruke
Iako je osnovni dekorator funkcija, često ćete morati proslijediti argumente dekoratoru kako biste konfigurirali njegovo ponašanje. To se postiže korištenjem tvornice dekoratora. Tvornica dekoratora je funkcija koja vraća stvarnu funkciju dekoratora. Kada primijenite tvornicu dekoratora, pozivate je s njezinim argumentima, a ona zatim vraća funkciju dekoratora koju TypeScript primjenjuje na vaš kod.
Primjer stvaranja jednostavne tvornice dekoratora
Stvorimo tvornicu za Logger
dekorator koji može bilježiti poruke s različitim prefiksima.
function Logger(prefix: string) {
return function (target: Function) {
console.log(`[${prefix}] Klasa ${target.name} je definirana.`);
};
}
@Logger("APP_INIT")
class ApplicationBootstrap {
constructor() {
console.log("Aplikacija se pokreće...");
}
}
const app = new ApplicationBootstrap();
// Izlaz:
// [APP_INIT] Klasa ApplicationBootstrap je definirana.
// Aplikacija se pokreće...
U ovom primjeru, Logger("APP_INIT")
je poziv tvornice dekoratora. On vraća stvarnu funkciju dekoratora koja kao argument uzima target: Function
(konstruktor klase). To omogućuje dinamičku konfiguraciju ponašanja dekoratora.
Vrste dekoratora u TypeScriptu
TypeScript podržava pet različitih vrsta dekoratora, od kojih je svaka primjenjiva na određenu vrstu deklaracije. Potpis funkcije dekoratora varira ovisno o kontekstu u kojem se primjenjuje.
1. Dekoratori klase
Dekoratori klase primjenjuju se na deklaracije klasa. Funkcija dekoratora kao jedini argument prima konstruktor klase. Dekorator klase može promatrati, mijenjati ili čak zamijeniti definiciju klase.
Potpis:
function ClassDecorator(target: Function) { ... }
Povratna vrijednost:
Ako dekorator klase vrati vrijednost, zamijenit će deklaraciju klase s priloženom funkcijom konstruktora. Ovo je moćna značajka, često korištena za mixine ili proširenje klase. Ako se ne vrati nikakva vrijednost, koristi se originalna klasa.
Slučajevi upotrebe:
- Registriranje klasa u spremniku za ubrizgavanje ovisnosti.
- Primjena mixina ili dodatnih funkcionalnosti na klasu.
- Konfiguracije specifične za okvire (npr. usmjeravanje u web okviru).
- Dodavanje životnih kuka (lifecycle hooks) klasama.
Primjer dekoratora klase: Ubrizgavanje servisa
Zamislite jednostavan scenarij ubrizgavanja ovisnosti gdje želite označiti klasu kao "injectable" (ubrizgivu) i opcionalno joj dati naziv u spremniku.
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(`Registriran servis: ${serviceName}`);
// Opcionalno, ovdje biste mogli vratiti novu klasu kako biste proširili ponašanje
return class extends constructor {
createdAt = new Date();
// Dodatna svojstva ili metode za sve ubrizgane servise
};
};
}
@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("--- Servisi registrirani ---");
console.log(Array.from(InjectableServiceRegistry.keys()));
const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
const userServiceInstance = new userServiceConstructor();
console.log("Korisnici:", userServiceInstance.getUsers());
// console.log("User Service Created At:", userServiceInstance.createdAt); // Ako se koristi vraćena klasa
}
Ovaj primjer pokazuje kako dekorator klase može registrirati klasu i čak izmijeniti njezin konstruktor. Dekorator Injectable
čini klasu otkrivljivom za teorijski sustav ubrizgavanja ovisnosti.
2. Dekoratori metoda
Dekoratori metoda primjenjuju se na deklaracije metoda. Primaju tri argumenta: ciljni objekt (za statičke članove, konstruktorsku funkciju; za članove instance, prototip klase), naziv metode i deskriptor svojstva metode.
Potpis:
function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Povratna vrijednost:
Dekorator metode može vratiti novi PropertyDescriptor
. Ako to učini, taj deskriptor će se koristiti za definiranje metode. To vam omogućuje da izmijenite ili zamijenite implementaciju originalne metode, što ga čini nevjerojatno moćnim za AOP.
Slučajevi upotrebe:
- Bilježenje poziva metoda i njihovih argumenata/rezultata.
- Spremanje rezultata metoda u predmemoriju radi poboljšanja performansi.
- Primjena provjera autorizacije prije izvršenja metode.
- Mjerenje vremena izvršenja metode.
- Debouncing ili throttling poziva metoda.
Primjer dekoratora metode: Praćenje performansi
Stvorimo MeasurePerformance
dekorator za bilježenje vremena izvršenja 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}" izvršena za ${duration.toFixed(2)} ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@MeasurePerformance
processData(data: number[]): number[] {
// Simulacija složene, vremenski zahtjevne 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(`Podaci za ID: ${id}`);
}, 500);
});
}
}
const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));
Dekorator MeasurePerformance
omotava originalnu metodu logikom za mjerenje vremena, ispisujući trajanje izvršenja bez zatrpavanja poslovne logike unutar same metode. Ovo je klasičan primjer aspektno orijentiranog programiranja (AOP).
3. Dekoratori pristupnika
Dekoratori pristupnika primjenjuju se na deklaracije pristupnika (get
i set
). Slično dekoratorima metoda, primaju ciljni objekt, naziv pristupnika i njegov deskriptor svojstva.
Potpis:
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Povratna vrijednost:
Dekorator pristupnika može vratiti novi PropertyDescriptor
, koji će se koristiti za definiranje pristupnika.
Slučajevi upotrebe:
- Validacija prilikom postavljanja svojstva.
- Transformacija vrijednosti prije nego što se postavi ili nakon što se dohvati.
- Kontroliranje dozvola pristupa za svojstva.
Primjer dekoratora pristupnika: Spremanje gettera u predmemoriju
Stvorimo dekorator koji sprema rezultat skupog izračuna gettera u predmemoriju.
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] Računanje vrijednosti za ${String(propertyKey)}`);
this[cacheKey] = originalGetter.apply(this);
} else {
console.log(`[Cache Hit] Korištenje spremljene vrijednosti za ${String(propertyKey)}`);
}
return this[cacheKey];
};
}
return descriptor;
}
class ReportGenerator {
private data: number[];
constructor(data: number[]) {
this.data = data;
}
// Simulira skupi izračun
@CachedGetter
get expensiveSummary(): number {
console.log("Obavljanje skupog izračuna sažetka...");
return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
}
}
const generator = new ReportGenerator([10, 20, 30, 40, 50]);
console.log("Prvi pristup:", generator.expensiveSummary);
console.log("Drugi pristup:", generator.expensiveSummary);
console.log("Treći pristup:", generator.expensiveSummary);
Ovaj dekorator osigurava da se izračun gettera expensiveSummary
izvrši samo jednom, a naknadni pozivi vraćaju predmemoriranu vrijednost. Ovaj obrazac je vrlo koristan za optimizaciju performansi gdje pristup svojstvu uključuje teške izračune ili vanjske pozive.
4. Dekoratori svojstava
Dekoratori svojstava primjenjuju se na deklaracije svojstava. Primaju dva argumenta: ciljni objekt (za statičke članove, konstruktorsku funkciju; za članove instance, prototip klase) i naziv svojstva.
Potpis:
function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }
Povratna vrijednost:
Dekoratori svojstava ne mogu vratiti nikakvu vrijednost. Njihova primarna upotreba je registracija metapodataka o svojstvu. Ne mogu izravno promijeniti vrijednost svojstva ili njegov deskriptor u vrijeme dekoracije, jer deskriptor za svojstvo još nije u potpunosti definiran kada se dekoratori svojstava izvršavaju.
Slučajevi upotrebe:
- Registriranje svojstava za serijalizaciju/deserijalizaciju.
- Primjena pravila validacije na svojstva.
- Postavljanje zadanih vrijednosti ili konfiguracija za svojstva.
- Mapiranje stupaca ORM-a (Object-Relational Mapping) (npr.
@Column()
u TypeORM-u).
Primjer dekoratora svojstva: Validacija obaveznog polja
Stvorimo dekorator za označavanje svojstva kao "obavezno" i zatim ga validirajmo u vrijeme izvođenja.
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 obavezno.`
});
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("Greške validacije korisnika 1:", validate(user1)); // []
const user2 = new UserProfile("", "Smith");
console.log("Greške validacije korisnika 2:", validate(user2)); // ["firstName je obavezno."]
const user3 = new UserProfile("Alice", "");
console.log("Greške validacije korisnika 3:", validate(user3)); // ["lastName je obavezno."]
Dekorator Required
jednostavno registrira pravilo validacije u središnjoj mapi validationRules
. Zasebna funkcija validate
zatim koristi te metapodatke za provjeru instance u vrijeme izvođenja. Ovaj obrazac odvaja logiku validacije od definicije podataka, čineći je višekratno upotrebljivom i čistom.
5. Dekoratori parametara
Dekoratori parametara primjenjuju se na parametre unutar konstruktora klase ili metode. Primaju tri argumenta: ciljni objekt (za statičke članove, konstruktorsku funkciju; za članove instance, prototip klase), naziv metode (ili undefined
za parametre konstruktora) i redni indeks parametra u listi parametara funkcije.
Potpis:
function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
Povratna vrijednost:
Dekoratori parametara ne mogu vratiti nikakvu vrijednost. Poput dekoratora svojstava, njihova primarna uloga je dodavanje metapodataka o parametru.
Slučajevi upotrebe:
- Registriranje tipova parametara za ubrizgavanje ovisnosti (npr.
@Inject()
u Angularu). - Primjena validacije ili transformacije na specifične parametre.
- Ekstrahiranje metapodataka o parametrima API zahtjeva u web okvirima.
Primjer dekoratora parametara: Ubrizgavanje podataka iz zahtjeva
Simulirajmo kako bi web okvir mogao koristiti dekoratore parametara za ubrizgavanje specifičnih podataka u parametar metode, poput ID-a korisnika iz zahtjeva.
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);
};
}
// Hipotetska funkcija okvira za pozivanje metode s razriješenim parametrima
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(`Dohvaćanje korisnika s ID-om: ${userId}, Token: ${authToken || "N/A"}`);
return { id: userId, name: "Jane Doe" };
}
deleteUser(@RequestParam("id") userId: string) {
console.log(`Brisanje korisnika s ID-om: ${userId}`);
return { status: "deleted", id: userId };
}
}
const userController = new UserController();
// Simulacija dolaznog zahtjeva
const mockRequest = {
id: "user123",
token: "abc-123",
someOtherProp: "xyz"
};
console.log("
--- Izvršavanje getUser ---");
executeWithParams(userController, "getUser", mockRequest);
console.log("
--- Izvršavanje deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });
Ovaj primjer pokazuje kako dekoratori parametara mogu prikupljati informacije o potrebnim parametrima metode. Okvir zatim može koristiti te prikupljene metapodatke za automatsko razrješavanje i ubrizgavanje odgovarajućih vrijednosti kada se metoda pozove, značajno pojednostavljujući logiku kontrolera ili servisa.
Kompozicija dekoratora i redoslijed izvršavanja
Dekoratori se mogu primijeniti u različitim kombinacijama, a razumijevanje njihovog redoslijeda izvršavanja ključno je za predviđanje ponašanja i izbjegavanje neočekivanih problema.
Više dekoratora na jednom cilju
Kada se više dekoratora primijeni na jednu deklaraciju (npr. klasu, metodu ili svojstvo), oni se izvršavaju određenim redoslijedom: odozdo prema gore, ili zdesna nalijevo, za njihovu evaluaciju. Međutim, njihovi se rezultati primjenjuju suprotnim redoslijedom.
@DecoratorA
@DecoratorB
class MyClass {
// ...
}
Ovdje će se prvo evaluirati DecoratorB
, a zatim DecoratorA
. Ako oni mijenjaju klasu (npr. vraćanjem novog konstruktora), modifikacija iz DecoratorA
će omotati ili se primijeniti preko modifikacije iz DecoratorB
.
Primjer: Ulančavanje dekoratora metoda
Razmotrimo dva dekoratora metoda: LogCall
i Authorization
.
function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] Pozivanje ${String(propertyKey)} s argumentima:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] Metoda ${String(propertyKey)} vratila je:`, 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 dohvaćanja uloga trenutnog korisnika
const authorized = roles.some(role => currentUserRoles.includes(role));
if (!authorized) {
console.warn(`[AUTH] Pristup odbijen za ${String(propertyKey)}. Potrebne uloge: ${roles.join(", ")}`);
throw new Error("Neovlašten pristup");
}
console.log(`[AUTH] Pristup odobren za ${String(propertyKey)}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class SecureService {
@LogCall
@Authorization(["admin"])
deleteSensitiveData(id: string) {
console.log(`Brisanje osjetljivih podataka za ID: ${id}`);
return `Podaci ID ${id} obrisani.`;
}
@Authorization(["user"])
@LogCall // Redoslijed je ovdje promijenjen
fetchPublicData(query: string) {
console.log(`Dohvaćanje javnih podataka s upitom: ${query}`);
return `Javni podaci za upit: ${query}`;
}
}
const service = new SecureService();
try {
console.log("
--- Pozivanje deleteSensitiveData (Admin korisnik) ---");
service.deleteSensitiveData("record123");
} catch (error: any) {
console.error(error.message);
}
try {
console.log("
--- Pozivanje fetchPublicData (Ne-Admin korisnik) ---");
// Simulira ne-admin korisnika koji pokušava pristupiti fetchPublicData što zahtijeva ulogu 'user'
const mockUserRoles = ["guest"]; // Ovo će neuspješno proći autorizaciju
// Da bi ovo bilo dinamično, trebali biste DI sustav ili statički kontekst za uloge trenutnog korisnika.
// Radi jednostavnosti, pretpostavljamo da dekorator Authorization ima pristup kontekstu trenutnog korisnika.
// Prilagodimo Authorization dekorator da uvijek pretpostavlja 'admin' za potrebe demonstracije,
// tako da prvi poziv uspije, a drugi ne uspije kako bi se pokazale različite putanje.
// Ponovno pokrenite s ulogom 'user' da bi fetchPublicData uspio.
// Zamislite da currentUserRoles u Authorization postane: ['user']
// Za ovaj primjer, neka bude jednostavno i pokažimo efekt redoslijeda.
service.fetchPublicData("pojam za pretragu"); // Ovo će izvršiti Auth -> Log
} catch (error: any) {
console.error(error.message);
}
/* Očekivani izlaz za deleteSensitiveData:
[AUTH] Pristup odobren za deleteSensitiveData
[LOG] Pozivanje deleteSensitiveData s argumentima: [ 'record123' ]
Brisanje osjetljivih podataka za ID: record123
[LOG] Metoda deleteSensitiveData vratila je: Podaci ID record123 obrisani.
*/
/* Očekivani izlaz za fetchPublicData (ako korisnik ima ulogu 'user'):
[LOG] Pozivanje fetchPublicData s argumentima: [ 'pojam za pretragu' ]
[AUTH] Pristup odobren za fetchPublicData
Dohvaćanje javnih podataka s upitom: pojam za pretragu
[LOG] Metoda fetchPublicData vratila je: Javni podaci za upit: pojam za pretragu
*/
Primijetite redoslijed: za deleteSensitiveData
, Authorization
(donji) se izvršava prvi, a zatim LogCall
(gornji) ga omotava. Unutarnja logika Authorization
se izvršava prva. Za fetchPublicData
, LogCall
(donji) se izvršava prvi, a zatim Authorization
(gornji) ga omotava. To znači da će aspekt LogCall
biti izvan aspekta Authorization
. Ova razlika je ključna za poprečne probleme (cross-cutting concerns) poput bilježenja ili rukovanja pogreškama, gdje redoslijed izvršavanja može značajno utjecati na ponašanje.
Redoslijed izvršavanja za različite ciljeve
Kada klasa, njezini članovi i parametri svi imaju dekoratore, redoslijed izvršavanja je dobro definiran:
- Dekoratori parametara se primjenjuju prvi, za svaki parametar, počevši od zadnjeg parametra prema prvom.
- Zatim se primjenjuju Dekoratori metoda, pristupnika ili svojstava za svakog člana.
- Konačno, Dekoratori klase se primjenjuju na samu klasu.
Unutar svake kategorije, više dekoratora na istom cilju primjenjuju se odozdo prema gore (ili zdesna nalijevo).
Primjer: Puni redoslijed izvršavanja
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 || "konstruktor")}`);
} else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
console.log(`Dekorator metode/pristupnika: ${message} na ${String(propertyKey)}`);
} else {
console.log(`Dekorator svojstva: ${message} na ${String(propertyKey)}`);
}
} else {
console.log(`Dekorator klase: ${message} na ${target.name}`);
}
return descriptorOrIndex; // Vraća deskriptor za metodu/pristupnik, undefined za ostale
};
}
@log("Razina klase D")
@log("Razina klase C")
class MyDecoratedClass {
@log("Statičko svojstvo A")
static staticProp: string = "";
@log("Svojstvo instance B")
instanceProp: number = 0;
@log("Metoda D")
@log("Metoda C")
myMethod(
@log("Parametar Z") paramZ: string,
@log("Parametar Y") paramY: number
) {
console.log("Metoda myMethod izvršena.");
}
@log("Getter/Setter F")
get myAccessor() {
return "";
}
set myAccessor(value: string) {
//...
}
constructor() {
console.log("Konstruktor izvršen.");
}
}
new MyDecoratedClass();
// Poziv metode za pokretanje dekoratora metode
new MyDecoratedClass().myMethod("hello", 123);
/* Predviđeni redoslijed izlaza (približan, ovisno o specifičnoj verziji TypeScripta i kompilaciji):
Dekorator parametra: Parametar Y na parametru #1 od myMethod
Dekorator parametra: Parametar Z na parametru #0 od myMethod
Dekorator svojstva: Statičko svojstvo A na staticProp
Dekorator svojstva: Svojstvo instance B na instanceProp
Dekorator metode/pristupnika: Getter/Setter F na myAccessor
Dekorator metode/pristupnika: Metoda C na myMethod
Dekorator metode/pristupnika: Metoda D na myMethod
Dekorator klase: Razina klase C na MyDecoratedClass
Dekorator klase: Razina klase D na MyDecoratedClass
Konstruktor izvršen.
Metoda myMethod izvršena.
*/
Točno vrijeme ispisa u konzoli može se malo razlikovati ovisno o tome kada se poziva konstruktor ili metoda, ali redoslijed u kojem se same funkcije dekoratora izvršavaju (i time primjenjuju njihovi nuspojave ili vraćene vrijednosti) slijedi gore navedena pravila.
Praktične primjene i obrasci dizajna s dekoratorima
Dekoratori, posebno u kombinaciji s reflect-metadata
polyfillom, otvaraju novo područje programiranja vođenog metapodacima. To omogućuje moćne obrasce dizajna koji apstrahiraju ponavljajući kod (boilerplate) i poprečne probleme.
1. Ubrizgavanje ovisnosti (DI)
Jedna od najistaknutijih upotreba dekoratora je u okvirima za ubrizgavanje ovisnosti (poput Angularovog @Injectable()
, @Component()
, itd., ili NestJS-ove opsežne upotrebe DI-ja). Dekoratori vam omogućuju da deklarirate ovisnosti izravno na konstruktorima ili svojstvima, omogućujući okviru da automatski instancira i pruži ispravne servise.
Primjer: Pojednostavljeno ubrizgavanje servisa
import "reflect-metadata"; // Neophodno 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(`Klasa ${target.name} nije označena kao @Injectable.`);
}
// Dohvati tipove parametara konstruktora (zahtijeva emitDecoratorMetadata)
const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
const dependencies = paramTypes.map((paramType, index) => {
// Koristi eksplicitni @Inject token ako je naveden, inače zaključi tip
const token = explicitInjections[index] || paramType;
if (token === undefined) {
throw new Error(`Ne mogu razriješiti parametar na indeksu ${index} za ${target.name}. Možda je riječ o kružnoj ovisnosti ili primitivnom tipu bez eksplicitnog @Inject.`);
}
return Container.resolve(token);
});
const instance = new target(...dependencies);
Container.instances.set(target, instance);
return instance;
}
}
// Definiraj servise
@Injectable()
class DatabaseService {
connect() {
console.log("Spajanje na bazu podataka...");
return "DB Connection";
}
}
@Injectable()
class AuthService {
private db: DatabaseService;
constructor(db: DatabaseService) {
this.db = db;
}
login() {
console.log(`AuthService: Autentikacija pomoću ${this.db.connect()}`);
return "User logged in";
}
}
@Injectable()
class UserService {
private authService: AuthService;
private dbService: DatabaseService; // Primjer ubrizgavanja putem svojstva pomoću prilagođenog dekoratora ili značajke okvira
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: Dohvaćanje korisničkog profila...");
return { id: 1, name: "Global User" };
}
}
// Razriješi glavni servis
console.log("--- Razrješavanje UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());
console.log("
--- Razrješavanje AuthService (trebao bi biti spremljen) ---");
const authService = Container.resolve(AuthService);
authService.login();
Ovaj razrađeni primjer pokazuje kako dekoratori @Injectable
i @Inject
, u kombinaciji s reflect-metadata
, omogućuju prilagođenom Container
-u da automatski razriješi i pruži ovisnosti. Metapodaci design:paramtypes
koje TypeScript automatski emitira (kada je emitDecoratorMetadata
postavljen na true) ovdje su ključni.
2. Aspektno orijentirano programiranje (AOP)
AOP se fokusira na modularizaciju poprečnih problema (npr. bilježenje, sigurnost, transakcije) koji se protežu kroz više klasa i modula. Dekoratori su izvrstan izbor za implementaciju AOP koncepata u TypeScriptu.
Primjer: Bilježenje s dekoratorom metode
Vraćajući se na LogCall
dekorator, to je savršen primjer AOP-a. Dodaje ponašanje bilježenja bilo kojoj metodi bez mijenjanja originalnog koda metode. To odvaja "što učiniti" (poslovna logika) od "kako to učiniti" (bilježenje, praćenje performansi, itd.).
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG AOP] Ulazak u metodu: ${String(propertyKey)} s argumentima:`, args);
try {
const result = originalMethod.apply(this, args);
console.log(`[LOG AOP] Izlazak iz metode: ${String(propertyKey)} s rezultatom:`, result);
return result;
} catch (error: any) {
console.error(`[LOG AOP] Greška u metodi ${String(propertyKey)}:`, error.message);
throw error;
}
};
return descriptor;
}
class PaymentProcessor {
@LogMethod
processPayment(amount: number, currency: string) {
if (amount <= 0) {
throw new Error("Iznos plaćanja mora biti pozitivan.");
}
console.log(`Obrada plaćanja od ${amount} ${currency}...`);
return `Plaćanje od ${amount} ${currency} uspješno obrađeno.`;
}
@LogMethod
refundPayment(transactionId: string) {
console.log(`Povrat plaćanja za ID transakcije: ${transactionId}...`);
return `Povrat pokrenut za ${transactionId}.`;
}
}
const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
processor.processPayment(-50, "EUR");
} catch (error: any) {
console.error("Uhvaćena pogreška:", error.message);
}
Ovaj pristup održava klasu PaymentProcessor
usredotočenom isključivo na logiku plaćanja, dok se dekorator LogMethod
bavi poprečnim problemom bilježenja.
3. Validacija i transformacija
Dekoratori su nevjerojatno korisni za definiranje pravila validacije izravno na svojstvima ili za transformaciju podataka tijekom serijalizacije/deserijalizacije.
Primjer: Validacija podataka s dekoratorima svojstava
Primjer @Required
ranije je to već pokazao. Evo još jednog primjera s validacijom numeričkog raspona.
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 pozitivan broj.`);
}
function MaxLength(maxLength: number) {
return function (target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} mora imati najviše ${maxLength} znakova.`);
};
}
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("Greške proizvoda 1:", Product.validate(product1)); // []
const product2 = new Product("Vrlo dugo ime proizvoda koje prelazi ograničenje od pedeset znakova za svrhu testiranja", 50);
console.log("Greške proizvoda 2:", Product.validate(product2)); // ["name mora imati najviše 50 znakova."]
const product3 = new Product("Knjiga", -10);
console.log("Greške proizvoda 3:", Product.validate(product3)); // ["price mora biti pozitivan broj."]
Ova postavka omogućuje vam da deklarativno definirate pravila validacije na svojstvima vašeg modela, čineći vaše modele podataka samopisujućima u smislu njihovih ograničenja.
Najbolje prakse i razmatranja
Iako su dekoratori moćni, treba ih koristiti razborito. Zloupotreba može dovesti do koda koji je teže otkloniti pogreške ili razumjeti.
Kada koristiti dekoratore (a kada ne)
- Koristite ih za:
- Poprečne probleme: Bilježenje, spremanje u predmemoriju, autorizacija, upravljanje transakcijama.
- Deklaraciju metapodataka: Definiranje sheme za ORM-ove, pravila validacije, DI konfiguraciju.
- Integraciju s okvirima: Prilikom izgradnje ili korištenja okvira koji se oslanjaju na metapodatke.
- Smanjenje ponavljajućeg koda: Apstrahiranje ponavljajućih obrazaca koda.
- Izbjegavajte ih za:
- Jednostavne pozive funkcija: Ako se isti rezultat može jasno postići običnim pozivom funkcije, dajte mu prednost.
- Poslovnu logiku: Dekoratori bi trebali proširivati, a ne definirati, temeljnu poslovnu logiku.
- Prekomjerno kompliciranje: Ako korištenje dekoratora čini kod manje čitljivim ili težim za testiranje, razmislite ponovno.
Implikacije na performanse
Dekoratori se izvršavaju u vrijeme kompilacije (ili u vrijeme definicije u JavaScript runtimeu ako se transpilira). Transformacija ili prikupljanje metapodataka događa se kada se klasa/metoda definira, a ne pri svakom pozivu. Stoga je utjecaj *primjene* dekoratora na performanse u vrijeme izvođenja minimalan. Međutim, *logika unutar* vaših dekoratora može imati utjecaj na performanse, posebno ako obavljaju skupe operacije pri svakom pozivu metode (npr. složeni izračuni unutar dekoratora metode).
Održivost i čitljivost
Dekoratori, kada se pravilno koriste, mogu značajno poboljšati čitljivost premještanjem ponavljajućeg koda izvan glavne logike. Međutim, ako obavljaju složene, skrivene transformacije, otklanjanje pogrešaka može postati izazovno. Osigurajte da su vaši dekoratori dobro dokumentirani i da je njihovo ponašanje predvidljivo.
Eksperimentalni status i budućnost dekoratora
Važno je ponoviti da se TypeScript dekoratori temelje na prijedlogu Faze 3 TC39. To znači da je specifikacija uglavnom stabilna, ali bi se još uvijek mogle dogoditi manje promjene prije nego što postane dio službenog ECMAScript standarda. Okviri poput Angulara su ih prihvatili, kladeći se na njihovu konačnu standardizaciju. To podrazumijeva određenu razinu rizika, iako su, s obzirom na njihovu široku primjenu, značajne prijelomne promjene malo vjerojatne.
Prijedlog TC39 se razvio. Trenutna implementacija TypeScripta temelji se na starijoj verziji prijedloga. Postoji razlika između "Legacy Decorators" i "Standard Decorators". Kada službeni standard bude usvojen, TypeScript će vjerojatno ažurirati svoju implementaciju. Za većinu programera koji koriste okvire, ovaj prijelaz će upravljati sam okvir. Za autore biblioteka, razumijevanje suptilnih razlika između naslijeđenih i budućih standardnih dekoratora moglo bi postati nužno.
Opcija kompajlera emitDecoratorMetadata
Ova opcija, kada je postavljena na true
u tsconfig.json
, nalaže TypeScript kompajleru da emitira određene metapodatke o tipovima iz vremena dizajna u kompilirani JavaScript. Ovi metapodaci uključuju tip parametara konstruktora (design:paramtypes
), povratni tip metoda (design:returntype
) i tip svojstava (design:type
).
Ovi emitirani metapodaci nisu dio standardnog JavaScript runtimea. Obično ih koristi reflect-metadata
polyfill, koji ih zatim čini dostupnima putem funkcija Reflect.getMetadata()
. Ovo je apsolutno ključno za napredne obrasce poput ubrizgavanja ovisnosti, gdje spremnik treba znati tipove ovisnosti koje klasa zahtijeva bez eksplicitne konfiguracije.
Napredni obrasci s dekoratorima
Dekoratori se mogu kombinirati i proširivati za izgradnju još sofisticiranijih obrazaca.
1. Dekoriranje dekoratora (Dekoratori višeg reda)
Možete stvarati dekoratore koji mijenjaju ili sastavljaju druge dekoratore. To je manje uobičajeno, ali pokazuje funkcionalnu prirodu dekoratora.
// Dekorator koji osigurava da se metoda bilježi i također zahtijeva administratorske uloge
function AdminAndLoggedMethod() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Prvo primijeni Authorization (unutarnji)
Authorization(["admin"])(target, propertyKey, descriptor);
// Zatim primijeni LogCall (vanjski)
LogCall(target, propertyKey, descriptor);
return descriptor; // Vrati izmijenjeni deskriptor
};
}
class AdminPanel {
@AdminAndLoggedMethod()
deleteUserAccount(userId: string) {
console.log(`Brisanje korisničkog računa: ${userId}`);
return `Korisnik ${userId} obrisan.`;
}
}
const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Očekivani izlaz (pod pretpostavkom admin uloge):
[AUTH] Pristup odobren za deleteUserAccount
[LOG] Pozivanje deleteUserAccount s argumentima: [ 'user007' ]
Brisanje korisničkog računa: user007
[LOG] Metoda deleteUserAccount vratila je: Korisnik user007 obrisan.
*/
Ovdje je AdminAndLoggedMethod
tvornica koja vraća dekorator, a unutar tog dekoratora primjenjuje dva druga dekoratora. Ovaj obrazac može enkapsulirati složene kompozicije dekoratora.
2. Korištenje dekoratora za Mixine
Iako TypeScript nudi druge načine za implementaciju mixina, dekoratori se mogu koristiti za ubrizgavanje sposobnosti u klase na deklarativan 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 je odbačen.");
}
}
class Loggable {
log(message: string) {
console.log(`[Loggable] ${message}`);
}
}
@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
// Ova svojstva/metode ubrizgava dekorator
isDisposed!: boolean;
dispose!: () => void;
log!: (message: string) => void;
constructor(public name: string) {
this.log(`Resurs ${this.name} je stvoren.`);
}
cleanUp() {
this.dispose();
this.log(`Resurs ${this.name} je očišćen.`);
}
}
const resource = new MyResource("NetworkConnection");
console.log(`Je li odbačen: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Je li odbačen: ${resource.isDisposed}`);
Ovaj @ApplyMixins
dekorator dinamički kopira metode i svojstva s prototipa baznih konstruktora na prototip izvedene klase, efektivno "miješajući" funkcionalnosti.
Zaključak: Osnaživanje modernog TypeScript razvoja
TypeScript dekoratori su moćna i izražajna značajka koja omogućuje novu paradigmu programiranja vođenog metapodacima i aspektno orijentiranog programiranja. Omogućuju programerima da poboljšaju, izmijene i dodaju deklarativna ponašanja klasama, metodama, svojstvima, pristupnicima i parametrima bez mijenjanja njihove temeljne logike. Ovo odvajanje briga dovodi do čišćeg, održivijeg i visoko višekratno upotrebljivog koda.
Od pojednostavljivanja ubrizgavanja ovisnosti i implementacije robusnih sustava validacije do dodavanja poprečnih problema poput bilježenja i praćenja performansi, dekoratori pružaju elegantno rješenje za mnoge uobičajene razvojne izazove. Iako njihov eksperimentalni status zahtijeva oprez, njihova široka primjena u glavnim okvirima označava njihovu praktičnu vrijednost i buduću relevantnost.
Ovladavanjem TypeScript dekoratorima, dobivate značajan alat u svom arsenalu, koji vam omogućuje izgradnju robusnijih, skalabilnijih i inteligentnijih aplikacija. Koristite ih odgovorno, razumijte njihovu mehaniku i otključajte novu razinu deklarativne moći u svojim TypeScript projektima.