Kurs JavaScript i TypeScript · Moduł 7: Zaawansowany TypeScript
Dekoratory metod i właściwości
W tej lekcji12
"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:
- Dodać dodatkowe zachowanie przed lub po wykonaniu metody
- Walidować parametry metod
- Cachować wyniki metod
- Mierzyć czas wykonania metod
- Kontrolować dostęp do właściwości
- Transformować wartości właściwości
- Dodawać metadane do metod i właściwości
Dekoratory metod
Dekoratory metod są funkcjami, które przyjmują trzy parametry:
target- prototyp klasy (dla metod instancji) lub konstruktor klasy (dla metod statycznych)propertyKey- nazwa metodydescriptor- 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 logowanaW 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:
target- prototyp klasy (dla właściwości instancji) lub konstruktor klasy (dla właściwości statycznych)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 -> OtwartaW 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:
@Zakres- zapewnia, że wartości właściwości pozostają w określonym zakresie@LogujOperację- loguje wywołania metod@MierzCzasWykonania- mierzy czas wykonania metod@WalidujParametry- sprawdza poprawność parametrów metod@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} ===\n\n`;
251
252 raport += `Liczba badań: ${badania.length}\n`;
253 raport += `Liczba szczepień: ${szczepienia.length}\n\n`;
254
255 if (badania.length > 0) {
256 const ostatnieBadanie = badania[0];
257 raport += `OSTATNIE BADANIE:\n`;
258 raport += `Data: ${ostatnieBadanie.dataBadania.toLocaleString()}\n`;
259 raport += `Stan zdrowia: ${ostatnieBadanie.wyniki.stanZdrowia}\n`;
260 raport += `Temperatura: ${ostatnieBadanie.wyniki.temperatura}°C\n`;
261 raport += `Waga: ${ostatnieBadanie.wyniki.waga} kg\n`;
262 raport += `Długość: ${ostatnieBadanie.wyniki.długość} m\n`;
263 if (ostatnieBadanie.wyniki.uwagi) {
264 raport += `Uwagi: ${ostatnieBadanie.wyniki.uwagi}\n`;
265 }
266 raport += "\n";
267 }
268
269 if (szczepienia.length > 0) {
270 const ostatnieSzczepienie = szczepienia[0];
271 raport += `OSTATNIE SZCZEPIENIE:\n`;
272 raport += `Data: ${ostatnieSzczepienie.dataSzczepienia.toLocaleString()}\n`;
273 raport += `Rodzaj szczepionki: ${ostatnieSzczepienie.rodzajSzczepionki}\n`;
274 raport += `Dawka: ${ostatnieSzczepienie.dawka} ml\n\n`;
275 }
276
277 raport += `HISTORIA BADAŃ:\n`;
278 badania.forEach((badanie, indeks) => {
279 raport += `${indeks + 1}. ${badanie.dataBadania.toLocaleDateString()} - ${badanie.wyniki.stanZdrowia}\n`;
280 });
281
282 raport += `\nHISTORIA SZCZEPIEŃ:\n`;
283 szczepienia.forEach((szczepienie, indeks) => {
284 raport += `${indeks + 1}. ${szczepienie.dataSzczepienia.toLocaleDateString()} - ${szczepienie.rodzajSzczepionki}\n`;
285 });
286
287 return raport;
288 }
289}
290
291// Testujemy system
292const rejestrMedyczny = new RejestrMedyczny();
293
294// Rejestrujemy badania i szczepienia
295try {
296 // Badanie z przeszłości - OK
297 const badanie1 = rejestrMedyczny.zarejestrujBadanie(
298 new Date("2023-09-01"),
299 "TREX-001",
300 38.2,
301 7500,
302 12.5,
303 "Zdrowy",
304 "Badanie rutynowe",
305 "Dr. Harding"
306 );
307
308 // Badanie z przyszłości - Błąd
309 const badanie2 = rejestrMedyczny.zarejestrujBadanie(
310 new Date("2025-01-01"), // przyszłość
311 "TREX-001",
312 39.1,
313 7450,
314 12.5,
315 "Lekkie objawy",
316 "Lekka gorączka"
317 );
318} catch (błąd) {
319 console.error(błąd.message);
320}
321
322// Rejestrujemy szczepienia
323try {
324 // Pierwsze szczepienie
325 const szczepienie1 = rejestrMedyczny.zarejestrujSzczepienie(
326 "TREX-001",
327 new Date("2023-01-15"),
328 "Szczepionka przeciw grypie dinozaurów",
329 50,
330 "Dr. Sattler"
331 );
332
333 // Drugie szczepienie - za wcześnie, powinien być błąd
334 const szczepienie2 = rejestrMedyczny.zarejestrujSzczepienie(
335 "TREX-001",
336 new Date("2023-03-15"), // Tylko 60 dni później, a minimum to 90 dni
337 "Szczepionka przeciw pasożytom",
338 30
339 );
340} catch (błąd) {
341 console.error(błąd.message);
342}
343
344// Testujemy pobieranie danych
345console.log(rejestrMedyczny.pobierzHistorięBadań("TREX-001")); // Wykonuje pełne pobieranie
346console.log(rejestrMedyczny.pobierzHistorięBadań("TREX-001")); // Użyje cache
347
348// Generujemy raport medyczny
349console.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:
@UnikalneID- zapewnia, że każde badanie i każde szczepienie ma unikalne ID@LogujOperację- loguje wywołania metod@WalidujDataBadania- sprawdza poprawność daty badania@KontrolujCzęstotliwość- zapewnia odpowiedni odstęp między szczepieniami@Cachuj- cachuje wyniki długotrwałych operacji@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:
- Gdy chcemy dodać aspektową funkcjonalność - logowanie, pomiar wydajności, walidacja
- Gdy chcemy cachować wyniki - dla kosztownych obliczeniowo metod
- Gdy potrzebujemy walidacji parametrów - sprawdzanie typów, zakresów, prawidłowości
- Gdy chcemy kontrolować dostęp - autoryzacja, sprawdzanie uprawnień
- Gdy chcemy dodać metadane - dla frameworków, ORM-ów, serializacji
- 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:
- Trudniejsze debugowanie - kod z wieloma dekoratorami może być trudniejszy do śledzenia
- Problem z typowaniem - może wymagać rzutowania typów (np. używania
as any) - Ukryta logika - implementacja może być ukryta w dekoratorze, co utrudnia zrozumienie kodu
- Kolejność stosowania - kolejność dekoratorów ma znaczenie i może prowadzić do subtelnych błędów
- 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.
Kod do tej lekcji: index.ts
1// Dekoratory metod i wlasciwosci - Nadzor operacji Parku Jurajskiego
2// Uwaga: Dekoratory wymagaja "experimentalDecorators": true w tsconfig.json
3
4// === DEKORATOR METODY - Logowanie operacji ===
5function LogOperation(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
6 const originalMethod = descriptor.value;
7
8 descriptor.value = function (...args: any[]) {
9 console.log("[LOG] Wywolanie: " + propertyKey + "(" + args.join(", ") + ")");
10 const startTime = Date.now();
11 const result = originalMethod.apply(this, args);
12 const duration = Date.now() - startTime;
13 console.log("[LOG] Zakonczono: " + propertyKey + " (" + duration + "ms)");
14 return result;
15 };
16
17 return descriptor;
18}
19
20// === DEKORATOR METODY - Walidacja parametrow ===
21function ValidateSector(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
22 const originalMethod = descriptor.value;
23
24 descriptor.value = function (...args: any[]) {
25 const sectorId = args[0];
26 if (typeof sectorId !== "string" || !/^[A-Z]\d{1,2}$/.test(sectorId)) {
27 throw new Error("Nieprawidlowy sektor: " + sectorId);
28 }
29 return originalMethod.apply(this, args);
30 };
31
32 return descriptor;
33}
34
35// === DEKORATOR METODY - Memoizacja (cache) ===
36function Memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
37 const originalMethod = descriptor.value;
38 const cache = new Map<string, any>();
39
40 descriptor.value = function (...args: any[]) {
41 const key = JSON.stringify(args);
42 if (cache.has(key)) {
43 console.log("[CACHE] Wynik z cache dla " + propertyKey);
44 return cache.get(key);
45 }
46 const result = originalMethod.apply(this, args);
47 cache.set(key, result);
48 return result;
49 };
50
51 return descriptor;
52}
53
54// === KLASA Z DEKORATORAMI ===
55class DinosaurTracker {
56 private dinosaurs: Map<string, any> = new Map();
57
58 constructor() {
59 this.dinosaurs.set("A1", { name: "Rex", species: "T-Rex", health: 95 });
60 this.dinosaurs.set("B3", { name: "Blue", species: "Velociraptor", health: 88 });
61 this.dinosaurs.set("C2", { name: "Bronty", species: "Brachiosaurus", health: 100 });
62 }
63
64 @LogOperation
65 @ValidateSector
66 checkSector(sectorId: string): any {
67 const dino = this.dinosaurs.get(sectorId);
68 if (!dino) return { sector: sectorId, status: "pusty" };
69 return { sector: sectorId, ...dino, status: "aktywny" };
70 }
71
72 @LogOperation
73 @Memoize
74 calculateFeedingCost(species: string, days: number): number {
75 // Symulacja kosztownych obliczen
76 const costs: Record<string, number> = {
77 "T-Rex": 500,
78 "Velociraptor": 200,
79 "Brachiosaurus": 300,
80 };
81 return (costs[species] || 100) * days;
82 }
83}
84
85// === TESTOWANIE ===
86const tracker = new DinosaurTracker();
87
88console.log("=== Sprawdzanie sektorow ===");
89console.log(tracker.checkSector("A1"));
90console.log(tracker.checkSector("B3"));
91
92console.log("\n=== Koszty karmienia ===");
93console.log("T-Rex 7 dni:", tracker.calculateFeedingCost("T-Rex", 7));
94console.log("T-Rex 7 dni (cache):", tracker.calculateFeedingCost("T-Rex", 7));
95
96console.log("\n=== Walidacja sektora ===");
97try {
98 tracker.checkSector("invalid");
99} catch (e: any) {
100 console.error("Blad:", e.message);
101}
102
103// TODO: Stworz dekorator @ReadOnly ktory uniemozliwia nadpisanie metody
104// TODO: Stworz dekorator @Throttle(ms) ograniczajacy czestotliwosc wywolanWidzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Który dekorator w TypeScript otrzymuje konstruktor klasy jako argument i może go rozszerzyć lub zastąpić?
2. Jakie trzy argumenty otrzymuje dekorator metody w TypeScript?
Zadania praktyczne w grze
- Edytor kodu
Zaimplementuj dekorator Trackable (klasy), Authorize (metody) i MaxLength (właściwości) według instrukcji w kodzie.
- Układanie w pionie
Uporządkuj kroki tworzenia dekoratora metody w TypeScript: