Polski

Odkryj moc dekoratorów TypeScript w programowaniu metadanych, programowaniu zorientowanym na aspekty i usprawnianiu kodu za pomocą wzorców deklaratywnych. Kompleksowy przewodnik dla globalnych deweloperów.

Dekoratory TypeScript: Opanowanie wzorców programowania metadanych dla solidnych aplikacji

W rozległym krajobrazie nowoczesnego tworzenia oprogramowania, utrzymanie czystych, skalowalnych i zarządzalnych baz kodu jest sprawą najwyższej wagi. TypeScript, dzięki swojemu potężnemu systemowi typów i zaawansowanym funkcjom, dostarcza programistom narzędzi do osiągnięcia tego celu. Wśród jego najbardziej intrygujących i transformujących funkcji znajdują się Dekoratory. Chociaż nadal są funkcją eksperymentalną w momencie pisania tego tekstu (propozycja Stage 3 dla ECMAScript), dekoratory są szeroko stosowane w frameworkach takich jak Angular i TypeORM, fundamentalnie zmieniając sposób, w jaki podchodzimy do wzorców projektowych, programowania metadanych i programowania zorientowanego na aspekty (AOP).

Ten kompleksowy przewodnik zagłębi się w dekoratory TypeScript, badając ich mechanizmy, różne typy, praktyczne zastosowania i najlepsze praktyki. Niezależnie od tego, czy tworzysz aplikacje korporacyjne na dużą skalę, mikrousługi, czy interfejsy internetowe po stronie klienta, zrozumienie dekoratorów pozwoli Ci pisać bardziej deklaratywny, łatwiejszy w utrzymaniu i potężniejszy kod TypeScript.

Zrozumienie podstawowej koncepcji: Co to jest dekorator?

U podstaw dekorator jest specjalnym rodzajem deklaracji, którą można dołączyć do deklaracji klasy, metody, akcesora, właściwości lub parametru. Dekoratory to funkcje, które zwracają nową wartość (lub modyfikują istniejącą) dla celu, który dekorują. Ich głównym celem jest dodawanie metadanych lub zmiana zachowania deklaracji, do której są dołączone, bez bezpośredniej modyfikacji bazowej struktury kodu. Ten zewnętrzny, deklaratywny sposób wzbogacania kodu jest niezwykle potężny.

Traktuj dekoratory jako adnotacje lub etykiety, które nakładasz na fragmenty swojego kodu. Etykiety te mogą być następnie odczytywane lub przetwarzane przez inne części Twojej aplikacji lub przez frameworki, często w czasie wykonywania, w celu zapewnienia dodatkowej funkcjonalności lub konfiguracji.

Składnia dekoratora

Dekoratory są poprzedzone symbolem @, po którym następuje nazwa funkcji dekoratora. Są one umieszczane bezpośrednio przed deklaracją, którą dekorują.

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

Włączanie dekoratorów w TypeScript

Zanim będziesz mógł używać dekoratorów, musisz włączyć opcję kompilatora experimentalDecorators w swoim pliku tsconfig.json. Dodatkowo, dla zaawansowanych możliwości odbicia metadanych (często używanych przez frameworki), będziesz również potrzebować emitDecoratorMetadata i polifillu reflect-metadata.

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

Musisz również zainstalować reflect-metadata:

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

I zaimportować go na samym początku punktu wejścia aplikacji (np. main.ts lub app.ts):

import "reflect-metadata";
// Twój kod aplikacji następuje później

Fabryki dekoratorów: Dostosowanie na wyciągnięcie ręki

Chociaż podstawowy dekorator jest funkcją, często trzeba przekazywać argumenty do dekoratora, aby skonfigurować jego zachowanie. Osiąga się to za pomocą fabryki dekoratorów. Fabryka dekoratorów to funkcja, która zwraca faktyczną funkcję dekoratora. Kiedy stosujesz fabrykę dekoratorów, wywołujesz ją z jej argumentami, a następnie zwraca ona funkcję dekoratora, którą TypeScript stosuje do Twojego kodu.

Tworzenie prostego przykładu fabryki dekoratorów

Stwórzmy fabrykę dla dekoratora Logger, który może logować komunikaty z różnymi prefiksami.

function Logger(prefix: string) {
  return function (target: Function) {
    console.log(`[${prefix}] Klasa ${target.name} została zdefiniowana.`);
  };
}

@Logger("APP_INIT")
class ApplicationBootstrap {
  constructor() {
    console.log("Aplikacja się uruchamia...");
  }
}

const app = new ApplicationBootstrap();
// Wyjście:
// [APP_INIT] Klasa ApplicationBootstrap została zdefiniowana.
// Aplikacja się uruchamia...

W tym przykładzie Logger("APP_INIT") jest wywołaniem fabryki dekoratorów. Zwraca ona faktyczną funkcję dekoratora, która przyjmuje target: Function (konstruktor klasy) jako argument. Pozwala to na dynamiczną konfigurację zachowania dekoratora.

Typy dekoratorów w TypeScript

TypeScript obsługuje pięć różnych typów dekoratorów, z których każdy ma zastosowanie do określonego rodzaju deklaracji. Sygnatura funkcji dekoratora różni się w zależności od kontekstu, w którym jest stosowana.

1. Dekoratory klas

Dekoratory klas są stosowane do deklaracji klas. Funkcja dekoratora otrzymuje konstruktor klasy jako jedyny argument. Dekorator klasy może obserwować, modyfikować, a nawet zastępować definicję klasy.

Sygnatura:

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

Wartość zwracana:

Jeśli dekorator klasy zwróci wartość, zastąpi deklarację klasy podanym konstruktorem. Jest to potężna funkcja, często używana do mixinów lub rozszerzeń klas. Jeśli żadna wartość nie zostanie zwrócona, używana jest oryginalna klasa.

Przypadki użycia:

Przykład dekoratora klasy: Wstrzykiwanie usługi

Wyobraź sobie prosty scenariusz wstrzykiwania zależności, gdzie chcesz oznaczyć klasę jako "możliwą do wstrzyknięcia" i opcjonalnie podać jej nazwę w kontenerze.

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(`Zarejestrowano usługę: ${serviceName}`);

    // Opcjonalnie możesz zwrócić nową klasę tutaj, aby rozszerzyć zachowanie
    return class extends constructor {
      createdAt = new Date();
      // Dodatkowe właściwości lub metody dla wszystkich wstrzykiwanych usług
    };
  };
}

@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: "Mysz" }];
  }
}

console.log("--- Usługi zarejestrowane ---");
console.log(Array.from(InjectableServiceRegistry.keys()));

const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
  const userServiceInstance = new userServiceConstructor();
  console.log("Użytkownicy:", userServiceInstance.getUsers());
  // console.log("UserService utworzony o:", userServiceInstance.createdAt); // Jeśli użyto zwróconej klasy
}

Ten przykład pokazuje, jak dekorator klasy może zarejestrować klasę, a nawet zmodyfikować jej konstruktor. Dekorator Injectable czyni klasę odkrywalną przez hipotetyczny system wstrzykiwania zależności.

2. Dekoratory metod

Dekoratory metod są stosowane do deklaracji metod. Otrzymują trzy argumenty: obiekt docelowy (dla członków statycznych, funkcję konstruktora; dla członków instancji, prototyp klasy), nazwę metody i deskryptor właściwości metody.

Sygnatura:

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

Wartość zwracana:

Dekorator metody może zwrócić nowy PropertyDescriptor. Jeśli to zrobi, ten deskryptor zostanie użyty do zdefiniowania metody. Pozwala to na modyfikację lub zastąpienie oryginalnej implementacji metody, co czyni ją niezwykle potężną dla AOP.

Przypadki użycia:

Przykład dekoratora metody: Monitorowanie wydajności

Stwórzmy dekorator MeasurePerformance, aby logować czas wykonania metody.

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}" wykonana w ${duration.toFixed(2)} ms`);
    return result;
  };

  return descriptor;
}

class DataProcessor {
  @MeasurePerformance
  processData(data: number[]): number[] {
    // Symulacja złożonej, czasochłonnej operacji
    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(`Dane dla ID: ${id}`);
      }, 500);
    });
  }
}

const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));

Dekorator MeasurePerformance opakowuje oryginalną metodę logiką pomiaru czasu, drukując czas wykonania bez zaśmiecania logiki biznesowej w samej metodzie. Jest to klasyczny przykład programowania zorientowanego na aspekty (AOP).

3. Dekoratory akcesorów

Dekoratory akcesorów są stosowane do deklaracji akcesorów (get i set). Podobnie jak dekoratory metod, otrzymują one obiekt docelowy, nazwę akcesora i jego deskryptor właściwości.

Sygnatura:

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

Wartość zwracana:

Dekorator akcesora może zwrócić nowy PropertyDescriptor, który zostanie użyty do zdefiniowania akcesora.

Przypadki użycia:

Przykład dekoratora akcesora: Cache'owanie getterów

Stwórzmy dekorator, który cache'uje wynik kosztownego obliczenia getteru.

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] Obliczanie wartości dla ${String(propertyKey)}`);
        this[cacheKey] = originalGetter.apply(this);
      } else {
        console.log(`[Cache Hit] Używanie cache'owanej wartości dla ${String(propertyKey)}`);
      }
      return this[cacheKey];
    };
  }
  return descriptor;
}

class ReportGenerator {
  private data: number[];

  constructor(data: number[]) {
    this.data = data;
  }

  // Symuluje kosztowne obliczenie
  @CachedGetter
  get expensiveSummary(): number {
    console.log("Wykonywanie kosztownego obliczenia podsumowania...");
    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
  }
}

const generator = new ReportGenerator([10, 20, 30, 40, 50]);

console.log("Pierwszy dostęp:", generator.expensiveSummary);
console.log("Drugi dostęp:", generator.expensiveSummary);
console.log("Trzeci dostęp:", generator.expensiveSummary);

Ten dekorator zapewnia, że obliczenie getteru expensiveSummary jest wykonywane tylko raz, kolejne wywołania zwracają cache'owaną wartość. Ten wzorzec jest bardzo przydatny do optymalizacji wydajności, gdy dostęp do właściwości obejmuje ciężkie obliczenia lub wywołania zewnętrzne.

4. Dekoratory właściwości

Dekoratory właściwości są stosowane do deklaracji właściwości. Otrzymują dwa argumenty: obiekt docelowy (dla członków statycznych, funkcję konstruktora; dla członków instancji, prototyp klasy) i nazwę właściwości.

Sygnatura:

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

Wartość zwracana:

Dekoratory właściwości nie mogą zwracać żadnej wartości. Ich głównym zastosowaniem jest rejestrowanie metadanych dotyczących właściwości. Nie mogą one bezpośrednio zmieniać wartości właściwości ani jej deskryptora w momencie dekoracji, ponieważ deskryptor dla właściwości nie jest jeszcze w pełni zdefiniowany, gdy uruchamiane są dekoratory właściwości.

Przypadki użycia:

Przykład dekoratora właściwości: Walidacja wymaganego pola

Stwórzmy dekorator, aby oznaczyć właściwość jako "wymaganą", a następnie zweryfikować ją w czasie wykonywania.

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)} jest wymagane.`
  });
  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("Błędy walidacji użytkownika 1:", validate(user1)); // []

const user2 = new UserProfile("", "Smith");
console.log("Błędy walidacji użytkownika 2:", validate(user2)); // ["firstName jest wymagane."]

const user3 = new UserProfile("Alice", "");
console.log("Błędy walidacji użytkownika 3:", validate(user3)); // ["lastName jest wymagane."]

Dekorator Required po prostu rejestruje regułę walidacji w centralnej mapie validationRules. Oddzielna funkcja validate następnie wykorzystuje te metadane do sprawdzania instancji w czasie wykonywania. Ten wzorzec oddziela logikę walidacji od definicji danych, czyniąc ją wielokrotnego użytku i czystą.

5. Dekoratory parametrów

Dekoratory parametrów są stosowane do parametrów w konstruktorze klasy lub metodzie. Otrzymują one trzy argumenty: obiekt docelowy (dla członków statycznych, funkcję konstruktora; dla członków instancji, prototyp klasy), nazwę metody (lub undefined dla parametrów konstruktora) i indeks kolejności parametru w liście parametrów funkcji.

Sygnatura:

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

Wartość zwracana:

Dekoratory parametrów nie mogą zwracać żadnej wartości. Podobnie jak dekoratory właściwości, ich główną rolą jest dodawanie metadanych dotyczących parametru.

Przypadki użycia:

Przykład dekoratora parametru: Wstrzykiwanie danych żądania

Zasymulujmy, jak framework webowy może używać dekoratorów parametrów do wstrzykiwania konkretnych danych do parametru metody, takich jak identyfikator użytkownika z żądania.

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

// Hipotetyczna funkcja frameworku do wywoływania metody z rozwiązanymi parametrami
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(`Pobieranie użytkownika o ID: ${userId}, Token: ${authToken || "N/A"}`);
    return { id: userId, name: "Jane Doe" };
  }

  deleteUser(@RequestParam("id") userId: string) {
    console.log(`Usuwanie użytkownika o ID: ${userId}`);
    return { status: "deleted", id: userId };
  }
}

const userController = new UserController();

// Symulacja przychodzącego żądania
const mockRequest = {
  id: "user123",
  token: "abc-123",
  someOtherProp: "xyz"
};

console.log("
--- Wykonywanie getUser ---");
executeWithParams(userController, "getUser", mockRequest);

console.log("
--- Wykonywanie deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });

Ten przykład pokazuje, jak dekoratory parametrów mogą zbierać informacje o wymaganych parametrach metod. Framework może następnie wykorzystać te zebrane metadane do automatycznego rozwiązywania i wstrzykiwania odpowiednich wartości podczas wywoływania metody, znacznie upraszczając logikę kontrolera lub usługi.

Kompozycja dekoratorów i kolejność wykonania

Dekoratory mogą być stosowane w różnych kombinacjach, a zrozumienie ich kolejności wykonania jest kluczowe dla przewidywania zachowania i unikania nieoczekiwanych problemów.

Wiele dekoratorów na jednym celu

Gdy wiele dekoratorów jest stosowanych do jednej deklaracji (np. klasy, metody lub właściwości), są one wykonywane w określonej kolejności: od dołu do góry, lub od prawej do lewej podczas ich ewaluacji. Jednak ich wyniki są stosowane w odwrotnej kolejności.

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

Tutaj DecoratorB zostanie ewaluowany jako pierwszy, a następnie DecoratorA. Jeśli modyfikują klasę (np. zwracając nowy konstruktor), modyfikacja z DecoratorA opakuje lub nałoży się na modyfikację z DecoratorB.

Przykład: Łączenie dekoratorów metod

Rozważmy dwa dekoratory metod: LogCall i Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] Wywołanie ${String(propertyKey)} z argumentami:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] Metoda ${String(propertyKey)} zwróciła:`, 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"]; // Symulacja pobrania ról bieżącego użytkownika
      const authorized = roles.some(role => currentUserRoles.includes(role));
      if (!authorized) {
        console.warn(`[AUTH] Dostęp zabroniony dla ${String(propertyKey)}. Wymagane role: ${roles.join(", ")}`);
        throw new Error("Nieautoryzowany dostęp");
      }
      console.log(`[AUTH] Dostęp przyznany dla ${String(propertyKey)}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class SecureService {
  @LogCall
  @Authorization(["admin"])
  deleteSensitiveData(id: string) {
    console.log(`Usuwanie poufnych danych dla ID: ${id}`);
    return `Dane ID ${id} usunięte.`;
  }

  @Authorization(["user"])
  @LogCall // Kolejność zmieniona tutaj
  fetchPublicData(query: string) {
    console.log(`Pobieranie danych publicznych z zapytaniem: ${query}`);
    return `Dane publiczne dla zapytania: ${query}`; 
  }
}

const service = new SecureService();

try {
  console.log("
--- Wywołanie deleteSensitiveData (Użytkownik Admin) ---");
  service.deleteSensitiveData("record123");
} catch (error: any) {
  console.error(error.message);
}

try {
  console.log("
--- Wywołanie fetchPublicData (Użytkownik nie-admin) ---");
  // Symulacja próby dostępu nie-administratora do fetchPublicData, która wymaga roli 'user'
  const mockUserRoles = ["guest"]; // To spowoduje błąd autoryzacji
  // Aby uczynić to dynamicznym, potrzebowalibyśmy systemu DI lub statycznego kontekstu dla ról bieżącego użytkownika.
  // Dla uproszczenia, zakładamy, że dekorator Authorization ma dostęp do kontekstu bieżącego użytkownika.
  // Dostosujmy dekorator Authorization, aby zawsze zakładał 'admin' dla celów demonstracyjnych, 
  // aby pierwsze wywołanie się powiodło, a drugie nie, pokazując różne ścieżki.
  
  // Ponowne uruchomienie z rolą użytkownika dla fetchPublicData, aby się powiodło.
  // Wyobraźmy sobie, że currentUserRoles w Authorization staje się: ['user']
  // Dla tego przykładu, zachowajmy prostotę i pokażmy efekt kolejności.
  
  service.fetchPublicData("szukany termin"); // To wykona Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* Oczekiwane wyjście dla deleteSensitiveData:
[AUTH] Dostęp przyznany dla deleteSensitiveData
[LOG] Wywołanie deleteSensitiveData z argumentami: [ 'record123' ]
Usuwanie poufnych danych dla ID: record123
[LOG] Metoda deleteSensitiveData zwróciła: Dane ID record123 usunięte.
*/

/* Oczekiwane wyjście dla fetchPublicData (jeśli użytkownik ma rolę 'user'):
[LOG] Wywołanie fetchPublicData z argumentami: [ 'szukany termin' ]
[AUTH] Dostęp przyznany dla fetchPublicData
Pobieranie danych publicznych z zapytaniem: szukany termin
[LOG] Metoda fetchPublicData zwróciła: Dane publiczne dla zapytania: szukany termin
*/

Zwróć uwagę na kolejność: dla deleteSensitiveData, Authorization (na dole) działa najpierw, a następnie LogCall (na górze) opakowuje ją. Wewnętrzna logika Authorization jest wykonywana najpierw. Dla fetchPublicData, LogCall (na dole) działa najpierw, a następnie Authorization (na górze) opakowuje ją. Oznacza to, że aspekt LogCall będzie znajdował się poza aspektem Authorization. Ta różnica jest kluczowa dla aspektów przekrojowych, takich jak logowanie lub obsługa błędów, gdzie kolejność wykonania może znacząco wpływać na zachowanie.

Kolejność wykonania dla różnych celów

Gdy klasa, jej członkowie i parametry mają dekoratory, kolejność wykonania jest jasno zdefiniowana:

  1. Dekoratory parametrów są stosowane najpierw, dla każdego parametru, od ostatniego parametru do pierwszego.
  2. Następnie stosowane są Dekoratory metod, akcesorów lub właściwości dla każdego członka.
  3. Na koniec Dekoratory klas są stosowane do samej klasy.

W ramach każdej kategorii, wiele dekoratorów na tym samym celu jest stosowanych od dołu do góry (lub od prawej do lewej).

Przykład: Pełna kolejność wykonania

function log(message: string) {
  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
    if (typeof descriptorOrIndex === 'number') {
      console.log(`Dekorator parametru: ${message} na parametrze #${descriptorOrIndex} z ${String(propertyKey || "constructor")}`);
    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
        console.log(`Dekorator metody/akcesora: ${message} na ${String(propertyKey)}`);
      } else {
        console.log(`Dekorator właściwości: ${message} na ${String(propertyKey)}`);
      }
    } else {
      console.log(`Dekorator klasy: ${message} na ${target.name}`);
    }
    return descriptorOrIndex; // Zwróć deskryptor dla metody/akcesora, undefined dla innych
  };
}

@log("Poziom klasy D")
@log("Poziom klasy C")
class MyDecoratedClass {
  @log("Właściwość statyczna A")
  static staticProp: string = "";

  @log("Właściwość instancji B")
  instanceProp: number = 0;

  @log("Metoda D")
  @log("Metoda C")
  myMethod(
    @log("Parametr Z") paramZ: string,
    @log("Parametr Y") paramY: number
  ) {
    console.log("Metoda myMethod wykonana.");
  }

  @log("Akcesor F")
  get myAccessor() {
    return "";
  }

  set myAccessor(value: string) {
    //...
  }

  constructor() {
    console.log("Konstruktor wykonany.");
  }
}

new MyDecoratedClass();
// Wywołaj metodę, aby wywołać dekorator metody
new MyDecoratedClass().myMethod("cześć", 123);

/* Przewidywana kolejność wyjścia (przybliżona, w zależności od konkretnej wersji TypeScript i kompilacji):
Dekorator parametru: Parametr Y na parametrze #1 z myMethod
Dekorator parametru: Parametr Z na parametrze #0 z myMethod
Dekorator właściwości: Właściwość statyczna A na staticProp
Dekorator właściwości: Właściwość instancji B na instanceProp
Dekorator metody/akcesora: Akcesor F na myAccessor
Dekorator metody/akcesora: Metoda C na myMethod
Dekorator metody/akcesora: Metoda D na myMethod
Dekorator klasy: Poziom klasy C na MyDecoratedClass
Dekorator klasy: Poziom klasy D na MyDecoratedClass
Konstruktor wykonany.
Metoda myMethod wykonana.
*/

Dokładny czas logowania konsoli może się nieznacznie różnić w zależności od tego, kiedy konstruktor lub metoda jest wywoływana, ale kolejność, w jakiej same funkcje dekoratorów są wykonywane (i w związku z tym ich efekty uboczne lub zwrócone wartości są stosowane), jest zgodna z powyższymi zasadami.

Praktyczne zastosowania i wzorce projektowe z dekoratorami

Dekoratory, zwłaszcza w połączeniu z polifillem reflect-metadata, otwierają nowe królestwo programowania sterowanego metadanymi. Pozwala to na potężne wzorce projektowe, które abstrahują od kodu powtarzalnego i aspektów przekrojowych.

1. Wstrzykiwanie zależności (DI)

Jednym z najbardziej znanych zastosowań dekoratorów są frameworki wstrzykiwania zależności (jak @Injectable(), @Component() itp. w Angularze lub szerokie użycie DI w NestJS). Dekoratory pozwalają na deklarowanie zależności bezpośrednio na konstruktorach lub właściwościach, umożliwiając frameworkowi automatyczne instancjonowanie i dostarczanie poprawnych usług.

Przykład: Uproszczone wstrzykiwanie usług

import "reflect-metadata"; // Niezbędne dla 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} nie jest oznaczona jako @Injectable.`);
    }

    // Pobierz typy parametrów konstruktora (wymaga emitDecoratorMetadata)
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // Użyj jawnego tokenu @Inject, jeśli jest podany, w przeciwnym razie wywnioskuj typ
      const token = explicitInjections[index] || paramType;
      if (token === undefined) {
        throw new Error(`Nie można rozwiązać parametru pod indeksem ${index} dla ${target.name}. Może to być cykliczna zależność lub typ prymitywny bez jawnego @Inject.`);
      }
      return Container.resolve(token);
    });

    const instance = new target(...dependencies);
    Container.instances.set(target, instance);
    return instance;
  }
}

// Definicja usług
@Injectable()
class DatabaseService {
  connect() {
    console.log("Łączenie z bazą danych...");
    return "Połączenie z bazą danych";
  }
}

@Injectable()
class AuthService {
  private db: DatabaseService;

  constructor(db: DatabaseService) {
    this.db = db;
  }

  login() {
    console.log(`AuthService: Autoryzacja przy użyciu ${this.db.connect()}`);
    return "Użytkownik zalogowany";
  }
}

@Injectable()
class UserService {
  private authService: AuthService;
  private dbService: DatabaseService; // Przykład wstrzykiwania przez właściwość przy użyciu niestandardowego dekoratora lub funkcji frameworka

  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: Pobieranie profilu użytkownika...");
    return { id: 1, name: "Globalny Użytkownik" };
  }
}

// Rozwiąż główną usługę
console.log("--- Rozwiązywanie UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("
--- Rozwiązywanie AuthService (powinno być cache'owane) ---");
const authService = Container.resolve(AuthService);
authService.login();

Ten rozbudowany przykład pokazuje, jak dekoratory @Injectable i @Inject, w połączeniu z reflect-metadata, pozwalają niestandardowemu Container na automatyczne rozwiązywanie i dostarczanie zależności. Metadane design:paramtypes automatycznie emitowane przez TypeScript (gdy emitDecoratorMetadata jest włączone) są tutaj kluczowe.

2. Programowanie zorientowane na aspekty (AOP)

AOP koncentruje się na modularizacji aspektów przekrojowych (np. logowanie, bezpieczeństwo, transakcje), które przenikają wiele klas i modułów. Dekoratory doskonale nadają się do implementacji koncepcji AOP w TypeScript.

Przykład: Logowanie za pomocą dekoratora metody

Powracając do dekoratora LogCall, jest to doskonały przykład AOP. Dodaje on funkcjonalność logowania do dowolnej metody bez modyfikowania oryginalnego kodu metody. Oddziela to „co zrobić” (logika biznesowa) od „jak to zrobić” (logowanie, monitorowanie wydajności itp.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG AOP] Wejście do metody: ${String(propertyKey)} z argumentami:`, args);
    try {
      const result = originalMethod.apply(this, args);
      console.log(`[LOG AOP] Wyjście z metody: ${String(propertyKey)} z wynikiem:`, result);
      return result;
    } catch (error: any) {
      console.error(`[LOG AOP] Błąd w metodzie ${String(propertyKey)}:`, error.message);
      throw error;
    }
  };
  return descriptor;
}

class PaymentProcessor {
  @LogMethod
  processPayment(amount: number, currency: string) {
    if (amount <= 0) {
      throw new Error("Kwota płatności musi być dodatnia.");
    }
    console.log(`Przetwarzanie płatności ${amount} ${currency}...`);
    return `Płatność ${amount} ${currency} przetworzona pomyślnie.`;
  }

  @LogMethod
  refundPayment(transactionId: string) {
    console.log(`Zwrot płatności dla ID transakcji: ${transactionId}...`);
    return `Zwrot zainicjowany dla ${transactionId}.`;
  }
}

const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
  processor.processPayment(-50, "EUR");
} catch (error: any) {
  console.error("Złapany błąd:", error.message);
}

To podejście sprawia, że klasa PaymentProcessor skupia się wyłącznie na logice płatności, podczas gdy dekorator LogMethod obsługuje przekrojowy aspekt logowania.

3. Walidacja i transformacja

Dekoratory są niezwykle użyteczne do definiowania reguł walidacji bezpośrednio na właściwościach lub do transformacji danych podczas serializacji/deserializacji.

Przykład: Walidacja danych za pomocą dekoratorów właściwości

Przykład @Required z wcześniejszego przykładu już to zademonstrował. Oto kolejny przykład z walidacją zakresu numerycznego.

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)} musi być dodatnią liczbą.`);
}

function MaxLength(maxLength: number) {
  return function (target: Object, propertyKey: string | symbol) {
    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} musi mieć co najwyżej ${maxLength} znaków.`);
  };
}

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("Błędy produktu 1:", Product.validate(product1)); // []

const product2 = new Product("Bardzo długa nazwa produktu przekraczająca limit pięćdziesięciu znaków dla celów testowych", 50);
console.log("Błędy produktu 2:", Product.validate(product2)); // ["name musi mieć co najwyżej 50 znaków."]

const product3 = new Product("Książka", -10);
console.log("Błędy produktu 3:", Product.validate(product3)); // ["price musi być dodatnią liczbą."]

Ten zestaw pozwala deklaratywnie definiować reguły walidacji dla właściwości modeli, czyniąc Twoje modele danych samoopisującymi się pod względem ich ograniczeń.

Najlepsze praktyki i rozważania

Chociaż dekoratory są potężne, należy ich używać rozważnie. Niewłaściwe ich użycie może prowadzić do kodu, który jest trudniejszy do debugowania lub zrozumienia.

Kiedy używać dekoratorów (i kiedy nie)

Implikacje wydajnościowe

Dekoratory są wykonywane w czasie kompilacji (lub w czasie definiowania w środowisku wykonawczym JavaScript, jeśli zostały przetransformowane). Transformacja lub zbieranie metadanych odbywa się, gdy klasa/metoda jest definiowana, a nie przy każdym wywołaniu. Dlatego wpływ dekoratorów na wydajność w czasie wykonywania jest minimalny. Jednak logika wewnątrz dekoratorów może mieć wpływ na wydajność, zwłaszcza jeśli wykonują one kosztowne operacje przy każdym wywołaniu metody (np. złożone obliczenia w dekoratorze metody).

Utrzymanie i czytelność

Dekoratory, gdy są używane prawidłowo, mogą znacząco poprawić czytelność, przenosząc boilerplate poza główną logikę. Jednak jeśli wykonują złożone, ukryte transformacje, debugowanie może stać się trudne. Upewnij się, że Twoje dekoratory są dobrze udokumentowane, a ich zachowanie jest przewidywalne.

Eksperymentalny status i przyszłość dekoratorów

Ważne jest, aby powtórzyć, że dekoratory TypeScript opierają się na propozycji TC39 Stage 3. Oznacza to, że specyfikacja jest w dużej mierze stabilna, ale może jeszcze ulec niewielkim zmianom, zanim stanie się częścią oficjalnego standardu ECMAScript. Frameworki takie jak Angular przyjęły je, stawiając na ich ostateczną standaryzację. Oznacza to pewien poziom ryzyka, chociaż biorąc pod uwagę ich szerokie zastosowanie, znaczące łamiące zmiany są mało prawdopodobne.

Propozycja TC39 ewoluowała. Implementacja TypeScript jest oparta na starszej wersji propozycji. Istnieje rozróżnienie między „Dekoratorami dziedziczonymi” a „Standardowymi Dekoratorami”. Gdy pojawi się oficjalny standard, TypeScript prawdopodobnie zaktualizuje swoją implementację. Dla większości programistów korzystających z frameworków, ta transformacja zostanie zarządzana przez sam framework. Dla autorów bibliotek, zrozumienie subtelnych różnic między dekoratorami dziedziczonymi a przyszłymi standardowymi może stać się konieczne.

Opcja kompilatora emitDecoratorMetadata

Ta opcja, gdy ustawiona na true w tsconfig.json, instruuje kompilator TypeScript, aby emitował pewne metadane typów czasu projektowania do skompilowanego kodu JavaScript. Metadane te obejmują typ parametrów konstruktora (design:paramtypes), typ zwracany przez metody (design:returntype) i typ właściwości (design:type).

Te emitowane metadane nie są częścią standardowego środowiska wykonawczego JavaScript. Są one zazwyczaj konsumowane przez polifill reflect-metadata, który następnie udostępnia je za pomocą funkcji Reflect.getMetadata(). Jest to absolutnie kluczowe dla zaawansowanych wzorców, takich jak wstrzykiwanie zależności, gdzie kontener musi znać typy zależności, których wymaga klasa, bez jawnej konfiguracji.

Zaawansowane wzorce z dekoratorami

Dekoratory można łączyć i rozszerzać, aby budować jeszcze bardziej wyrafinowane wzorce.

1. Dekorowanie dekoratorów (dekoratory wyższego rzędu)

Możesz tworzyć dekoratory, które modyfikują lub komponują inne dekoratory. Jest to mniej powszechne, ale demonstruje funkcjonalną naturę dekoratorów.

// Dekorator, który zapewnia, że metoda jest logowana i wymaga również ról administratora
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // Zastosuj autoryzację najpierw (wewnętrznie)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // Następnie zastosuj LogCall (zewnętrznie)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // Zwróć zmodyfikowany deskryptor
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`Usuwanie konta użytkownika: ${userId}`);
    return `Użytkownik ${userId} usunięty.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Oczekiwane wyjście (zakładając rolę administratora):
[AUTH] Dostęp przyznany dla deleteUserAccount
[LOG] Wywołanie deleteUserAccount z argumentami: [ 'user007' ]
Usuwanie konta użytkownika: user007
[LOG] Metoda deleteUserAccount zwróciła: Użytkownik user007 usunięty.
*/

Tutaj AdminAndLoggedMethod jest fabryką, która zwraca dekorator, a wewnątrz tego dekoratora stosuje dwa inne dekoratory. Ten wzorzec może enkapsulować złożone kompozycje dekoratorów.

2. Używanie dekoratorów do mixinów

Chociaż TypeScript oferuje inne sposoby implementacji mixinów, dekoratory mogą być używane do deklaratywnego wstrzykiwania możliwości do klas.

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("Obiekt został zwolniony.");
  }
}

class Loggable {
  log(message: string) {
    console.log(`[Loggable] ${message}`);
  }
}

@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
  // Te właściwości/metody są wstrzykiwane przez dekorator
  isDisposed!: boolean;
  dispose!: () => void;
  log!: (message: string) => void;

  constructor(public name: string) {
    this.log(`Zasób ${this.name} utworzony.`);
  }

  cleanUp() {
    this.dispose();
    this.log(`Zasób ${this.name} został posprzątany.`);
  }
}

const resource = new MyResource("PołączenieSieciowe");
console.log(`Czy zwolniony: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Czy zwolniony: ${resource.isDisposed}`);

Ten dekorator @ApplyMixins dynamicznie kopiuje metody i właściwości z konstruktorów bazowych do prototypu klasy pochodnej, skutecznie "mieszając" funkcjonalności.

Wniosek: Wzmacnianie nowoczesnego rozwoju TypeScript

Dekoratory TypeScript to potężna i ekspresywna funkcja, która umożliwia nowy paradygmat programowania sterowanego metadanymi i zorientowanego na aspekty. Pozwalają one programistom wzbogacać, modyfikować i dodawać deklaratywne zachowania do klas, metod, właściwości, akcesorów i parametrów bez zmiany ich podstawowej logiki. To rozdzielenie odpowiedzialności prowadzi do czystszego, łatwiejszego w utrzymaniu i wysoce reużywalnego kodu.

Od upraszczania wstrzykiwania zależności i implementowania solidnych systemów walidacji, po dodawanie aspektów przekrojowych, takich jak logowanie i monitorowanie wydajności, dekoratory zapewniają eleganckie rozwiązanie wielu powszechnych wyzwań w tworzeniu oprogramowania. Chociaż ich eksperymentalny status uzasadnia świadomość, ich szerokie przyjęcie w głównych frameworkach oznacza ich praktyczną wartość i przyszłą trafność.

Opanowując dekoratory TypeScript, zyskujesz znaczące narzędzie w swoim arsenale, umożliwiające budowanie bardziej solidnych, skalowalnych i inteligentnych aplikacji. Przyjmij je odpowiedzialnie, zrozum ich mechanizmy i odblokuj nowy poziom deklaratywnej mocy w swoich projektach TypeScript.