Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Dekoratory metod i właściwości

"Nadzór nad poszczególnymi operacjami w Parku Jurajskim jest równie ważny, jak zarządzanie całym parkiem," wyjaśnia dr. Henry Wu, główny genetyk InGen. "Podobnie jak monitorujemy każdy aspekt życia dinozaurów - ich żywienie, zachowanie czy wzrost - tak samo musimy kontrolować i zabezpieczać poszczególne operacje w naszym systemie."

W TypeScripcie, po zrozumieniu dekoratorów klas, naturalnym krokiem jest zagłębienie się w dekoratory metod i właściwości. Te dekoratory pozwalają nam na precyzyjne modyfikowanie zachowania konkretnych członków klasy, podobnie jak w Parku Jurajskim, gdzie każda operacja, od karmienia dinozaurów po zabezpieczenia zagród, wymaga specjalnego nadzoru i protokołów.

Wprowadzenie do dekoratorów metod i właściwości

Dekoratory metod i właściwości pozwalają na modyfikowanie zachowania, dostępu lub wartości poszczególnych członków klasy. Dzięki nim możemy:

  1. Dodać dodatkowe zachowanie przed lub po wykonaniu metody
  2. Walidować parametry metod
  3. Cachować wyniki metod
  4. Mierzyć czas wykonania metod
  5. Kontrolować dostęp do właściwości
  6. Transformować wartości właściwości
  7. Dodawać metadane do metod i właściwości

Dekoratory metod

Dekoratory metod są funkcjami, które przyjmują trzy parametry:

  1. target
    - prototyp klasy (dla metod instancji) lub konstruktor klasy (dla metod statycznych)
  2. propertyKey
    - nazwa metody
  3. descriptor
    - deskryptor właściwości dla metody

Podstawowa składnia dekoratora metody:

1function dekoratorMetody(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
2  // Modyfikacja deskryptora metody
3  // Zwrócenie zmodyfikowanego deskryptora lub undefined
4}
5
6class MojaKlasa {
7  @dekoratorMetody
8  mojaMetoda() {
9    // Implementacja metody
10  }
11}

Przykład: System logowania operacji w Parku Jurajskim

Zacznijmy od prostego dekoratora metody, który będzie logował wykonanie operacji w Parku Jurajskim:

1// Dekorator metody logujący wywołania
2function LogujOperację(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
3  // Zapisujemy oryginalną metodę
4  const oryginalnaMetoda = descriptor.value;
5
6  // Nadpisujemy metodę
7  descriptor.value = function(...args: any[]) {
8    console.log(`[${new Date().toISOString()}] Wywołanie ${propertyKey} z argumentami: ${JSON.stringify(args)}`);
9
10    try {
11      // Wywołujemy oryginalną metodę
12      const wynik = oryginalnaMetoda.apply(this, args);
13
14      // Logujemy wynik
15      console.log(`[${new Date().toISOString()}] ${propertyKey} zakończone. Wynik: ${JSON.stringify(wynik)}`);
16
17      return wynik;
18    } catch (błąd) {
19      // Logujemy błąd
20      console.error(`[${new Date().toISOString()}] ${propertyKey} zakończone błędem: ${błąd}`);
21
22      // Rzucamy błąd dalej
23      throw błąd;
24    }
25  };
26
27  return descriptor;
28}
29
30// Zastosowanie dekoratora w klasie zarządzającej zagrodami
31class ZarządzanieZagrodami {
32  constructor(private nazwaPracownika: string) {}
33
34  @LogujOperację
35  otwórzZagrodę(idZagrody: string): boolean {
36    console.log(`${this.nazwaPracownika} otwiera zagrodę: ${idZagrody}`);
37    // Tutaj logika otwierania zagrody
38    return true;
39  }
40
41  @LogujOperację
42  zamknijZagrodę(idZagrody: string): boolean {
43    console.log(`${this.nazwaPracownika} zamyka zagrodę: ${idZagrody}`);
44    // Tutaj logika zamykania zagrody
45    return true;
46  }
47
48  sprawdźStan(idZagrody: string): string {
49    // Ta metoda nie jest logowana
50    return "Zagroda zamknięta";
51  }
52}
53
54// Używamy klasy z dekorowanymi metodami
55const zarządca = new ZarządzanieZagrodami("John");
56zarządca.otwórzZagrodę("A-001");
57zarządca.zamknijZagrodę("A-001");
58zarządca.sprawdźStan("A-001"); // Ta operacja nie będzie logowana

W powyższym przykładzie, dekorator

LogujOperację
rejestruje każde wywołanie metody, jej argumenty oraz wynik lub błąd. Metody
otwórzZagrodę
i
zamknijZagrodę
są logowane, podczas gdy metoda
sprawdźStan
nie ma dekoratora, więc nie jest logowana.

Dekoratory metod z parametrami

Możemy również tworzyć dekoratory metod, które przyjmują parametry:

1// Dekorator metody z parametrami
2function WymagaPoziomDostępu(poziom: number) {
3  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
4    // Zapisujemy oryginalną metodę
5    const oryginalnaMetoda = descriptor.value;
6
7    // Nadpisujemy metodę
8    descriptor.value = function(...args: any[]) {
9      // Sprawdzamy, czy obiekt ma właściwość poziomDostępu
10      if (!(this as any).poziomDostępu) {
11        throw new Error("Obiekt nie posiada zdefiniowanego poziomu dostępu!");
12      }
13
14      // Weryfikujemy poziom dostępu
15      if ((this as any).poziomDostępu < poziom) {
16        throw new Error(`Wymagany poziom dostępu: ${poziom}, dostępny: ${(this as any).poziomDostępu}`);
17      }
18
19      // Wywołujemy oryginalną metodę
20      return oryginalnaMetoda.apply(this, args);
21    };
22
23    return descriptor;
24  };
25}
26
27// Klasa reprezentująca laboratorium
28class LaboratoriumGenetyczne {
29  constructor(public nazwaPracownika: string, public poziomDostępu: number) {}
30
31  @WymagaPoziomDostępu(1)
32  odczytajDaneDNA(idDinozaura: string): string {
33    return `DNA dinozaura ${idDinozaura}: ACGT...`;
34  }
35
36  @WymagaPoziomDostępu(2)
37  modyfikujDNA(idDinozaura: string, sekwencja: string): boolean {
38    console.log(`Modyfikacja DNA dinozaura ${idDinozaura}: ${sekwencja}`);
39    return true;
40  }
41
42  @WymagaPoziomDostępu(3)
43  stwórzNowyGatunek(nazwa: string, bazoweDNA: string): string {
44    const nowyGatunekId = `NGT-${Math.floor(Math.random() * 1000)}`;
45    console.log(`Tworzenie nowego gatunku: ${nazwa} (ID: ${nowyGatunekId})`);
46    return nowyGatunekId;
47  }
48}
49
50// Używamy klasy z różnymi poziomami dostępu
51const stażysta = new LaboratoriumGenetyczne("Tim", 1);
52const naukowiec = new LaboratoriumGenetyczne("Dr. Grant", 2);
53const dyrektor = new LaboratoriumGenetyczne("Dr. Wu", 3);
54
55// Testujemy dostęp
56try {
57  console.log(stażysta.odczytajDaneDNA("D-001")); // Zadziała
58  stażysta.modyfikujDNA("D-001", "ACGT");         // Wyrzuci błąd
59} catch (błąd) {
60  console.error(błąd.message);
61}
62
63try {
64  naukowiec.modyfikujDNA("D-001", "ACGT");        // Zadziała
65  naukowiec.stwórzNowyGatunek("Neo-Raptor", "ACGT..."); // Wyrzuci błąd
66} catch (błąd) {
67  console.error(błąd.message);
68}
69
70// Dyrektor ma pełny dostęp
71dyrektor.stwórzNowyGatunek("Indominus Rex", "ACGT...");

W tym przykładzie, dekorator

WymagaPoziomDostępu
sprawdza, czy osoba wywołująca metodę ma wystarczający poziom dostępu. Różne metody wymagają różnych poziomów dostępu, co odzwierciedla rzeczywiste protokoły bezpieczeństwa, jakie mogłyby istnieć w Parku Jurajskim.

Dekoratory właściwości

Dekoratory właściwości są podobne do dekoratorów metod, ale otrzymują tylko dwa parametry:

  1. target
    - prototyp klasy (dla właściwości instancji) lub konstruktor klasy (dla właściwości statycznych)
  2. propertyKey
    - nazwa właściwości

Podstawowa składnia dekoratora właściwości:

1function dekoratorWłaściwości(target: any, propertyKey: string) {
2  // Implementacja dekoratora
3}
4
5class MojaKlasa {
6  @dekoratorWłaściwości
7  mojaWłaściwość: string;
8}

Ponieważ dekoratory właściwości nie otrzymują deskryptora, często są używane do generowania getterów i setterów lub do stosowania metadanych.

Przykład: System monitorowania zasobów w Parku Jurajskim

Zaimplementujmy dekorator właściwości, który będzie monitorował zmiany zasobów w Parku Jurajskim:

1// Dekorator właściwości monitorujący zmiany
2function MonitorujZmiany(target: any, propertyKey: string) {
3  // Tworzymy unikalną nazwę dla prywatnej właściwości
4  const prywatnaNazwa = Symbol(propertyKey);
5
6  // Definiujemy getter i setter
7  Object.defineProperty(target, propertyKey, {
8    get: function() {
9      // Zwracamy wartość prywatnej właściwości
10      return this[prywatnaNazwa];
11    },
12    set: function(wartość: any) {
13      // Zapisujemy starą wartość
14      const staraWartość = this[prywatnaNazwa];
15
16      // Logujemy zmianę, jeśli nie jest to pierwsze przypisanie
17      if (this[prywatnaNazwa] !== undefined) {
18        console.log(`[ZMIANA][${this.constructor.name}] ${propertyKey}: ${staraWartość} -> ${wartość}`);
19      }
20
21      // Zapisujemy nową wartość
22      this[prywatnaNazwa] = wartość;
23    },
24    enumerable: true,
25    configurable: true
26  });
27}
28
29// Klasa monitorująca zasoby parku
30class ZasobyParku {
31  @MonitorujZmiany
32  ilośćPożywienia: number = 1000;
33
34  @MonitorujZmiany
35  stanGeneratorów: "Aktywne" | "Awaryjne" | "Wyłączone" = "Aktywne";
36
37  @MonitorujZmiany
38  statusBramy: "Otwarta" | "Zamknięta" = "Zamknięta";
39
40  constructor(public nazwaSekcji: string) {}
41
42  użyjPożywienia(ilość: number): boolean {
43    if (this.ilośćPożywienia >= ilość) {
44      this.ilośćPożywienia -= ilość;
45      return true;
46    }
47    return false;
48  }
49
50  uzupełnijPożywienie(ilość: number): void {
51    this.ilośćPożywienia += ilość;
52  }
53
54  przełączGeneratory(stan: "Aktywne" | "Awaryjne" | "Wyłączone"): void {
55    this.stanGeneratorów = stan;
56  }
57
58  przełączBramę(status: "Otwarta" | "Zamknięta"): void {
59    this.statusBramy = status;
60  }
61}
62
63// Używamy klasy z monitorowanymi właściwościami
64const sekcjaA = new ZasobyParku("Sekcja A");
65
66// Testujemy zmiany właściwości
67sekcjaA.użyjPożywienia(200);  // ilośćPożywienia: 1000 -> 800
68sekcjaA.uzupełnijPożywienie(500);  // ilośćPożywienia: 800 -> 1300
69sekcjaA.przełączGeneratory("Awaryjne");  // stanGeneratorów: Aktywne -> Awaryjne
70sekcjaA.przełączBramę("Otwarta");  // statusBramy: Zamknięta -> Otwarta

W tym przykładzie, dekorator

MonitorujZmiany
tworzy getter i setter dla właściwości, które logują każdą zmianę wartości. Dzięki temu, możemy śledzić, kiedy i jakie zmiany zostały wprowadzone, co jest kluczowe dla monitorowania zasobów w parku.

Łączenie dekoratorów metod i właściwości

Możemy łączyć różne dekoratory metod i właściwości, aby stworzyć bardziej zaawansowane zachowania:

1// Dekorator mierzący czas wykonania metody
2function MierzCzasWykonania(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
3  const oryginalnaMetoda = descriptor.value;
4
5  descriptor.value = function(...args: any[]) {
6    const start = performance.now();
7    const wynik = oryginalnaMetoda.apply(this, args);
8    const koniec = performance.now();
9
10    console.log(`[CZAS] ${propertyKey}: ${(koniec - start).toFixed(2)}ms`);
11
12    return wynik;
13  };
14
15  return descriptor;
16}
17
18// Dekorator walidujący parametry metody
19function WalidujParametry(walidator: (args: any[]) => boolean, komunikatBłędu: string) {
20  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
21    const oryginalnaMetoda = descriptor.value;
22
23    descriptor.value = function(...args: any[]) {
24      if (!walidator(args)) {
25        throw new Error(`[BŁĄD WALIDACJI] ${propertyKey}: ${komunikatBłędu}`);
26      }
27
28      return oryginalnaMetoda.apply(this, args);
29    };
30
31    return descriptor;
32  };
33}
34
35// Dekorator cachujący wyniki metody
36function Cachuj(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
37  const oryginalnaMetoda = descriptor.value;
38  const cache = new Map<string, any>();
39
40  descriptor.value = function(...args: any[]) {
41    const klucz = JSON.stringify(args);
42
43    if (cache.has(klucz)) {
44      console.log(`[CACHE] ${propertyKey}: Użyto zapisanego wyniku dla ${klucz}`);
45      return cache.get(klucz);
46    }
47
48    const wynik = oryginalnaMetoda.apply(this, args);
49    cache.set(klucz, wynik);
50
51    return wynik;
52  };
53
54  return descriptor;
55}
56
57// Dekorator dodający zakres wartości dla właściwości numerycznej
58function Zakres(min: number, max: number) {
59  return function(target: any, propertyKey: string) {
60    // Tworzymy unikalną nazwę dla prywatnej właściwości
61    const prywatnaNazwa = Symbol(propertyKey);
62
63    // Definiujemy getter i setter
64    Object.defineProperty(target, propertyKey, {
65      get: function() {
66        return this[prywatnaNazwa];
67      },
68      set: function(wartość: number) {
69        if (wartość < min) {
70          console.warn(`[ZAKRES] ${propertyKey}: Wartość ${wartość} poniżej minimum ${min}. Ustawiam ${min}.`);
71          this[prywatnaNazwa] = min;
72        } else if (wartość > max) {
73          console.warn(`[ZAKRES] ${propertyKey}: Wartość ${wartość} powyżej maximum ${max}. Ustawiam ${max}.`);
74          this[prywatnaNazwa] = max;
75        } else {
76          this[prywatnaNazwa] = wartość;
77        }
78      },
79      enumerable: true,
80      configurable: true
81    });
82  };
83}
84
85// Klasa zarządzająca systemem ogrodzenia elektrycznego
86class SystemOgrodzeniaElektrycznego {
87  @Zakres(0, 10000)
88  napięcie: number = 5000; // w woltach
89
90  @Zakres(0, 100)
91  zasięg: number = 50; // w metrach
92
93  private statusAktywny: boolean = false;
94
95  @LogujOperację
96  @MierzCzasWykonania
97  @WalidujParametry(
98    args => args[0] >= 0 && args[0] <= 10000,
99    "Nieprawidłowe napięcie. Dozwolony zakres: 0-10000V"
100  )
101  ustawNapięcie(noweNapięcie: number): boolean {
102    // Symulacja długiej operacji dla testowania dekoratora MierzCzasWykonania
103    for (let i = 0; i < 1000000; i++) {
104      // Obciążamy procesor
105    }
106
107    this.napięcie = noweNapięcie;
108    return true;
109  }
110
111  @LogujOperację
112  @Cachuj
113  pobierzStatusSystemu(idSekcji: string): { aktywny: boolean; napięcie: number; zasięg: number } {
114    console.log(`Pobieranie statusu systemu dla sekcji ${idSekcji}...`);
115
116    // Symulacja długiej operacji
117    for (let i = 0; i < 500000; i++) {
118      // Obciążamy procesor
119    }
120
121    return {
122      aktywny: this.statusAktywny,
123      napięcie: this.napięcie,
124      zasięg: this.zasięg
125    };
126  }
127
128  @LogujOperację
129  @MierzCzasWykonania
130  aktywujSystem(): boolean {
131    if (this.statusAktywny) {
132      console.log("System już jest aktywny");
133      return false;
134    }
135
136    console.log(`Aktywacja systemu ogrodzenia elektrycznego. Napięcie: ${this.napięcie}V, Zasięg: ${this.zasięg}m`);
137    this.statusAktywny = true;
138
139    return true;
140  }
141
142  @LogujOperację
143  @MierzCzasWykonania
144  deaktywujSystem(): boolean {
145    if (!this.statusAktywny) {
146      console.log("System już jest nieaktywny");
147      return false;
148    }
149
150    console.log("Deaktywacja systemu ogrodzenia elektrycznego");
151    this.statusAktywny = false;
152
153    return true;
154  }
155}
156
157// Używamy klasy z wieloma dekoratorami
158const systemOgrodzenia = new SystemOgrodzeniaElektrycznego();
159
160// Testujemy różne funkcjonalności
161systemOgrodzenia.ustawNapięcie(7500);  // Loguje operację, mierzy czas, waliduje parametr
162systemOgrodzenia.ustawNapięcie(12000); // Wyjdzie poza zakres, zostanie ustawione 10000
163systemOgrodzenia.aktywujSystem();      // Loguje operację, mierzy czas
164
165// Testujemy cachowanie
166console.log(systemOgrodzenia.pobierzStatusSystemu("A1")); // Wykona pełną operację
167console.log(systemOgrodzenia.pobierzStatusSystemu("A1")); // Użyje cache
168console.log(systemOgrodzenia.pobierzStatusSystemu("B2")); // Wykona pełną operację
169console.log(systemOgrodzenia.pobierzStatusSystemu("B2")); // Użyje cache
170
171// Deaktywujemy system
172systemOgrodzenia.deaktywujSystem();

