Suomi

Tutustu TypeScript-dekoraattoreiden voimaan metadatalla ohjelmoinnissa, aspektisuuntautuneessa ohjelmoinnissa ja koodin parantamisessa deklaratiivisilla malleilla. Kattava opas globaaleille kehittäjille.

TypeScript-dekoraattorit: Metadatalla ohjelmoinnin mallien hallinta vankkojen sovellusten luomisessa

Nykyaikaisen ohjelmistokehityksen laajassa kentässä puhtaiden, skaalautuvien ja hallittavien koodikantojen ylläpito on ensisijaisen tärkeää. TypeScript voimakkaalla tyyppijärjestelmällään ja edistyneillä ominaisuuksillaan tarjoaa kehittäjille työkalut tämän saavuttamiseksi. Sen kiehtovimpia ja mullistavimpia ominaisuuksia ovat dekoraattorit. Vaikka dekoraattorit ovat kirjoitushetkellä vielä kokeellinen ominaisuus (Stage 3 -ehdotus ECMAScriptille), niitä käytetään laajalti viitekehyksissä, kuten Angular ja TypeORM, ja ne muuttavat perustavanlaatuisesti tapaamme lähestyä suunnittelumalleja, metadatalla ohjelmointia ja aspektisuuntautunutta ohjelmointia (AOP).

Tämä kattava opas sukeltaa syvälle TypeScript-dekoraattoreihin, tutkien niiden mekaniikkaa, eri tyyppejä, käytännön sovelluksia ja parhaita käytäntöjä. Olitpa rakentamassa suuria yrityssovelluksia, mikropalveluita tai asiakaspuolen verkkokäyttöliittymiä, dekoraattoreiden ymmärtäminen antaa sinulle valmiudet kirjoittaa deklaratiivisempaa, ylläpidettävämpää ja tehokkaampaa TypeScript-koodia.

Ydinkonseptin ymmärtäminen: Mikä on dekoraattori?

Pohjimmiltaan dekoraattori on erityinen julistus, joka voidaan liittää luokkajulistukseen, metodiin, aksessoriin, ominaisuuteen tai parametriin. Dekoraattorit ovat funktioita, jotka palauttavat uuden arvon (tai muokkaavat olemassa olevaa) kohteelle, jota ne dekoroivat. Niiden ensisijainen tarkoitus on lisätä metadataa tai muuttaa julistuksen käyttäytymistä ilman, että sen taustalla olevaa koodirakennetta muutetaan suoraan. Tämä ulkoinen, deklaratiivinen tapa laajentaa koodia on uskomattoman tehokas.

Ajattele dekoraattoreita merkintöinä tai leimoina, joita lisäät koodisi osiin. Nämä leimat voidaan sitten lukea tai niiden perusteella voidaan toimia sovelluksesi muissa osissa tai viitekehyksissä, usein ajon aikana, lisätoiminnallisuuden tai konfiguraation tarjoamiseksi.

Dekoraattorin syntaksi

Dekoraattorit alkavat @-merkillä, jota seuraa dekoraattorifunktion nimi. Ne sijoitetaan välittömästi ennen julistusta, jota ne dekoroivat.

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

Dekoraattoreiden käyttöönotto TypeScriptissä

Ennen kuin voit käyttää dekoraattoreita, sinun on otettava käyttöön experimentalDecorators-kääntäjäasetus tsconfig.json-tiedostossasi. Lisäksi edistyneitä metadatan heijastusominaisuuksia varten (joita viitekehykset usein käyttävät) tarvitset myös emitDecoratorMetadata-asetuksen ja reflect-metadata-polyfillin.

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

Sinun on myös asennettava reflect-metadata:

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

Ja tuotava se sovelluksesi sisääntulopisteen (esim. main.ts tai app.ts) yläosaan:

import "reflect-metadata";
// Sovelluskoodisi seuraa tässä

Dekoraattoritehtaat: Räätälöintiä sormiesi ulottuvilla

Vaikka perusdekoraattori on funktio, usein sinun täytyy välittää argumentteja dekoraattorille sen käyttäytymisen konfiguroimiseksi. Tämä saavutetaan käyttämällä dekoraattoritehdasta. Dekoraattoritehdas on funktio, joka palauttaa varsinaisen dekoraattorifunktion. Kun käytät dekoraattoritehdasta, kutsut sitä sen argumenteilla, ja se palauttaa dekoraattorifunktion, jonka TypeScript soveltaa koodiisi.

Esimerkki yksinkertaisen dekoraattoritehtaan luomisesta

Luodaan tehdas Logger-dekoraattorille, joka voi kirjata viestejä eri etuliitteillä.

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

Tässä esimerkissä Logger("APP_INIT") on dekoraattoritehtaan kutsu. Se palauttaa varsinaisen dekoraattorifunktion, joka ottaa argumentikseen target: Function (luokan konstruktorin). Tämä mahdollistaa dekoraattorin käyttäytymisen dynaamisen konfiguroinnin.

Dekoraattorityypit TypeScriptissä

TypeScript tukee viittä erillistä dekoraattorityyppiä, joista jokainen soveltuu tietynlaiseen julistukseen. Dekoraattorifunktion allekirjoitus vaihtelee sen käyttökontekstin mukaan.

1. Luokkadekoraattorit

Luokkadekoraattoreita sovelletaan luokkajulistuksiin. Dekoraattorifunktio vastaanottaa luokan konstruktorin ainoana argumenttinaan. Luokkadekoraattori voi tarkkailla, muokata tai jopa korvata luokan määritelmän.

Allekirjoitus:

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

Palautusarvo:

Jos luokkadekoraattori palauttaa arvon, se korvaa luokkajulistuksen annetulla konstruktorifunktiolla. Tämä on tehokas ominaisuus, jota käytetään usein mixineissä tai luokan laajentamisessa. Jos arvoa ei palauteta, käytetään alkuperäistä luokkaa.

Käyttötapaukset:

Esimerkki luokkadekoraattorista: Palvelun injektointi

Kuvitellaan yksinkertainen riippuvuuksien injektointitilanne, jossa haluat merkitä luokan "injektoitavaksi" ja valinnaisesti antaa sille nimen säiliössä.

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

    // Valinnaisesti voitaisiin palauttaa uusi luokka käyttäytymisen laajentamiseksi
    return class extends constructor {
      createdAt = new Date();
      // Lisäominaisuuksia tai -metodeja kaikille injektoiduille palveluille
    };
  };
}

@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); // Jos palautettua luokkaa käytetään
}

Tämä esimerkki osoittaa, kuinka luokkadekoraattori voi rekisteröidä luokan ja jopa muokata sen konstruktoria. Injectable-dekoraattori tekee luokasta löydettävän teoreettiselle riippuvuuksien injektointijärjestelmälle.

2. Metodidekoraattorit

Metodidekoraattoreita sovelletaan metodijulistuksiin. Ne saavat kolme argumenttia: kohdeobjektin (staattisille jäsenille konstruktorifunktio; instanssijäsenille luokan prototyyppi), metodin nimen ja metodin ominaisuuden kuvaajan (property descriptor).

Allekirjoitus:

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

Palautusarvo:

Metodidekoraattori voi palauttaa uuden PropertyDescriptor-objektin. Jos se tekee niin, tätä kuvaajaa käytetään metodin määrittelyyn. Tämä antaa sinun muokata tai korvata alkuperäisen metodin toteutuksen, mikä tekee siitä uskomattoman tehokkaan AOP:n kannalta.

Käyttötapaukset:

Esimerkki metodidekoraattorista: Suorituskyvyn seuranta

Luodaan MeasurePerformance-dekoraattori metodin suoritusajan kirjaamiseksi.

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[] {
    // Simuloidaan monimutkaista, aikaa vievää operaatiota
    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));

MeasurePerformance-dekoraattori käärii alkuperäisen metodin ajoituslogiikkaan, tulostaen suorituksen keston ilman, että se sotkee liiketoimintalogiikkaa metodin sisällä. Tämä on klassinen esimerkki aspektisuuntautuneesta ohjelmoinnista (AOP).

3. Aksessoridekoraattorit

Aksessoridekoraattoreita sovelletaan aksessori- (get ja set) julistuksiin. Kuten metodidekoraattorit, ne saavat kohdeobjektin, aksessorin nimen ja sen ominaisuuden kuvaajan.

Allekirjoitus:

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

Palautusarvo:

Aksessoridekoraattori voi palauttaa uuden PropertyDescriptor-objektin, jota käytetään aksessorin määrittelyyn.

Käyttötapaukset:

Esimerkki aksessoridekoraattorista: Getterien välimuistiin tallentaminen

Luodaan dekoraattori, joka tallentaa välimuistiin kalliin getter-laskennan tuloksen.

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

  // Simuloi kallista laskentaa
  @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);

Tämä dekoraattori varmistaa, että expensiveSummary-getterin laskenta suoritetaan vain kerran, ja myöhemmät kutsut palauttavat välimuistissa olevan arvon. Tämä malli on erittäin hyödyllinen suorituskyvyn optimoinnissa, kun ominaisuuden käyttö sisältää raskasta laskentaa tai ulkoisia kutsuja.

4. Ominaisuusdekoraattorit

Ominaisuusdekoraattoreita sovelletaan ominaisuusjulistuksiin. Ne saavat kaksi argumenttia: kohdeobjektin (staattisille jäsenille konstruktorifunktio; instanssijäsenille luokan prototyyppi) ja ominaisuuden nimen.

Allekirjoitus:

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

Palautusarvo:

Ominaisuusdekoraattorit eivät voi palauttaa arvoa. Niiden ensisijainen käyttö on metadatan rekisteröinti ominaisuudesta. Ne eivät voi suoraan muuttaa ominaisuuden arvoa tai sen kuvaajaa dekoroinnin hetkellä, koska ominaisuuden kuvaaja ei ole vielä täysin määritelty, kun ominaisuusdekoraattorit suoritetaan.

Käyttötapaukset:

Esimerkki ominaisuusdekoraattorista: Pakollisen kentän validointi

Luodaan dekoraattori merkitsemään ominaisuus "pakolliseksi" ja validoimaan se sitten ajon aikana.

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

Required-dekoraattori yksinkertaisesti rekisteröi validointisäännön keskitettyyn validationRules-mappiin. Erillinen validate-funktio käyttää sitten tätä metadataa tarkistaakseen instanssin ajon aikana. Tämä malli erottaa validointilogiikan datan määrittelystä, tehden siitä uudelleenkäytettävän ja puhtaan.

5. Parametridekoraattorit

Parametridekoraattoreita sovelletaan parametreihin luokan konstruktorissa tai metodissa. Ne saavat kolme argumenttia: kohdeobjektin (staattisille jäsenille konstruktorifunktio; instanssijäsenille luokan prototyyppi), metodin nimen (tai undefined konstruktorin parametreille) ja parametrin järjestysluvun funktion parametrilistassa.

Allekirjoitus:

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

Palautusarvo:

Parametridekoraattorit eivät voi palauttaa arvoa. Kuten ominaisuusdekoraattorit, niiden päätehtävä on lisätä metadataa parametrista.

Käyttötapaukset:

Esimerkki parametridekoraattorista: Pyyntödatan injektointi

Simuloidaan, kuinka verkkokehys voisi käyttää parametridekoraattoreita injektoimaan tiettyä dataa metodin parametriin, kuten käyttäjätunnuksen pyynnöstä.

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

// Hypoteettinen kehysfunktio metodin kutsumiseksi ratkaistuilla parametreilla
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();

// Simuloidaan saapuvaa pyyntöä
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" });

Tämä esimerkki näyttää, kuinka parametridekoraattorit voivat kerätä tietoa vaadituista metodiparametreista. Kehys voi sitten käyttää tätä kerättyä metadataa ratkaistakseen ja injektoidakseen automaattisesti sopivat arvot, kun metodia kutsutaan, mikä yksinkertaistaa merkittävästi kontrolleri- tai palvelulogiikkaa.

Dekoraattoreiden yhdistely ja suoritusjärjestys

Dekoraattoreita voidaan soveltaa eri yhdistelminä, ja niiden suoritusjärjestyksen ymmärtäminen on ratkaisevan tärkeää käyttäytymisen ennustamiseksi ja odottamattomien ongelmien välttämiseksi.

Useita dekoraattoreita yhdessä kohteessa

Kun useita dekoraattoreita sovelletaan yhteen julistukseen (esim. luokkaan, metodiin tai ominaisuuteen), ne suoritetaan tietyssä järjestyksessä: alhaalta ylös, tai oikealta vasemmalle niiden evaluoinnissa. Niiden tulokset sovelletaan kuitenkin päinvastaisessa järjestyksessä.

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

Tässä DecoratorB evaluoidaan ensin, sitten DecoratorA. Jos ne muokkaavat luokkaa (esim. palauttamalla uuden konstruktorin), DecoratorA:n tekemä muutos käärii tai soveltuu DecoratorB:n tekemän muutoksen päälle.

Esimerkki: Metodidekoraattoreiden ketjutus

Tarkastellaan kahta metodidekoraattoria: LogCall ja 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"]; // Simuloidaan nykyisen käyttäjän roolien hakua
      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 // Järjestys muutettu tässä
  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) ---");
  // Simuloidaan ei-admin-käyttäjää, joka yrittää käyttää fetchPublicData-metodia, joka vaatii 'user'-roolin
  const mockUserRoles = ["guest"]; // Tämä epäonnistuu autentikoinnissa
  // Jotta tämä olisi dynaaminen, tarvitsisit DI-järjestelmän tai staattisen kontekstin nykyisille käyttäjärooleille.
  // Yksinkertaisuuden vuoksi oletamme, että Authorization-dekoraattorilla on pääsy nykyiseen käyttäjäkontekstiin.
  // Muutetaan Authorization-dekoraattori olettamaan aina 'admin' demotarkoituksessa,
  // jotta ensimmäinen kutsu onnistuu ja toinen epäonnistuu näyttääkseen eri polut.
  
  // Aja uudelleen käyttäjäroolilla, jotta fetchPublicData onnistuu.
  // Kuvittele, että currentUserRoles Authorization-dekoraattorissa muuttuu: ['user']
  // Tässä esimerkissä pidetään se yksinkertaisena ja näytetään järjestyksen vaikutus.
  service.fetchPublicData("search term"); // Tämä suorittaa Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* Odotettu tuloste deleteSensitiveData-metodille:
[AUTH] Access granted for deleteSensitiveData
[LOG] Calling deleteSensitiveData with args: [ 'record123' ]
Deleting sensitive data for ID: record123
[LOG] Method deleteSensitiveData returned: Data ID record123 deleted.
*/

/* Odotettu tuloste fetchPublicData-metodille (jos käyttäjällä on 'user'-rooli):
[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
*/

Huomaa järjestys: deleteSensitiveData-metodissa Authorization (alempi) suoritetaan ensin, sitten LogCall (ylempi) käärii sen ympärille. Authorization-dekoraattorin sisäinen logiikka suoritetaan ensin. fetchPublicData-metodissa LogCall (alempi) suoritetaan ensin, sitten Authorization (ylempi) käärii sen ympärille. Tämä tarkoittaa, että LogCall-aspekti on Authorization-aspektin ulkopuolella. Tämä ero on kriittinen läpileikkaavien huolenaiheiden, kuten kirjaamisen tai virheenkäsittelyn, kannalta, missä suoritusjärjestys voi vaikuttaa merkittävästi käyttäytymiseen.

Suoritusjärjestys eri kohteille

Kun luokalla, sen jäsenillä ja parametreilla on kaikilla dekoraattoreita, suoritusjärjestys on tarkasti määritelty:

  1. Parametridekoraattorit sovelletaan ensin, jokaiselle parametrille, alkaen viimeisestä parametrista ensimmäiseen.
  2. Sitten Metodi-, Aksessori- tai Ominaisuusdekoraattorit sovelletaan jokaiselle jäsenelle.
  3. Lopuksi Luokkadekoraattorit sovelletaan itse luokkaan.

Kussakin kategoriassa useat dekoraattorit samassa kohteessa sovelletaan alhaalta ylös (tai oikealta vasemmalle).

Esimerkki: Täydellinen suoritusjärjestys

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; // Palauta kuvaaja metodi/aksessori-dekoraattorille, muuten undefined
  };
}

@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();
// Kutsu metodia laukaistaksesi metodidekoraattorin
new MyDecoratedClass().myMethod("hello", 123);

/* Ennustettu tulostusjärjestys (likimääräinen, riippuen TypeScript-versiosta ja käännöksestä):
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.
*/

Tarkka konsolilokin ajoitus voi vaihdella hieman sen mukaan, milloin konstruktori tai metodi kutsutaan, mutta järjestys, jossa dekoraattorifunktiot itse suoritetaan (ja siten niiden sivuvaikutukset tai palautetut arvot sovelletaan), noudattaa yllä olevia sääntöjä.

Käytännön sovellukset ja suunnittelumallit dekoraattoreilla

Dekoraattorit, erityisesti yhdessä reflect-metadata-polyfillin kanssa, avaavat uuden metadatavetoisen ohjelmoinnin maailman. Tämä mahdollistaa tehokkaita suunnittelumalleja, jotka abstrahoivat pois toistuvaa koodia ja läpileikkaavia huolenaiheita.

1. Riippuvuuksien injektointi (DI)

Yksi merkittävimmistä dekoraattoreiden käyttötavoista on riippuvuuksien injektointikehyksissä (kuten Angularin @Injectable(), @Component() jne., tai NestJS:n laajassa DI-käytössä). Dekoraattorit mahdollistavat riippuvuuksien ilmoittamisen suoraan konstruktoreissa tai ominaisuuksissa, jolloin kehys voi automaattisesti luoda ja tarjota oikeat palvelut.

Esimerkki: Yksinkertaistettu palvelun injektointi

import "reflect-metadata"; // Välttämätön emitDecoratorMetadata-asetukselle

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

    // Hae konstruktorin parametrien tyypit (vaatii emitDecoratorMetadata)
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // Käytä eksplisiittistä @Inject-tokenia, jos annettu, muuten päättele tyyppi
      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;
  }
}

// Määritä palvelut
@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; // Esimerkki injektoinnista ominaisuuden kautta käyttäen omaa dekoraattoria tai kehysominaisuutta

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

// Ratkaise pääpalvelu
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();

Tämä monimutkainen esimerkki osoittaa, kuinka @Injectable- ja @Inject-dekoraattorit yhdessä reflect-metadata:n kanssa mahdollistavat mukautetun Container-säiliön automaattisen riippuvuuksien ratkaisun ja tarjoamisen. TypeScriptin automaattisesti emittoima design:paramtypes-metadata (kun emitDecoratorMetadata on tosi) on tässä ratkaisevan tärkeä.

2. Aspektisuuntautunut ohjelmointi (AOP)

AOP keskittyy läpileikkaavien huolenaiheiden (esim. kirjaaminen, turvallisuus, transaktiot) modularisointiin, jotka leikkaavat useiden luokkien ja moduulien läpi. Dekoraattorit sopivat erinomaisesti AOP-konseptien toteuttamiseen TypeScriptissä.

Esimerkki: Kirjaaminen metodidekoraattorilla

Palatakseni LogCall-dekoraattoriin, se on täydellinen esimerkki AOP:sta. Se lisää kirjaamiskäyttäytymisen mihin tahansa metodiin muuttamatta metodin alkuperäistä koodia. Tämä erottaa "mitä tehdä" (liiketoimintalogiikka) "miten se tehdään" (kirjaaminen, suorituskyvyn seuranta jne.).

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

Tämä lähestymistapa pitää PaymentProcessor-luokan keskittyneenä puhtaasti maksulogiikkaan, kun taas LogMethod-dekoraattori hoitaa kirjaamisen läpileikkaavan huolenaiheen.

