Kas tie dekoratoriai ir kodėl jie tokie įdomūs
Jei dirbate su TypeScript, tikriausiai esate girdėję apie dekoratorius. Galbūt net matėte juos Angular projektuose ar nestebėjote @Component virš klasių. Bet kas iš tikrųjų yra šie keisti simboliai su @ ženklu ir kodėl apie juos verta žinoti?
Dekoratoriai – tai meta programavimo įrankis, leidžiantis modifikuoti klasių, metodų, savybių ar parametrų elgseną neliečiant paties kodo. Skamba abstrakčiai? Įsivaizduokite, kad turite galimybę „apvynioti” funkciją ar klasę papildoma logika, tarsi dovanų popierių – originalus turinys lieka nepakitęs, bet įgyja naujų savybių.
TypeScript dekoratoriai šiuo metu yra eksperimentinė funkcija (nors jau seniai naudojama produkcijoje), kuri leidžia programuotojams rašyti deklaratyvesnį ir aiškesnį kodą. Vietoj to, kad rašytumėte daugybę boilerplate kodo, galite tiesiog pridėti @dekoratorius ir leisti jiems atlikti rutininį darbą.
Kaip įjungti dekoratorius savo projekte
Pirmas žingsnis – reikia pasakyti TypeScript kompiliatoriui, kad norite naudoti dekoratorius. Tai padaroma tsconfig.json faile:
„`json
{
„compilerOptions”: {
„target”: „ES2020”,
„experimentalDecorators”: true,
„emitDecoratorMetadata”: true
}
}
„`
Parametras experimentalDecorators įjungia pačią dekoratorių funkciją, o emitDecoratorMetadata leidžia išsaugoti tipo informaciją runtime metu – tai ypač naudinga, kai dirbate su dependency injection sistemomis.
Svarbu paminėti, kad TypeScript 5.0 versijoje buvo pristatyti nauji Stage 3 dekoratoriai, kurie šiek tiek skiriasi nuo senųjų. Jei naudojate naujausią versiją, galite išjungti experimentalDecorators ir naudoti naują standartą. Tačiau daugelis bibliotekų vis dar remiasi senąja implementacija, todėl būkite atsargūs.
Klasių dekoratoriai – pirmasis žingsnis į meta programavimą
Prasidėkime nuo paprasčiausio pavyzdžio – klasės dekoratoriaus. Tarkime, norite automatiškai užloginti, kada klasė buvo sukurta:
„`typescript
function LogClass(target: Function) {
console.log(`Klasė ${target.name} buvo sukurta`);
}
@LogClass
class UserService {
constructor() {
console.log(‘UserService konstruktorius’);
}
}
const service = new UserService();
„`
Šis paprastas pavyzdys parodo pagrindinį principą – dekoratorius yra tiesiog funkcija, kuri priima tikslą (target) kaip parametrą. Klasės atveju tai yra pati konstruktoriaus funkcija.
Bet galime eiti toliau ir modifikuoti klasę:
„`typescript
function Timestamped
return class extends constructor {
createdAt = new Date();
getAge() {
return Date.now() – this.createdAt.getTime();
}
};
}
@Timestamped
class Article {
constructor(public title: string) {}
}
const article = new Article(‘TypeScript dekoratoriai’);
console.log((article as any).createdAt); // Rodo sukūrimo datą
„`
Čia jau matome tikrą meta programavimą – automatiškai pridedame naują funkcionalumą klasei, nekeisdami jos kodo. Praktikoje tai gali būti naudojama audito žurnalams, cache’inimui ar bet kokiai kitai cross-cutting logikai.
Metodų dekoratoriai – funkcionalumo praturtinimas
Metodų dekoratoriai leidžia modifikuoti ar stebėti atskirų klasės metodų elgseną. Štai klasikinis pavyzdys su laikmačiu:
„`typescript
function Measure(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(…args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} užtruko ${end – start}ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@Measure
processLargeDataset(data: number[]) {
return data.reduce((sum, num) => sum + num, 0);
}
}
„`
Šis dekoratorius automatiškai matuoja metodo vykdymo laiką. Įsivaizduokite, kiek kodo sutaupytumėte, jei turėtumėte matuoti dešimtis metodų – vietoj to, kad kiekviename rašytumėte console.time() ir console.timeEnd(), tiesiog pridėkite @Measure.
Dar vienas populiarus panaudojimo atvejis – memoizacija:
„`typescript
function Memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const cache = new Map();
descriptor.value = function(…args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log(‘Grąžinama iš cache’);
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class Calculator {
@Memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
```
Savybių ir parametrų dekoratoriai
Savybių dekoratoriai dažnai naudojami validacijai ar metaduomenų pridėjimui. Pavyzdžiui, galite sukurti paprastą validacijos sistemą:
„`typescript
function Required(target: any, propertyKey: string) {
let value: any;
const getter = () => value;
const setter = (newValue: any) => {
if (newValue === null || newValue === undefined) {
throw new Error(`${propertyKey} yra privalomas laukas`);
}
value = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class User {
@Required
email: string;
constructor(email: string) {
this.email = email;
}
}
// const user = new User(null); // Išmes klaidą
„`
Parametrų dekoratoriai retesni, bet naudingi dependency injection sistemose. Angular juos naudoja masiškai:
„`typescript
function LogParameter(target: any, propertyKey: string, parameterIndex: number) {
const existingParams = Reflect.getMetadata(‘log_parameters’, target, propertyKey) || [];
existingParams.push(parameterIndex);
Reflect.defineMetadata(‘log_parameters’, existingParams, target, propertyKey);
}
class OrderService {
createOrder(@LogParameter orderId: string, @LogParameter userId: string) {
console.log(‘Kuriamas užsakymas’);
}
}
„`
Dekoratorių komponavimas ir tvarka
Vienas iš įdomiausių dalykų – galite naudoti kelis dekoratorius ant vieno elemento. Bet čia svarbu suprasti, kokia tvarka jie vykdomi:
„`typescript
function First() {
console.log(‘First(): factory’);
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(‘First(): called’);
};
}
function Second() {
console.log(‘Second(): factory’);
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(‘Second(): called’);
};
}
class Example {
@First()
@Second()
method() {}
}
// Išvestis:
// First(): factory
// Second(): factory
// Second(): called
// First(): called
„`
Dekoratorių factory funkcijos vykdomos iš viršaus į apačią, bet patys dekoratoriai taikomi atvirkščia tvarka – iš apačios į viršų. Tai svarbu žinoti, kai dekoratoriai priklauso vienas nuo kito.
Praktiniai panaudojimo atvejai realuose projektuose
Teorija teorija, bet kur visa tai naudinga realiame gyvenime? Štai keletas konkrečių pavyzdžių:
API route’ų aprašymas: Jei kuriate REST API su Express ar panašiu framework’u, dekoratoriai puikiai tinka route’ams apibrėžti:
„`typescript
function Get(path: string) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata(‘route’, { method: ‘GET’, path }, target, propertyKey);
};
}
function Post(path: string) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata(‘route’, { method: ‘POST’, path }, target, propertyKey);
};
}
class UserController {
@Get(‘/users’)
getUsers() {
return { users: [] };
}
@Post(‘/users’)
createUser() {
return { success: true };
}
}
„`
Autorizacijos tikrinimas: Vietoj to, kad kiekviename metode tikrintumėte vartotojo teises, galite sukurti dekoratorių:
„`typescript
function RequireRole(role: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(…args: any[]) {
const user = (this as any).currentUser;
if (!user || user.role !== role) {
throw new Error(‘Neturite prieigos teisių’);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class AdminPanel {
currentUser = { role: ‘admin’ };
@RequireRole(‘admin’)
deleteUser(userId: string) {
console.log(`Ištrinamas vartotojas ${userId}`);
}
}
„`
Duomenų validacija: Galite sukurti išsamią validacijos sistemą naudodami savybių dekoratorius:
„`typescript
function MinLength(length: number) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata(‘minLength’, length, target, propertyKey);
};
}
function Email(target: any, propertyKey: string) {
Reflect.defineMetadata(‘isEmail’, true, target, propertyKey);
}
function validate(obj: any): boolean {
const properties = Object.keys(obj);
for (const prop of properties) {
const minLength = Reflect.getMetadata(‘minLength’, obj, prop);
if (minLength && obj[prop].length < minLength) {
return false;
}
const isEmail = Reflect.getMetadata('isEmail', obj, prop);
if (isEmail && !obj[prop].includes('@')) {
return false;
}
}
return true;
}
class RegistrationForm {
@MinLength(3)
username: string;
@Email
email: string;
constructor(username: string, email: string) {
this.username = username;
this.email = email;
}
}
```
Dažniausios klaidos ir kaip jų išvengti
Dirbant su dekoratoriais, lengva įkliūti į tam tikras spąstus. Štai keletas dalykų, į kuriuos verta atkreipti dėmesį:
Konteksto praradimas: Kai keičiate metodo elgseną, būtinai naudokite apply(this, args), kad išlaikytumėte teisingą this kontekstą. Kitaip galite susidurti su keistomis klaidomis.
Tipų saugumas: TypeScript dekoratoriai ne visada idealiai dirba su tipų sistema. Kartais teks naudoti any arba type assertions. Tai normalu, bet stenkitės kuo labiau apriboti any naudojimą.
Perpildymas: Dekoratoriai gali padaryti kodą gražesnį, bet per daug jų gali sukelti priešingą efektą. Jei ant vienos klasės matote 10 dekoratorių, galbūt laikas pagalvoti apie refaktoringą.
Debugging: Kai kas nors neveikia su dekoratoriais, gali būti sunku suprasti, kur problema. Naudokite console.log dekoratoriaus viduje, kad matytumėte, kas vyksta.
Performance: Dekoratoriai prideda papildomą abstrakcijos sluoksnį. Daugeliu atvejų tai neturi įtakos našumui, bet jei dekoratorius vykdomas tūkstančius kartų per sekundę, verta pažiūrėti į profilerį.
Ateitis ir nauji standartai
TypeScript dekoratoriai ilgą laiką buvo eksperimentinė funkcija, bet dabar jie artėja prie oficialaus JavaScript standarto. Stage 3 dekoratoriai jau yra TypeScript 5.0+, ir jie šiek tiek skiriasi nuo senųjų.
Pagrindiniai skirtumai:
– Naujieji dekoratoriai turi kitokią signature
– Geresnė integracija su JavaScript ekosistema
– Paprastesnis metaduomenų valdymas
– Geresnis našumas
Jei pradedate naują projektą, rekomenduočiau naudoti naujuosius dekoratorius. Bet jei dirbate su egzistuojančiu projektu, kuris naudoja Angular ar NestJS, greičiausiai teks likti su senaisiais, kol šie framework’ai atsinaujins.
Įdomu tai, kad React bendruomenė ilgą laiką vengė dekoratorių, bet su naujaisiais standartais situacija gali keistis. Galbūt ateityje matysime daugiau dekoratorių ir React projektuose.
Kada verta ir kada neverta naudoti dekoratorius
Dekoratoriai – galingas įrankis, bet ne visada tinkamas. Štai keletas gairių, kada juos naudoti ir kada geriau susilaikyti.
Naudokite dekoratorius, kai:
– Turite daug pasikartojančio cross-cutting kodo (logging, validacija, autorizacija)
– Norite aiškesnio, deklaratyvesnio kodo
– Kuriate framework’ą ar biblioteką, kur API aiškumas svarbus
– Reikia metaduomenų runtime metu (dependency injection)
Vengkite dekoratorių, kai:
– Projektas paprastas ir dekoratoriai pridėtų nereikalingos sudėtingumo
– Komanda nėra susipažinusi su meta programavimu
– Reikia maksimalaus našumo kritinėse vietose
– Debugging ir palaikymas tampa per sudėtingas
Asmeniškai pastebėjau, kad dekoratoriai labiausiai praverčia vidutinio ir didelio dydžio projektuose, kur yra daug panašių operacijų. Mažuose projektuose jie dažnai būna overkill.
Dar vienas svarbus aspektas – komandos brandumas. Jei jūsų komandoje dirba daug junior programuotojų, dekoratoriai gali sukelti painiavos. Geriau pradėti nuo paprastesnių abstrakcijų ir palaipsniui įvesti sudėtingesnius pattern’us.
Galiausiai, dekoratoriai puikiai dera su kitais TypeScript feature’ais kaip generics, utility types ir conditional types. Kai visa tai sujungiate, galite sukurti labai galingą ir type-safe API. Bet atminkite – su didele galia ateina didelė atsakomybė. Stenkitės, kad jūsų kodas būtų ne tik galingas, bet ir suprantamas kitiems programuotojams.
Meta programavimas su TypeScript dekoratoriais atveria naujas galimybes rašyti švaresnį, aiškesnį ir lengviau palaikomą kodą. Nors iš pradžių koncepcija gali atrodyti sudėtinga, praktika rodo, kad dekoratoriai gali žymiai supaprastinti kasdieninį programavimą. Pradėkite nuo paprastų pavyzdžių, eksperimentuokite ir pamažu rasite savo stilių, kaip geriausiai juos integruoti į savo projektus.