W tym przykładzie używamy wielu dekoratorów do stworzenia zaawansowanego systemu ogrodzenia elektrycznego:

  1. @Zakres
    - zapewnia, że wartości właściwości pozostają w określonym zakresie
  2. @LogujOperację
    - loguje wywołania metod
  3. @MierzCzasWykonania
    - mierzy czas wykonania metod
  4. @WalidujParametry
    - sprawdza poprawność parametrów metod
  5. @Cachuj
    - zapisuje wyniki metod w pamięci podręcznej

Metadane i refleksja z dekoratorami

TypeScript umożliwia również używanie metadanych z dekoratorami, co pozwala na bardziej zaawansowane przypadki użycia. Wymaga to włączenia opcji

emitDecoratorMetadata
i
experimentalDecorators
w pliku
tsconfig.json
oraz zainstalowania pakietu
reflect-metadata
.

Oto przykład użycia metadanych z dekoratorami:

1// Potrzebujemy importować reflect-metadata na początku aplikacji
2import "reflect-metadata";
3
4// Klucze dla metadanych
5const PARAMETRY_KLUCZ = Symbol("parametry");
6
7// Dekorator parametru
8function Parametr(target: any, propertyKey: string, parameterIndex: number) {
9  // Pobierz istniejące metadane lub stwórz puste
10  const parametry: number[] = Reflect.getOwnMetadata(PARAMETRY_KLUCZ, target, propertyKey) || [];
11
12  // Dodaj indeks parametru do listy
13  parametry.push(parameterIndex);
14
15  // Zapisz metadane
16  Reflect.defineMetadata(PARAMETRY_KLUCZ, parametry, target, propertyKey);
17}
18
19// Dekorator metody walidujący parametry
20function WalidujDane(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
21  const oryginalnaMetoda = descriptor.value;
22
23  descriptor.value = function(...args: any[]) {
24    // Pobierz typy parametrów (wymaga opcji emitDecoratorMetadata)
25    const typy = Reflect.getMetadata("design:paramtypes", target, propertyKey);
26
27    // Pobierz indeksy parametrów oznaczonych dekoratorem @Parametr
28    const oznaczenieParametry: number[] = Reflect.getOwnMetadata(PARAMETRY_KLUCZ, target, propertyKey) || [];
29
30    // Waliduj parametry
31    for (const indeks of oznaczenieParametry) {
32      const wartość = args[indeks];
33      const typ = typy[indeks];
34
35      // Walidacja typu
36      if (wartość === null || wartość === undefined) {
37        throw new Error(`Parametr #${indeks} metody ${propertyKey} nie może być null lub undefined`);
38      }
39
40      // Walidacja dla różnych typów
41      if (typ === String && typeof wartość !== "string") {
42        throw new Error(`Parametr #${indeks} metody ${propertyKey} musi być stringiem`);
43      } else if (typ === Number && typeof wartość !== "number") {
44        throw new Error(`Parametr #${indeks} metody ${propertyKey} musi być liczbą`);
45      } else if (typ === Boolean && typeof wartość !== "boolean") {
46        throw new Error(`Parametr #${indeks} metody ${propertyKey} musi być wartością logiczną`);
47      } else if (typ === Date && !(wartość instanceof Date)) {
48        throw new Error(`Parametr #${indeks} metody ${propertyKey} musi być datą`);
49      }
50    }
51
52    // Wywołaj oryginalną metodę
53    return oryginalnaMetoda.apply(this, args);
54  };
55
56  return descriptor;
57}
58
59// Klasa operacji na dinozaurach z metadanymi
60class OperacjeDinozaurów {
61  @WalidujDane
62  przeprowadźBadanieZdrowia(
63    idDinozaura: string,
64    @Parametr dataBadania: Date,
65    @Parametr temperatura: number,
66    @Parametr waga: number,
67    notatki?: string
68  ): string {
69    console.log(`Badanie dinozaura ${idDinozaura}`);
70    console.log(`Data: ${dataBadania.toLocaleDateString()}`);
71    console.log(`Temperatura: ${temperatura}°C`);
72    console.log(`Waga: ${waga}kg`);
73
74    if (notatki) {
75      console.log(`Notatki: ${notatki}`);
76    }
77
78    return "Badanie zakończone";
79  }
80
81  @WalidujDane
82  przeprowadźSzczepienie(
83    @Parametr idDinozaura: string,
84    @Parametr typSzczepienia: string,
85    @Parametr dataSzczepienia: Date
86  ): boolean {
87    console.log(`Szczepienie dinozaura ${idDinozaura}`);
88    console.log(`Typ szczepienia: ${typSzczepienia}`);
89    console.log(`Data: ${dataSzczepienia.toLocaleDateString()}`);
90
91    return true;
92  }
93}
94
95// Używamy klasy z walidacją parametrów
96const operacje = new OperacjeDinozaurów();
97
98try {
99  // Poprawne wywołanie
100  operacje.przeprowadźBadanieZdrowia(
101    "D-001",
102    new Date(),
103    38.5,
104    1200,
105    "Dinozaur w dobrym stanie"
106  );
107
108  // Niepoprawne wywołanie - temperatura nie jest liczbą
109  operacje.przeprowadźBadanieZdrowia(
110    "D-002",
111    new Date(),
112    "wysoka" as any, // To spowoduje błąd
113    1500
114  );
115} catch (błąd) {
116  console.error(błąd.message);
117}
118
119try {
120  // Poprawne wywołanie
121  operacje.przeprowadźSzczepienie(
122    "D-003",
123    "Szczepionka przeciw pasożytom",
124    new Date()
125  );
126
127  // Niepoprawne wywołanie - data nie jest obiektem Date
128  operacje.przeprowadźSzczepienie(
129    "D-004",
130    "Szczepionka przeciw grypie",
131    "dzisiaj" as any // To spowoduje błąd
132  );
133} catch (błąd) {
134  console.error(błąd.message);
135}

W tym przykładzie używamy metadanych i refleksji do walidacji parametrów metod. Dekorator

@Parametr
oznacza parametry, które mają być walidowane, a dekorator
@WalidujDane
sprawdza typy tych parametrów przy każdym wywołaniu metody.

Praktyczny przykład: System zarządzania badaniami dinozaurów

Teraz połączmy wszystkie omówione techniki w jednym większym przykładzie - systemie zarządzania badaniami dinozaurów:

1// Dekorator właściwości zapewniający unikalność ID
2function UnikalneID(prefiks: string = "ID") {
3  return function(target: any, propertyKey: string) {
4    const generujID = () => `${prefiks}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
5
6    Object.defineProperty(target, propertyKey, {
7      get: function() {
8        // Jeśli właściwość nie została jeszcze ustawiona, generujemy ID
9        if (!this[`_${propertyKey}`]) {
10          this[`_${propertyKey}`] = generujID();
11        }
12        return this[`_${propertyKey}`];
13      },
14      set: function(wartość: string) {
15        // Pozwalamy na ustawienie tylko raz
16        if (!this[`_${propertyKey}`]) {
17          this[`_${propertyKey}`] = wartość;
18        } else {
19          console.warn(`Właściwość ${propertyKey} już została ustawiona i nie może być zmieniona.`);
20        }
21      },
22      enumerable: true,
23      configurable: true
24    });
25  };
26}
27
28// Dekorator metody do walidacji daty
29function WalidujDataBadania(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
30  const oryginalnaMetoda = descriptor.value;
31
32  descriptor.value = function(...args: any[]) {
33    // Zakładamy, że pierwszy argument to data badania
34    const dataBadania = args[0];
35
36    if (!(dataBadania instanceof Date)) {
37      throw new Error("Pierwszy parametr musi być instancją Date");
38    }
39
40    const teraz = new Date();
41
42    if (dataBadania > teraz) {
43      throw new Error("Data badania nie może być z przyszłości");
44    }
45
46    // Sprawdzamy, czy data nie jest starsza niż 30 dni
47    const trzydzieściDniWstecz = new Date();
48    trzydzieściDniWstecz.setDate(trzydzieściDniWstecz.getDate() - 30);
49
50    if (dataBadania < trzydzieściDniWstecz) {
51      console.warn("Uwaga: Data badania jest starsza niż 30 dni");
52    }
53
54    return oryginalnaMetoda.apply(this, args);
55  };
56
57  return descriptor;
58}
59
60// Dekorator metody do kontroli częstotliwości badań
61function KontrolujCzęstotliwość(minCzasMiędzySzczepieniamiDni: number) {
62  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
63    // Mapa przechowująca daty ostatnich szczepień dla każdego dinozaura
64    const ostatnieSzczepienia = new Map<string, Date>();
65
66    const oryginalnaMetoda = descriptor.value;
67
68    descriptor.value = function(...args: any[]) {
69      // Zakładamy, że pierwszy argument to ID dinozaura, a drugi to data szczepienia
70      const idDinozaura = args[0];
71      const dataSzczepienia = args[1];
72
73      if (ostatnieSzczepienia.has(idDinozaura)) {
74        const ostatniaSzczepionka = ostatnieSzczepienia.get(idDinozaura)!;
75        const różnicaDni = Math.floor((dataSzczepienia.getTime() - ostatniaSzczepionka.getTime()) / (1000 * 60 * 60 * 24));
76
77        if (różnicaDni < minCzasMiędzySzczepieniamiDni) {
78          throw new Error(`Za wcześnie na kolejne szczepienie. Minimalny odstęp to ${minCzasMiędzySzczepieniamiDni} dni. Pozostało ${minCzasMiędzySzczepieniamiDni - różnicaDni} dni.`);
79        }
80      }
81
82      // Zapisujemy datę szczepienia
83      ostatnieSzczepienia.set(idDinozaura, dataSzczepienia);
84
85      return oryginalnaMetoda.apply(this, args);
86    };
87
88    return descriptor;
89  };
90}
91
92// Interfejs wyników badania
93interface WynikiBadania {
94  temperatura: number;  // w stopniach Celsjusza
95  waga: number;         // w kilogramach
96  długość: number;      // w metrach
97  stanZdrowia: "Zdrowy" | "Lekkie objawy" | "Chory" | "Krytyczny";
98  uwagi?: string;
99}
100
101// Klasa badania dinozaura
102class BadanieDinozaura {
103  @UnikalneID("BAD")
104  idBadania: string;
105
106  constructor(
107    public idDinozaura: string,
108    public dataBadania: Date,
109    public wyniki: WynikiBadania,
110    public przeprowadzającyBadanie: string
111  ) {}
112
113  @LogujOperację
114  pobierzRaport(): string {
115    return `
116      === RAPORT BADANIA ===
117      ID Badania: ${this.idBadania}
118      ID Dinozaura: ${this.idDinozaura}
119      Data: ${this.dataBadania.toLocaleString()}
120      Przeprowadzający: ${this.przeprowadzającyBadanie}
121
122      WYNIKI:
123      - Temperatura: ${this.wyniki.temperatura}°C
124      - Waga: ${this.wyniki.waga} kg
125      - Długość: ${this.wyniki.długość} m
126      - Stan zdrowia: ${this.wyniki.stanZdrowia}
127      ${this.wyniki.uwagi ? `- Uwagi: ${this.wyniki.uwagi}` : ""}
128    `;
129  }
130}
131
132// Klasa szczepienia dinozaura
133class SzczepienieDinozaura {
134  @UnikalneID("SZCZ")
135  idSzczepienia: string;
136
137  constructor(
138    public idDinozaura: string,
139    public dataSzczepienia: Date,
140    public rodzajSzczepionki: string,
141    public dawka: number,
142    public przeprowadzającySzczepienie: string
143  ) {}
144
145  @LogujOperację
146  pobierzRaport(): string {
147    return `
148      === RAPORT SZCZEPIENIA ===
149      ID Szczepienia: ${this.idSzczepienia}
150      ID Dinozaura: ${this.idDinozaura}
151      Data: ${this.dataSzczepienia.toLocaleString()}
152      Rodzaj szczepionki: ${this.rodzajSzczepionki}
153      Dawka: ${this.dawka} ml
154      Przeprowadzający: ${this.przeprowadzającySzczepienie}
155    `;
156  }
157}
158
159// Rejestr badań i szczepień
160class RejestrMedyczny {
161  private badania: BadanieDinozaura[] = [];
162  private szczepienia: SzczepienieDinozaura[] = [];
163
164  @LogujOperację
165  @WalidujDataBadania
166  zarejestrujBadanie(
167    dataBadania: Date,
168    idDinozaura: string,
169    temperatura: number,
170    waga: number,
171    długość: number,
172    stanZdrowia: "Zdrowy" | "Lekkie objawy" | "Chory" | "Krytyczny",
173    uwagi?: string,
174    przeprowadzającyBadanie: string = "Dr. Harding"
175  ): BadanieDinozaura {
176    const wyniki: WynikiBadania = {
177      temperatura,
178      waga,
179      długość,
180      stanZdrowia,
181      uwagi
182    };
183
184    const badanie = new BadanieDinozaura(idDinozaura, dataBadania, wyniki, przeprowadzającyBadanie);
185    this.badania.push(badanie);
186
187    console.log(`Zarejestrowano badanie: ${badanie.idBadania} dla dinozaura ${idDinozaura}`);
188
189    return badanie;
190  }
191
192  @LogujOperację
193  @KontrolujCzęstotliwość(90) // Minimum 90 dni między szczepieniami
194  zarejestrujSzczepienie(
195    idDinozaura: string,
196    dataSzczepienia: Date,
197    rodzajSzczepionki: string,
198    dawka: number,
199    przeprowadzającySzczepienie: string = "Dr. Harding"
200  ): SzczepienieDinozaura {
201    const szczepienie = new SzczepienieDinozaura(
202      idDinozaura,
203      dataSzczepienia,
204      rodzajSzczepionki,
205      dawka,
206      przeprowadzającySzczepienie
207    );
208
209    this.szczepienia.push(szczepienie);
210
211    console.log(`Zarejestrowano szczepienie: ${szczepienie.idSzczepienia} dla dinozaura ${idDinozaura}`);
212
213    return szczepienie;
214  }
215
216  @Cachuj
217  @LogujOperację
218  pobierzHistorięBadań(idDinozaura: string): BadanieDinozaura[] {
219    console.log(`Pobieranie historii badań dla dinozaura ${idDinozaura}...`);
220
221    // Symulacja długiej operacji
222    for (let i = 0; i < 500000; i++) {
223      // Obciążamy procesor
224    }
225
226    return this.badania.filter(badanie => badanie.idDinozaura === idDinozaura)
227      .sort((a, b) => b.dataBadania.getTime() - a.dataBadania.getTime());
228  }
229
230  @Cachuj
231  @LogujOperację
232  pobierzHistorięSzczepień(idDinozaura: string): SzczepienieDinozaura[] {
233    console.log(`Pobieranie historii szczepień dla dinozaura ${idDinozaura}...`);
234
235    // Symulacja długiej operacji
236    for (let i = 0; i < 500000; i++) {
237      // Obciążamy procesor
238    }
239
240    return this.szczepienia.filter(szczepienie => szczepienie.idDinozaura === idDinozaura)
241      .sort((a, b) => b.dataSzczepienia.getTime() - a.dataSzczepienia.getTime());
242  }
243
244  @MierzCzasWykonania
245  @LogujOperację
246  generujRaportMedyczny(idDinozaura: string): string {
247    const badania = this.pobierzHistorięBadań(idDinozaura);
248    const szczepienia = this.pobierzHistorięSzczepień(idDinozaura);
249
250    let raport = `=== RAPORT MEDYCZNY DINOZAURA ${idDinozaura} ===
251
252`;
253
254    raport += `Liczba badań: ${badania.length}
255`;
256    raport += `Liczba szczepień: ${szczepienia.length}
257
258`;
259
260    if (badania.length > 0) {
261      const ostatnieBadanie = badania[0];
262      raport += `OSTATNIE BADANIE:
263`;
264      raport += `Data: ${ostatnieBadanie.dataBadania.toLocaleString()}
265`;
266      raport += `Stan zdrowia: ${ostatnieBadanie.wyniki.stanZdrowia}
267`;
268      raport += `Temperatura: ${ostatnieBadanie.wyniki.temperatura}°C
269`;
270      raport += `Waga: ${ostatnieBadanie.wyniki.waga} kg
271`;
272      raport += `Długość: ${ostatnieBadanie.wyniki.długość} m
273`;
274      if (ostatnieBadanie.wyniki.uwagi) {
275        raport += `Uwagi: ${ostatnieBadanie.wyniki.uwagi}
276`;
277      }
278      raport += "
279";
280    }
281
282    if (szczepienia.length > 0) {
283      const ostatnieSzczepienie = szczepienia[0];
284      raport += `OSTATNIE SZCZEPIENIE:
285`;
286      raport += `Data: ${ostatnieSzczepienie.dataSzczepienia.toLocaleString()}
287`;
288      raport += `Rodzaj szczepionki: ${ostatnieSzczepienie.rodzajSzczepionki}
289`;
290      raport += `Dawka: ${ostatnieSzczepienie.dawka} ml
291
292`;
293    }
294
295    raport += `HISTORIA BADAŃ:
296`;
297    badania.forEach((badanie, indeks) => {
298      raport += `${indeks + 1}. ${badanie.dataBadania.toLocaleDateString()} - ${badanie.wyniki.stanZdrowia}
299`;
300    });
301
302    raport += `
303HISTORIA SZCZEPIEŃ:
304`;
305    szczepienia.forEach((szczepienie, indeks) => {
306      raport += `${indeks + 1}. ${szczepienie.dataSzczepienia.toLocaleDateString()} - ${szczepienie.rodzajSzczepionki}
307`;
308    });
309
310    return raport;
311  }
312}
313
314// Testujemy system
315const rejestrMedyczny = new RejestrMedyczny();
316
317// Rejestrujemy badania i szczepienia
318try {
319  // Badanie z przeszłości - OK
320  const badanie1 = rejestrMedyczny.zarejestrujBadanie(
321    new Date("2023-09-01"),
322    "TREX-001",
323    38.2,
324    7500,
325    12.5,
326    "Zdrowy",
327    "Badanie rutynowe",
328    "Dr. Harding"
329  );
330
331  // Badanie z przyszłości - Błąd
332  const badanie2 = rejestrMedyczny.zarejestrujBadanie(
333    new Date("2025-01-01"), // przyszłość
334    "TREX-001",
335    39.1,
336    7450,
337    12.5,
338    "Lekkie objawy",
339    "Lekka gorączka"
340  );
341} catch (błąd) {
342  console.error(błąd.message);
343}
344
345// Rejestrujemy szczepienia
346try {
347  // Pierwsze szczepienie
348  const szczepienie1 = rejestrMedyczny.zarejestrujSzczepienie(
349    "TREX-001",
350    new Date("2023-01-15"),
351    "Szczepionka przeciw grypie dinozaurów",
352    50,
353    "Dr. Sattler"
354  );
355
356  // Drugie szczepienie - za wcześnie, powinien być błąd
357  const szczepienie2 = rejestrMedyczny.zarejestrujSzczepienie(
358    "TREX-001",
359    new Date("2023-03-15"), // Tylko 60 dni później, a minimum to 90 dni
360    "Szczepionka przeciw pasożytom",
361    30
362  );
363} catch (błąd) {
364  console.error(błąd.message);
365}
366
367// Testujemy pobieranie danych
368console.log(rejestrMedyczny.pobierzHistorięBadań("TREX-001")); // Wykonuje pełne pobieranie
369console.log(rejestrMedyczny.pobierzHistorięBadań("TREX-001")); // Użyje cache
370
371// Generujemy raport medyczny
372console.log(rejestrMedyczny.generujRaportMedyczny("TREX-001"));

W tym rozbudowanym przykładzie stworzyliśmy kompleksowy system zarządzania badaniami i szczepieniami dinozaurów w Parku Jurajskim, wykorzystując różne dekoratory:

  1. @UnikalneID
    - zapewnia, że każde badanie i każde szczepienie ma unikalne ID
  2. @LogujOperację
    - loguje wywołania metod
  3. @WalidujDataBadania
    - sprawdza poprawność daty badania
  4. @KontrolujCzęstotliwość
    - zapewnia odpowiedni odstęp między szczepieniami
  5. @Cachuj
    - cachuje wyniki długotrwałych operacji
  6. @MierzCzasWykonania
    - mierzy czas generowania raportów

System ten pokazuje, jak dekoratory metod i właściwości mogą być wykorzystane do tworzenia zaawansowanych funkcjonalności biznesowych, zapewniając jednocześnie spójność kodu, bezpieczeństwo danych i wydajność.

Kiedy używać dekoratorów metod i właściwości

Dekoratory metod i właściwości są szczególnie przydatne:

  1. Gdy chcemy dodać aspektową funkcjonalność - logowanie, pomiar wydajności, walidacja
  2. Gdy chcemy cachować wyniki - dla kosztownych obliczeniowo metod
  3. Gdy potrzebujemy walidacji parametrów - sprawdzanie typów, zakresów, prawidłowości
  4. Gdy chcemy kontrolować dostęp - autoryzacja, sprawdzanie uprawnień
  5. Gdy chcemy dodać metadane - dla frameworków, ORM-ów, serializacji
  6. Gdy potrzebujemy monitorować zmiany - śledzenie zmian właściwości

Ograniczenia i problemy

Podobnie jak dekoratory klas, dekoratory metod i właściwości mają swoje ograniczenia i problemy:

  1. Trudniejsze debugowanie - kod z wieloma dekoratorami może być trudniejszy do śledzenia
  2. Problem z typowaniem - może wymagać rzutowania typów (np. używania
    as any
    )
  3. Ukryta logika - implementacja może być ukryta w dekoratorze, co utrudnia zrozumienie kodu
  4. Kolejność stosowania - kolejność dekoratorów ma znaczenie i może prowadzić do subtelnych błędów
  5. Zmiany w specyfikacji - specyfikacja dekoratorów wciąż ewoluuje w JavaScript i TypeScript

Podsumowanie

Dekoratory metod i właściwości w TypeScripcie to potężne narzędzia, które pozwalają na dodanie dodatkowej funkcjonalności do metod i właściwości klas w sposób deklaratywny. Podobnie jak protokoły operacyjne w Parku Jurajskim pomagają kontrolować poszczególne działania, dekoratory metod i właściwości pomagają kontrolować i rozszerzać zachowanie poszczególnych elementów klas.

Dzięki dekoratorom, możemy tworzyć bardziej modularne, czytelne i łatwiejsze w utrzymaniu aplikacje, izolując funkcjonalności przekrojowe takie jak logowanie, walidacja czy zabezpieczenia. W dużych projektach, dobrze zaprojektowane dekoratory mogą znacząco poprawić jakość kodu i zmniejszyć jego redundancję.

Jednakże, jak każde zaawansowane narzędzie, dekoratory metod i właściwości powinny być używane z rozwagą. Nadmierne wykorzystanie dekoratorów może prowadzić do kodu, który jest trudny do zrozumienia, debugowania i testowania. Najlepszą praktyką jest używanie ich tam, gdzie rzeczywiście przynoszą korzyści, a nie tylko dla samego faktu ich użycia.

Ir a CodeWorlds