3. Validointi ja muunnos

Dekoraattorit ovat uskomattoman hyödyllisiä validointisääntöjen määrittelyyn suoraan ominaisuuksille tai datan muuntamiseen serialisoinnin/deserialisoinnin aikana.

Esimerkki: Datan validointi ominaisuusdekoraattoreilla

Aiempi @Required-esimerkki jo osoitti tämän. Tässä on toinen esimerkki numeerisella aluevalidoinnilla.

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

Tämä asetus antaa sinun määrittää validointisäännöt deklaratiivisesti mallin ominaisuuksille, mikä tekee datamalleistasi itsemäärääviä rajoitustensa suhteen.

Parhaat käytännöt ja huomioitavat asiat

Vaikka dekoraattorit ovat tehokkaita, niitä tulisi käyttää harkitusti. Väärinkäyttö voi johtaa koodiin, joka on vaikeampi debugata tai ymmärtää.

Milloin käyttää dekoraattoreita (ja milloin ei)

Suorituskykyvaikutukset

Dekoraattorit suoritetaan käännösaikana (tai määrittelyaikana JavaScript-ajoympäristössä, jos se on transpiiloitu). Muunnos tai metadatan kerääminen tapahtuu, kun luokka/metodi määritellään, ei jokaisella kutsulla. Siksi dekoraattoreiden *soveltamisen* ajonaikainen suorituskykyvaikutus on minimaalinen. Kuitenkin dekoraattoreidesi *sisällä oleva logiikka* voi vaikuttaa suorituskykyyn, erityisesti jos ne suorittavat kalliita operaatioita jokaisella metodikutsulla (esim. monimutkaiset laskelmat metodidekoraattorissa).

Ylläpidettävyys ja luettavuus

Oikein käytettynä dekoraattorit voivat parantaa merkittävästi luettavuutta siirtämällä toistuvaa koodia pois päälogiikasta. Jos ne kuitenkin suorittavat monimutkaisia, piilotettuja muunnoksia, debuggaus voi muuttua haastavaksi. Varmista, että dekoraattorisi ovat hyvin dokumentoituja ja niiden käyttäytyminen on ennustettavissa.

Kokeellinen status ja dekoraattoreiden tulevaisuus

On tärkeää toistaa, että TypeScript-dekoraattorit perustuvat Stage 3 TC39 -ehdotukseen. Tämä tarkoittaa, että määrittely on suurelta osin vakaa, mutta voi silti kokea pieniä muutoksia ennen kuin siitä tulee osa virallista ECMAScript-standardia. Kehykset, kuten Angular, ovat ottaneet ne omakseen, luottaen niiden lopulliseen standardointiin. Tämä merkitsee tiettyä riskiä, vaikka niiden laajan käyttöönoton vuoksi merkittävät rikkovat muutokset ovat epätodennäköisiä.

TC39-ehdotus on kehittynyt. TypeScriptin nykyinen toteutus perustuu ehdotuksen vanhempaan versioon. On olemassa ero "Legacy Decorators" ja "Standard Decorators" välillä. Kun virallinen standardi julkaistaan, TypeScript todennäköisesti päivittää toteutuksensa. Useimmille kehittäjille, jotka käyttävät kehyksiä, tämä siirtymä hoidetaan kehyksen itsensä toimesta. Kirjastojen tekijöille hienovaraisten erojen ymmärtäminen vanhojen ja tulevien standardidekoraattoreiden välillä saattaa tulla tarpeelliseksi.

emitDecoratorMetadata-kääntäjäasetus

Tämä asetus, kun se on asetettu true-arvoon tsconfig.json-tiedostossa, ohjaa TypeScript-kääntäjää emittoimaan tiettyä suunnitteluaikaista tyyppimetadataa käännettyyn JavaScriptiin. Tämä metadata sisältää konstruktorin parametrien tyypin (design:paramtypes), metodien palautustyypin (design:returntype) ja ominaisuuksien tyypin (design:type).

Tämä emittoitu metadata ei ole osa standardia JavaScript-ajoympäristöä. Sitä kuluttaa tyypillisesti reflect-metadata-polyfill, joka sitten tekee sen saataville Reflect.getMetadata()-funktioiden kautta. Tämä on ehdottoman kriittistä edistyneille malleille, kuten riippuvuuksien injektoinnille, jossa säiliön on tiedettävä luokan vaatimien riippuvuuksien tyypit ilman eksplisiittistä konfigurointia.

Edistyneet mallit dekoraattoreilla

Dekoraattoreita voidaan yhdistellä ja laajentaa entistäkin kehittyneempien mallien rakentamiseksi.

1. Dekoraattoreiden dekorointi (korkeamman asteen dekoraattorit)

Voit luoda dekoraattoreita, jotka muokkaavat tai yhdistelevät muita dekoraattoreita. Tämä on harvinaisempaa, mutta osoittaa dekoraattoreiden funktionaalisen luonteen.

// Dekoraattori, joka varmistaa, että metodi kirjataan ja vaatii myös admin-roolit
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // Sovella Authorization ensin (sisempi)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // Sitten sovella LogCall (ulompi)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // Palauta muokattu kuvaaja
  };
}

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

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Odotettu tuloste (olettaen admin-roolin):
[AUTH] Access granted for deleteUserAccount
[LOG] Calling deleteUserAccount with args: [ 'user007' ]
Deleting user account: user007
[LOG] Method deleteUserAccount returned: User user007 deleted.
*/

Tässä AdminAndLoggedMethod on tehdas, joka palauttaa dekoraattorin, ja sen dekoraattorin sisällä se soveltaa kahta muuta dekoraattoria. Tämä malli voi kapseloida monimutkaisia dekoraattoriyhdistelmiä.

2. Dekoraattoreiden käyttö mixineissä

Vaikka TypeScript tarjoaa muita tapoja toteuttaa mixinejä, dekoraattoreita voidaan käyttää ominaisuuksien injektoimiseen luokkiin deklaratiivisella tavalla.

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 {
  // Nämä ominaisuudet/metodit injektoidaan dekoraattorilla
  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}`);

Tämä @ApplyMixins-dekoraattori kopioi dynaamisesti metodeja ja ominaisuuksia peruskonstruktoreista johdetun luokan prototyyppiin, tehokkaasti "sekoittaen" toiminnallisuuksia.

Yhteenveto: Modernin TypeScript-kehityksen voimaannuttaminen

TypeScript-dekoraattorit ovat tehokas ja ilmeikäs ominaisuus, joka mahdollistaa uuden metadatavetoisen ja aspektisuuntautuneen ohjelmoinnin paradigman. Ne antavat kehittäjille mahdollisuuden parantaa, muokata ja lisätä deklaratiivisia käyttäytymismalleja luokkiin, metodeihin, ominaisuuksiin, aksessoreihin ja parametreihin muuttamatta niiden ydinlogiikkaa. Tämä huolenaiheiden erottelu johtaa puhtaampaan, ylläpidettävämpään ja erittäin uudelleenkäytettävään koodiin.

Riippuvuuksien injektoinnin yksinkertaistamisesta ja vankkojen validointijärjestelmien toteuttamisesta aina läpileikkaavien huolenaiheiden, kuten kirjaamisen ja suorituskyvyn seurannan, lisäämiseen, dekoraattorit tarjoavat elegantin ratkaisun moniin yleisiin kehityshaasteisiin. Vaikka niiden kokeellinen status vaatii tietoisuutta, niiden laaja käyttöönotto suurissa kehyksissä osoittaa niiden käytännön arvon ja tulevaisuuden merkityksen.

Hallitsemalla TypeScript-dekoraattorit saat merkittävän työkalun arsenaaliisi, joka mahdollistaa entistä vankempien, skaalautuvampien ja älykkäämpien sovellusten rakentamisen. Ota ne käyttöön vastuullisesti, ymmärrä niiden mekaniikka ja avaa uusi deklaratiivisen voiman taso TypeScript-projekteissasi.