Kurs JavaScript i TypeScript · Moduł 7: Zaawansowany TypeScript
Fabryki dekoratorów
W tej lekcji7
"Standaryzacja i wielokrotne użycie to klucz do sukcesu w parku," podkreśla Simon Masrani, nowy właściciel Parku Jurajskiego. "Potrzebujemy systemów, które można łatwo dostosować do różnych gatunków dinozaurów, różnych części parku i różnych poziomów bezpieczeństwa, ale wszystko zgodnie z jednolitymi standardami."
W świecie TypeScripta, podobnie jak w zarządzaniu Parkiem Jurajskim, często potrzebujemy tworzyć podobne, ale nieco różniące się dekoratory dla różnych przypadków użycia. Fabryki dekoratorów pozwalają nam tworzyć wysoce konfigurowalne dekoratory, które możemy dostosować do konkretnych potrzeb, zachowując jednocześnie spójną strukturę i logikę.
Wprowadzenie do fabryk dekoratorów
Fabryka dekoratorów to po prostu funkcja, która zwraca dekorator, przyjmując parametry konfiguracyjne. Pozwala to na tworzenie dekoratorów "na żądanie" z różnymi wariantami zachowania, bazując na tym samym szablonie.
Główne zalety fabryk dekoratorów:
- Wielokrotne użycie - możemy tworzyć wiele różnych dekoratorów z jednej fabryki
- Konfigurowalność - dostosowujemy zachowanie dekoratora do konkretnych potrzeb
- Spójność - wszystkie dekoratory z jednej fabryki mają tę samą strukturę i koncepcję
- Łatwiejsza konserwacja - zmiana w fabryce aktualizuje wszystkie dekoratory z niej powstałe
- Typowanie - możemy definiować parametry fabryki z precyzyjnymi typami
Podstawowa struktura fabryki dekoratorów
Fabryka dekoratorów ma zazwyczaj następującą strukturę:
1// Fabryka dekoratorów klasy
2function fabrykaDekoratoraKlasy(opcje: OpcjeFabryki) {
3 // Zwraca dekorator klasy
4 return function<T extends new (...args: any[]) => any>(target: T) {
5 // Implementacja dekoratora użyłwająca opcji
6 //
7 return target; // lub zmodyfikowana klasa
8 };
9}
10
11// Fabryka dekoratorów metod
12function fabrykaDekoratoraMetody(opcje: OpcjeFabryki) {
13 // Zwraca dekorator metody
14 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
15 // Implementacja dekoratora używająca opcji
16 //
17 return descriptor;
18 };
19}
20
21// Fabryka dekoratorów właściwości
22function fabrykaDekoratoraWłaściwości(opcje: OpcjeFabryki) {
23 // Zwraca dekorator właściwości
24 return function(target: any, propertyKey: string) {
25 // Implementacja dekoratora używająca opcji
26 //
27 };
28}Przykłady fabryk dekoratorów w Parku Jurajskim
Zobaczmy, jak możemy wykorzystać fabryki dekoratorów w kontekście zarządzania Parkiem Jurajskim.
Przykład 1: Fabryka dekoratorów logowania dla różnych podsystemów
1// Typy podsystemów parku
2type PodsystemParku = "Bezpieczeństwo" | "Zagrody" | "Laboratorium" | "Transport" | "Zasilanie";
3
4// Poziomy logowania
5type PoziomLogowania = "Debug" | "Info" | "Ostrzeżenie" | "Błąd" | "Krytyczny";
6
7// Interfejs opcji dla fabryki dekoratorów logowania
8interface OpcjeLogowania {
9 podsystem: PodsystemParku;
10 poziom: PoziomLogowania;
11 rejestrujKolejnośćWywołań?: boolean;
12 czasWykonania?: boolean;
13}
14
15// Fabryka dekoratorów logowania
16function LogowanieSystemu(opcje: OpcjeLogowania) {
17 const { podsystem, poziom, rejestrujKolejnośćWywołań = false, czasWykonania = false } = opcje;
18
19 // Licznik wywołań dla każdej metody
20 const licznikWywołań: Record<string, number> = {};
21
22 // Zwracamy dekorator metody
23 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
24 // Zapisujemy oryginalną metodę
25 const oryginalnaMetoda = descriptor.value;
26
27 // Definiujemy nową metodę
28 descriptor.value = function(...args: any[]) {
29 // Rejestrowanie kolejności wywołania jeśli potrzebne
30 if (rejestrujKolejnośćWywołań) {
31 if (!licznikWywołań[propertyKey]) {
32 licznikWywołań[propertyKey] = 0;
33 }
34 licznikWywołań[propertyKey]++;
35 console.log(`[${podsystem}] [Wywołanie #${licznikWywołań[propertyKey]}] ${propertyKey}`);
36 }
37
38 // Logowanie początkowe
39 console.log(`[${poziom}] [${podsystem}] Wywołanie ${propertyKey}(${JSON.stringify(args)})`);
40
41 // Pomiar czasu jeśli potrzebny
42 const startTime = czasWykonania ? performance.now() : 0;
43
44 try {
45 // Wywołanie oryginalnej metody
46 const wynik = oryginalnaMetoda.apply(this, args);
47
48 // Logowanie po pomyślnym wykonaniu
49 console.log(`[${poziom}] [${podsystem}] Zakończono ${propertyKey} - Status: Sukces`);
50
51 // Logowanie czasu wykonania jeśli potrzebne
52 if (czasWykonania) {
53 const endTime = performance.now();
54 console.log(`[${podsystem}] Czas wykonania ${propertyKey}: ${(endTime - startTime).toFixed(2)}ms`);
55 }
56
57 return wynik;
58 } catch (błąd) {
59 // Logowanie przy błędzie
60 console.error(`[${poziom}] [${podsystem}] Błąd w ${propertyKey}: ${błąd.message}`);
61
62 // Propagujemy błąd dalej
63 throw błąd;
64 }
65 };
66
67 return descriptor;
68 };
69}
70
71// Klasa zarządzająca systemem zagród
72class SystemZagród {
73 // Używamy fabryki do tworzenia różnych dekoratorów logowania
74 @LogowanieSystemu({ podsystem: "Zagrody", poziom: "Info" })
75 otwórzZagrodę(idZagrody: string): boolean {
76 console.log(`Otwieranie zagrody ${idZagrody}`);
77 return true;
78 }
79
80 @LogowanieSystemu({ podsystem: "Zagrody", poziom: "Ostrzeżenie", czasWykonania: true })
81 zamknijZagrodę(idZagrody: string): boolean {
82 console.log(`Zamykanie zagrody ${idZagrody}`);
83
84 // Symulujemy dłuższe wykonanie
85 for (let i = 0; i < 1000000; i++) {
86 // Obciążenie procesora
87 }
88
89 return true;
90 }
91
92 @LogowanieSystemu({ podsystem: "Zagrody", poziom: "Krytyczny", rejestrujKolejnośćWywołań: true, czasWykonania: true })
93 tryBezpieczeństwa(idZagrody: string, powód: string): boolean {
94 console.log(`Aktywacja trybu bezpieczeństwa dla zagrody ${idZagrody}. Powód: ${powód}`);
95
96 // Symulujemy złożoną operację
97 for (let i = 0; i < 2000000; i++) {
98 // Obciążenie procesora
99 }
100
101 return true;
102 }
103}
104
105// Klasa zarządzająca systemem bezpieczeństwa
106class SystemBezpieczeństwa {
107 @LogowanieSystemu({ podsystem: "Bezpieczeństwo", poziom: "Info" })
108 sprawdźStatus(): { status: string, aktywneAlarmy: number } {
109 return { status: "Normalny", aktywneAlarmy: 0 };
110 }
111
112 @LogowanieSystemu({ podsystem: "Bezpieczeństwo", poziom: "Ostrzeżenie", czasWykonania: true })
113 aktywujAlarm(obszar: string, poziom: number): boolean {
114 console.log(`Aktywacja alarmu w obszarze ${obszar} na poziomie ${poziom}`);
115 return true;
116 }
117
118 @LogowanieSystemu({ podsystem: "Bezpieczeństwo", poziom: "Krytyczny", rejestrujKolejnośćWywołań: true })
119 ewakuacja(obszar: string): boolean {
120 console.log(`Protokół ewakuacji dla obszaru ${obszar}`);
121 return true;
122 }
123}
124
125// Użycie klas z dekoratorami
126const systemZagród = new SystemZagród();
127const systemBezpieczeństwa = new SystemBezpieczeństwa();
128
129// Testowanie
130systemZagród.otwórzZagrodę("A-001");
131systemZagród.zamknijZagrodę("A-001");
132systemZagród.tryBezpieczeństwa("A-001", "Test protokołu");
133systemZagród.tryBezpieczeństwa("A-001", "Ponowny test"); // Pokaże, że to drugie wywołanie
134
135systemBezpieczeństwa.sprawdźStatus();
136systemBezpieczeństwa.aktywujAlarm("Sektor B", 2);
137systemBezpieczeństwa.ewakuacja("Sektor B");W tym przykładzie, fabryka dekoratorów LogowanieSystemu tworzy dekoratory do logowania metod, które można dostosować do różnych podsystemów, poziomów logowania oraz dodatkowych opcji, takich jak rejestrowanie kolejności wywołań czy mierzenie czasu wykonania.
Przykład 2: Fabryka dekoratorów walidacji danych
1// Interfejs opcji dla fabryki dekoratorów walidacji
2interface OpcjeWalidacji<T> {
3 walidator: (wartość: T) => boolean;
4 komunikatBłędu: string;
5 wyjątek?: typeof Error;
6 transformacja?: (wartość: T) => T;
7}
8
9// Fabryka dekoratorów walidacji parametrów metody
10function WalidujParametr<T>(opcje: OpcjeWalidacji<T>) {
11 const { walidator, komunikatBłędu, wyjątek = Error, transformacja } = opcje;
12
13 // Zwracamy dekorator parametru metody
14 return function(target: Object, propertyKey: string, parameterIndex: number) {
15 // Klucz dla metadanych
16 const kluczWalidacji = `walidacja_${propertyKey}_param_${parameterIndex}`;
17
18 // Zapisujemy metadane walidacji
19 const walidacje = Reflect.getOwnMetadata(kluczWalidacji, target) || [];
20
21 // Dodajemy nową walidację
22 walidacje.push({
23 indeks: parameterIndex,
24 walidator,
25 komunikatBłędu,
26 wyjątek,
27 transformacja
28 });
29
30 // Zapisujemy zaktualizowane metadane
31 Reflect.defineMetadata(kluczWalidacji, walidacje, target);
32
33 // Patrzymy, czy już istnieje dekorator metody
34 if (!Reflect.hasMetadata(`walidacja_${propertyKey}_decorated`, target)) {
35 // Oznaczamy metodę jako już dekorowaną
36 Reflect.defineMetadata(`walidacja_${propertyKey}_decorated`, true, target);
37
38 // Pobieramy deskryptor metody
39 const oryginalnaMetoda = target[propertyKey];
40
41 // Przeładowujemy metodę
42 target[propertyKey] = function(...args: any[]) {
43 // Pobieramy wszystkie walidacje dla tej metody
44 const walidacje = Reflect.getOwnMetadata(kluczWalidacji, target) || [];
45
46 // Stosujemy walidacje
47 for (const walidacja of walidacje) {
48 const { indeks, walidator, komunikatBłędu, wyjątek, transformacja } = walidacja;
49
50 // Jeśli wartość jest dostarczana, walidujemy ją
51 if (indeks < args.length) {
52 const wartość = args[indeks];
53
54 // Sprawdzamy walidację
55 if (!walidator(wartość)) {
56 throw new wyjątek(`Błąd walidacji parametru ${indeks} w metodzie ${propertyKey}: ${komunikatBłędu}`);
57 }
58
59 // Stosujemy transformację, jeśli jest dostarczana
60 if (transformacja) {
61 args[indeks] = transformacja(wartość);
62 }
63 }
64 }
65
66 // Wywołujemy oryginalną metodę
67 return oryginalnaMetoda.apply(this, args);
68 };
69 }
70 };
71}
72
73// Zestaw predefiniowanych walidatorów
74// Walidator niepustego stringa
75function NiePusty() {
76 return WalidujParametr<string>({
77 walidator: (str) => str !== null && str !== undefined && str.trim() !== '',
78 komunikatBłędu: "Wartość nie może być pusta"
79 });
80}
81
82// Walidator minimalnej długości
83function MinDługość(długość: number) {
84 return WalidujParametr<string>({
85 walidator: (str) => str !== null && str !== undefined && str.length >= długość,
86 komunikatBłędu: `Wartość musi mieć co najmniej ${długość} znaków`
87 });
88}
89
90// Walidator zakresu liczbowego
91function Zakres(min: number, max: number) {
92 return WalidujParametr<number>({
93 walidator: (num) => num >= min && num <= max,
94 komunikatBłędu: `Wartość musi być w zakresie od ${min} do ${max}`,
95 // Opcjonalnie możemy ograniczyć wartość do zakresu
96 transformacja: (num) => Math.min(Math.max(num, min), max)
97 });
98}
99
100// Walidator pozytywnej liczby
101function Pozytywna() {
102 return WalidujParametr<number>({
103 walidator: (num) => num > 0,
104 komunikatBłędu: "Wartość musi być dodatnia"
105 });
106}
107
108// Walidator formatu daty
109function FormatDaty(format: RegExp = /^\d{4}-\d{2}-\d{2}$/) {
110 return WalidujParametr<string>({
111 walidator: (str) => format.test(str),
112 komunikatBłędu: "Wartość musi być w formacie daty YYYY-MM-DD"
113 });
114}
115
116// Klasa do zarządzania danymi dinozaurów
117class ZarządzanieDinozaurami {
118 // Utworzenie nowego dinozaura
119 dodajDinozaura(
120 @NiePusty() @MinDługość(3) nazwa: string,
121 @NiePusty() gatunek: string,
122 @Pozytywna() @Zakres(0, 100) wiek: number,
123 @Pozytywna() waga: number,
124 @FormatDaty() dataDodania: string
125 ): string {
126 const idDinozaura = `DINO-${Date.now()}`;
127
128 console.log(`Dodano nowego dinozaura:`);
129 console.log(`- ID: ${idDinozaura}`);
130 console.log(`- Nazwa: ${nazwa}`);
131 console.log(`- Gatunek: ${gatunek}`);
132 console.log(`- Wiek: ${wiek} lat`);
133 console.log(`- Waga: ${waga} kg`);
134 console.log(`- Data dodania: ${dataDodania}`);
135
136 return idDinozaura;
137 }
138
139 // Aktualizacja danych dinozaura
140 aktualizujDinozaura(
141 @NiePusty() idDinozaura: string,
142 @Zakres(0, 100) wiek?: number,
143 @Pozytywna() waga?: number
144 ): boolean {
145 console.log(`Aktualizacja dinozaura ${idDinozaura}:`);
146
147 if (wiek !== undefined) {
148 console.log(`- Nowy wiek: ${wiek} lat`);
149 }
150
151 if (waga !== undefined) {
152 console.log(`- Nowa waga: ${waga} kg`);
153 }
154
155 return true;
156 }
157
158 // Przeniesienie dinozaura do nowej zagrody
159 przenieśDinozaura(
160 @NiePusty() idDinozaura: string,
161 @NiePusty() doZagrody: string,
162 @FormatDaty() dataPrzeniesienia: string
163 ): boolean {
164 console.log(`Przeniesienie dinozaura ${idDinozaura} do zagrody ${doZagrody} w dniu ${dataPrzeniesienia}`);
165 return true;
166 }
167}
168
169// Testowanie klasy z walidacją parametrów
170const zarządzanieDino = new ZarządzanieDinozaurami();
171
172try {
173 // Poprawne wywołanie
174 zarządzanieDino.dodajDinozaura(
175 "Blue",
176 "Velociraptor",
177 5,
178 150,
179 "2023-09-15"
180 );
181
182 // Błędne wywołanie - puste imię
183 zarządzanieDino.dodajDinozaura(
184 "", // Puste imię
185 "Tyrannosaurus",
186 8,
187 7500,
188 "2023-09-15"
189 );
190} catch (błąd) {
191 console.error(błąd.message);
192}
193
194try {
195 // Poprawne wywołanie
196 zarządzanieDino.aktualizujDinozaura("DINO-123", 6, 160);
197
198 // Błędne wywołanie - ujemna waga
199 zarządzanieDino.aktualizujDinozaura("DINO-123", 6, -100);
200} catch (błąd) {
201 console.error(błąd.message);
202}
203
204try {
205 // Poprawne wywołanie
206 zarządzanieDino.przenieśDinozaura("DINO-123", "B-002", "2023-09-20");
207
208 // Błędne wywołanie - nieprawidłowy format daty
209 zarządzanieDino.przenieśDinozaura("DINO-123", "B-003", "20/09/2023");
210} catch (błąd) {
211 console.error(błąd.message);
212}W tym przykładzie stworzyliśmy fabrykę dekoratorów walidacji parametrów, która pozwala na tworzenie różnych walidatorów, takich jak NiePusty, MinDługość, Zakres, Pozytywna i FormatDaty. Dzięki temu możemy łatwo zastosować walidację do parametrów metod w sposób deklaratywny.
Przykład 3: Fabryka dekoratorów uprawnień
1// Poziomy uprawnień w parku
2enum PoziomUprawnień {
3 Gość = 0,
4 Pracownik = 1,
5 Kierownik = 2,
6 Administrator = 3
7}
8
9// Akcje w systemie
10type AkcjaSystemu = "odczyt" | "zapis" | "usuwanie" | "administracja";
11
12// Mapowanie poziomów uprawnień na dozwolone akcje
13const mapaUprawnień: Record<PoziomUprawnień, AkcjaSystemu[]> = {
14 [PoziomUprawnień.Gość]: ["odczyt"],
15 [PoziomUprawnień.Pracownik]: ["odczyt", "zapis"],
16 [PoziomUprawnień.Kierownik]: ["odczyt", "zapis", "usuwanie"],
17 [PoziomUprawnień.Administrator]: ["odczyt", "zapis", "usuwanie", "administracja"]
18};
19
20// Interfejs dla kontekstu bezpieczeństwa
21interface KontekstBezpieczeństwa {
22 użytkownik: string;
23 poziomUprawnień: PoziomUprawnień;
24 tokeny?: string[];
25}
26
27// Błąd braku uprawnień
28class BłądUprawnień extends Error {
29 constructor(akcja: string, wymaganyPoziom: PoziomUprawnień, aktualnyPoziom: PoziomUprawnień) {
30 super(`Brak uprawnień do akcji ${akcja}. Wymagany poziom: ${PoziomUprawnień[wymaganyPoziom]}, posiadany: ${PoziomUprawnień[aktualnyPoziom]}`);
31 this.name = "BłądUprawnień";
32 }
33}
34
35// Fabryka dekoratorów uprawnień
36function WymagaUprawnienia(wymaganaAkcja: AkcjaSystemu, komunikatBłędu?: string) {
37 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
38 const oryginalnaMetoda = descriptor.value;
39
40 descriptor.value = function(this: { kontekstBezpieczeństwa?: KontekstBezpieczeństwa }, ...args: any[]) {
41 // Sprawdzamy, czy mamy kontekst bezpieczeństwa
42 if (!this.kontekstBezpieczeństwa) {
43 throw new Error("Brak kontekstu bezpieczeństwa");
44 }
45
46 const { poziomUprawnień } = this.kontekstBezpieczeństwa;
47
48 // Sprawdzamy, czy użytkownik ma akcję dla swojego poziomu uprawnień
49 const dozwoloneAkcje = mapaUprawnień[poziomUprawnień] || [];
50
51 if (!dozwoloneAkcje.includes(wymaganaAkcja)) {
52 // Znajdujemy minimalny poziom uprawnień potrzebny dla tej akcji
53 let minimalnyPoziom: PoziomUprawnień = PoziomUprawnień.Administrator;
54
55 for (const [poziom, akcje] of Object.entries(mapaUprawnień)) {
56 if (akcje.includes(wymaganaAkcja) && Number(poziom) < minimalnyPoziom) {
57 minimalnyPoziom = Number(poziom) as PoziomUprawnień;
58 }
59 }
60
61 throw new BłądUprawnień(
62 wymaganaAkcja,
63 minimalnyPoziom,
64 poziomUprawnień
65 );
66 }
67
68 // Wywołujemy oryginalną metodę
69 return oryginalnaMetoda.apply(this, args);
70 };
71
72 return descriptor;
73 };
74}
75
76// Uproszczone wersje dla często używanych poziomów
77function DlaGości() {
78 return WymagaUprawnienia("odczyt");
79}
80
81function DlaPracowników() {
82 return WymagaUprawnienia("zapis");
83}
84
85function DlaKierowników() {
86 return WymagaUprawnienia("usuwanie");
87}
88
89function DlaAdministratorów() {
90 return WymagaUprawnienia("administracja");
91}
92
93// Klasa systemu zarządzania parkiem
94class SystemZarządzaniaParkiem {
95 kontekstBezpieczeństwa?: KontekstBezpieczeństwa;
96
97 constructor() {
98 // Domyślnie brak kontekstu bezpieczeństwa
99 }
100
101 // Metoda do ustawiania kontekstu bezpieczeństwa
102 zaloguj(użytkownik: string, poziomUprawnień: PoziomUprawnień): void {
103 this.kontekstBezpieczeństwa = { użytkownik, poziomUprawnień };
104 console.log(`Zalogowano użytkownika ${użytkownik} z poziomem uprawnień: ${PoziomUprawnień[poziomUprawnień]}`);
105 }
106
107 // Metoda dostępna dla wszystkich
108 @DlaGości()
109 pobierzInformacjePubliczne(): string {
110 return "Informacje publiczne o parku";
111 }
112
113 // Metoda dostępna dla pracowników i wyżej
114 @DlaPracowników()
115 aktualizujDaneDinozaura(idDinozaura: string, dane: any): boolean {
116 console.log(`Aktualizacja danych dinozaura ${idDinozaura}`);
117 return true;
118 }
119
120 // Metoda dostępna dla kierowników i wyżej
121 @DlaKierowników()
122 usuńDinozaura(idDinozaura: string): boolean {
123 console.log(`Usunięcie dinozaura ${idDinozaura}`);
124 return true;
125 }
126
127 // Metoda dostępna tylko dla administratorów
128 @DlaAdministratorów()
129 zmianaKonfiguracji(klucz: string, wartość: any): boolean {
130 console.log(`Zmiana konfiguracji: ${klucz} = ${wartość}`);
131 return true;
132 }
133
134 // Metoda z bezpośrednim użyciem fabryki
135 @WymagaUprawnienia("administracja", "Tylko administratorzy mogą restartować system")
136 restartSystemu(): boolean {
137 console.log("Restartowanie systemu...");
138 return true;
139 }
140}
141
142// Testowanie systemu uprawnień
143const system = new SystemZarządzaniaParkiem();
144
145// Test dla gościa
146console.log("--- Test dla gościa ---");
147system.zaloguj("JohnVisitor", PoziomUprawnień.Gość);
148
149try {
150 console.log(system.pobierzInformacjePubliczne()); // Powinno działać
151 system.aktualizujDaneDinozaura("DINO-123", { wiek: 5 }); // Powinno dać błąd
152} catch (błąd) {
153 console.error(błąd.message);
154}
155
156// Test dla pracownika
157console.log("\n--- Test dla pracownika ---");
158system.zaloguj("JaneEmployee", PoziomUprawnień.Pracownik);
159
160try {
161 console.log(system.pobierzInformacjePubliczne()); // Powinno działać
162 system.aktualizujDaneDinozaura("DINO-123", { wiek: 5 }); // Powinno działać
163 system.usuńDinozaura("DINO-123"); // Powinno dać błąd
164} catch (błąd) {
165 console.error(błąd.message);
166}
167
168// Test dla kierownika
169console.log("\n--- Test dla kierownika ---");
170system.zaloguj("BobManager", PoziomUprawnień.Kierownik);
171
172try {
173 system.usuńDinozaura("DINO-123"); // Powinno działać
174 system.zmianaKonfiguracji("maxDino", 100); // Powinno dać błąd
175} catch (błąd) {
176 console.error(błąd.message);
177}
178
179// Test dla administratora
180console.log("\n--- Test dla administratora ---");
181system.zaloguj("AliceAdmin", PoziomUprawnień.Administrator);
182
183try {
184 system.zmianaKonfiguracji("maxDino", 100); // Powinno działać
185 system.restartSystemu(); // Powinno działać
186} catch (błąd) {
187 console.error(błąd.message);
188}W tym przykładzie stworzyliśmy fabrykę dekoratorów uprawnień, która umożliwia kontrolowanie dostępu do metod na podstawie poziomów uprawnień użytkowników. Na jej podstawie stworzyliśmy uproszczone dekoratory dla często używanych poziomów uprawnień, takie jak DlaGości, DlaPracowników, DlaKierowników i DlaAdministratorów.
Przykład 4: Fabryka dekoratorów zarządzania zasobami
1// Typy zasobów parku
2type TypZasobu = "dinozaur" | "zagroda" | "budynek" | "pojazd" | "sprzęt";
3
4// Interfejs śledzenia wykorzystania zasobów
5interface ŚledzioneWykorzystanie {
6 zasobId: string;
7 typ: TypZasobu;
8 czasRozpoczęcia: Date;
9 czasZakończenia?: Date;
10 użytkownik: string;
11 status: "wUżyciu" | "zakończone" | "anulowane";
12}
13
14// Globalny rejestr wykorzystania zasobów
15class RejestrWykorzystaniaZasobów {
16 private static instancja: RejestrWykorzystaniaZasobów;
17 private historia: ŚledzioneWykorzystanie[] = [];
18 private aktywneWykorzystanie: Map<string, ŚledzioneWykorzystanie> = new Map();
19
20 private constructor() {}
21
22 // Metoda do pobrania singletona
23 static pobierzInstancję(): RejestrWykorzystaniaZasobów {
24 if (!RejestrWykorzystaniaZasobów.instancja) {
25 RejestrWykorzystaniaZasobów.instancja = new RejestrWykorzystaniaZasobów();
26 }
27 return RejestrWykorzystaniaZasobów.instancja;
28 }
29
30 // Metoda do rozpoczynania śledzenia
31 rozpocznijŚledzenie(zasobId: string, typ: TypZasobu, użytkownik: string): string {
32 const idUżycia = `USE-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
33
34 const śledzenie: ŚledzioneWykorzystanie = {
35 zasobId,
36 typ,
37 czasRozpoczęcia: new Date(),
38 użytkownik,
39 status: "wUżyciu"
40 };
41
42 this.aktywneWykorzystanie.set(idUżycia, śledzenie);
43
44 console.log(`Rozpoczęto śledzenie zasobu ${typ} ${zasobId} przez ${użytkownik}`);
45
46 return idUżycia;
47 }
48
49 // Metoda do zakończenia śledzenia
50 zakończŚledzenie(idUżycia: string, sukces: boolean = true): boolean {
51 if (!this.aktywneWykorzystanie.has(idUżycia)) {
52 console.warn(`Nie znaleziono aktywnego śledzenia dla ID: ${idUżycia}`);
53 return false;
54 }
55
56 const śledzenie = this.aktywneWykorzystanie.get(idUżycia)!;
57 śledzenie.czasZakończenia = new Date();
58 śledzenie.status = sukces ? "zakończone" : "anulowane";
59
60 this.historia.push(śledzenie);
61 this.aktywneWykorzystanie.delete(idUżycia);
62
63 console.log(`Zakończono śledzenie zasobu ${śledzenie.typ} ${śledzenie.zasobId} przez ${śledzenie.użytkownik} ze statusem: ${śledzenie.status}`);
64
65 return true;
66 }
67
68 // Metoda do sprawdzania, czy zasób jest aktualnie używany
69 czyZasóbWUżyciu(zasobId: string, typ: TypZasobu): boolean {
70 for (const [idUżycia, śledzenie] of this.aktywneWykorzystanie.entries()) {
71 if (śledzenie.zasobId === zasobId && śledzenie.typ === typ) {
72 return true;
73 }
74 }
75 return false;
76 }
77
78 // Metoda do pobierania historii użycia zasobu
79 pobierzHistorięZasobu(zasobId: string, typ: TypZasobu): ŚledzioneWykorzystanie[] {
80 return this.historia.filter(śledzenie =>
81 śledzenie.zasobId === zasobId && śledzenie.typ === typ
82 );
83 }
84
85 // Metoda do pobierania liczby aktywnych użyć
86 liczbaAktywnychUżyć(): number {
87 return this.aktywneWykorzystanie.size;
88 }
89}
90
91// Interfejs opcji dla fabryki dekoratorów zasobów
92interface OpcjeŚledzeniaZasobów {
93 typZasobu: TypZasobu;
94 sprawdźDostępność?: boolean;
95 indeksIdZasobu?: number;
96 komunikatNiedostępności?: string;
97}
98
99// Fabryka dekoratorów śledzenia zasobów
100function ŚledzioneWykorzystanieZasobu(opcje: OpcjeŚledzeniaZasobów) {
101 const {
102 typZasobu,
103 sprawdźDostępność = false,
104 indeksIdZasobu = 0,
105 komunikatNiedostępności = "Zasób jest obecnie niedostępny"
106 } = opcje;
107
108 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
109 const oryginalnaMetoda = descriptor.value;
110
111 descriptor.value = function(...args: any[]) {
112 // Pobieramy ID zasobu z parametrów
113 const zasobId = args[indeksIdZasobu];
114
115 if (!zasobId) {
116 throw new Error(`Nie podano ID zasobu (parametr #${indeksIdZasobu})`);
117 }
118
119 // Pobieramy rejestr
120 const rejestr = RejestrWykorzystaniaZasobów.pobierzInstancję();
121
122 // Jeśli trzeba sprawdzić dostępność
123 if (sprawdźDostępność && rejestr.czyZasóbWUżyciu(zasobId, typZasobu)) {
124 throw new Error(`${komunikatNiedostępności} (${typZasobu} ${zasobId})`);
125 }
126
127 // Pobieramy nazwę użytkownika z obiektu, jeśli dostępna
128 const użytkownik = this.użytkownik || "system";
129
130 // Rozpoczynamy śledzenie
131 const idUżycia = rejestr.rozpocznijŚledzenie(zasobId, typZasobu, użytkownik);
132
133 try {
134 // Wywołujemy oryginalną metodę
135 const wynik = oryginalnaMetoda.apply(this, args);
136
137 // Kończymy śledzenie z sukcesem
138 rejestr.zakończŚledzenie(idUżycia, true);
139
140 return wynik;
141 } catch (błąd) {
142 // Kończymy śledzenie z błędem
143 rejestr.zakończŚledzenie(idUżycia, false);
144
145 // Propagujemy błąd dalej
146 throw błąd;
147 }
148 };
149
150 return descriptor;
151 };
152}
153
154// Funkcje pomocnicze dla często używanych typów zasobów
155function ŚledzioneWykorzystanieDinozaura(opcje: Omit<OpcjeŚledzeniaZasobów, "typZasobu"> = {}) {
156 return ŚledzioneWykorzystanieZasobu({ ...opcje, typZasobu: "dinozaur" });
157}
158
159function ŚledzioneWykorzystanieZagrody(opcje: Omit<OpcjeŚledzeniaZasobów, "typZasobu"> = {}) {
160 return ŚledzioneWykorzystanieZasobu({ ...opcje, typZasobu: "zagroda" });
161}
162
163function ŚledzioneWykorzystaniePojazdu(opcje: Omit<OpcjeŚledzeniaZasobów, "typZasobu"> = {}) {
164 return ŚledzioneWykorzystanieZasobu({
165 ...opcje,
166 typZasobu: "pojazd",
167 sprawdźDostępność: true,
168 komunikatNiedostępności: "Pojazd jest obecnie w użyciu"
169 });
170}
171
172function ŚledzioneWykorzystanieSprzętu(opcje: Omit<OpcjeŚledzeniaZasobów, "typZasobu"> = {}) {
173 return ŚledzioneWykorzystanieZasobu({
174 ...opcje,
175 typZasobu: "sprzęt",
176 sprawdźDostępność: true,
177 komunikatNiedostępności: "Sprzęt jest obecnie w użyciu"
178 });
179}
180
181// Klasa zarządzania zasobami parku
182class ZarządzanieZasobamiParku {
183 użytkownik: string;
184
185 constructor(użytkownik: string) {
186 this.użytkownik = użytkownik;
187 }
188
189 // Śledzenie badania dinozaura
190 @ŚledzioneWykorzystanieDinozaura()
191 przeprowadźBadanieDinozaura(idDinozaura: string): { status: string, temperatura: number } {
192 console.log(`Przeprowadzanie badania dinozaura ${idDinozaura}`);
193
194 // Symulacja długiego procesu
195 for (let i = 0; i < 1000000; i++) {
196 // Obciążenie procesora
197 }
198
199 return { status: "zdrowy", temperatura: 38.5 };
200 }
201
202 // Śledzenie karmienia dinozaura
203 @ŚledzioneWykorzystanieDinozaura()
204 nakarmDinozaura(idDinozaura: string, typPokarmu: string, ilość: number): boolean {
205 console.log(`Karmienie dinozaura ${idDinozaura} pokarmem ${typPokarmu} (${ilość} kg)`);
206
207 // Symulacja procesu
208 for (let i = 0; i < 500000; i++) {
209 // Obciążenie procesora
210 }
211
212 return true;
213 }
214
215 // Śledzenie kontroli zagrody
216 @ŚledzioneWykorzystanieZagrody()
217 skontrolujZagrodę(idZagrody: string): { status: string, problemy: string[] } {
218 console.log(`Kontrola zagrody ${idZagrody}`);
219
220 // Symulacja kontroli
221 for (let i = 0; i < 800000; i++) {
222 // Obciążenie procesora
223 }
224
225 return { status: "OK", problemy: [] };
226 }
227
228 // Śledzenie wykorzystania pojazdu
229 @ŚledzioneWykorzystaniePojazdu()
230 użyjPojazdu(idPojazdu: string, cel: string): boolean {
231 console.log(`Użycie pojazdu ${idPojazdu} do celu: ${cel}`);
232
233 // Symulacja jazdy
234 for (let i = 0; i < 1500000; i++) {
235 // Obciążenie procesora
236 }
237
238 return true;
239 }
240
241 // Śledzenie wykorzystania sprzętu
242 @ŚledzioneWykorzystanieSprzętu({ indeksIdZasobu: 1 })
243 użyjSprzętu(cel: string, idSprzętu: string): boolean {
244 console.log(`Użycie sprzętu ${idSprzętu} do celu: ${cel}`);
245
246 // Symulacja użycia
247 for (let i = 0; i < 700000; i++) {
248 // Obciążenie procesora
249 }
250
251 return true;
252 }
253
254 // Bezpośrednie użycie fabryki z niestandardowymi opcjami
255 @ŚledzioneWykorzystanieZasobu({
256 typZasobu: "budynek",
257 sprawdźDostępność: true,
258 indeksIdZasobu: 0,
259 komunikatNiedostępności: "Budynek jest obecnie w konserwacji"
260 })
261 przeprowadźKonserwacjęBudynku(idBudynku: string, typKonserwacji: string): boolean {
262 console.log(`Konserwacja budynku ${idBudynku}: ${typKonserwacji}`);
263
264 // Symulacja konserwacji
265 for (let i = 0; i < 2000000; i++) {
266 // Obciążenie procesora
267 }
268
269 return true;
270 }
271}
272
273// Testowanie systemu śledzenia zasobów
274async function testŚledzeniaZasobów() {
275 const pracownik1 = new ZarządzanieZasobamiParku("John");
276 const pracownik2 = new ZarządzanieZasobamiParku("Alice");
277
278 // Test badania dinozaura
279 console.log("--- Test badania dinozaura ---");
280 await pracownik1.przeprowadźBadanieDinozaura("DINO-001");
281
282 // Test karmienia dinozaura
283 console.log("\n--- Test karmienia dinozaura ---");
284 await pracownik2.nakarmDinozaura("DINO-001", "mięso", 50);
285
286 // Test kontroli zagrody
287 console.log("\n--- Test kontroli zagrody ---");
288 await pracownik1.skontrolujZagrodę("ZAG-001");
289
290 // Test użycia pojazdu
291 console.log("\n--- Test użycia pojazdu ---");
292
293 try {
294 // Pierwszy pracownik używa pojazdu
295 console.log("Pracownik 1 używa pojazdu...");
296 const obietnica1 = pracownik1.użyjPojazdu("JEEP-001", "Obchód parku");
297
298 // Próbujemy użyć tego samego pojazdu przez drugiego pracownika
299 console.log("Próba użycia tego samego pojazdu przez Pracownika 2...");
300 const obietnica2 = pracownik2.użyjPojazdu("JEEP-001", "Transport");
301
302 await Promise.all([obietnica1, obietnica2]);
303 } catch (błąd) {
304 console.error(`Błąd: ${błąd.message}`);
305 }
306
307 // Test użycia sprzętu
308 console.log("\n--- Test użycia sprzętu ---");
309 await pracownik1.użyjSprzętu("Badanie", "EQUIP-001");
310
311 // Test konserwacji budynku
312 console.log("\n--- Test konserwacji budynku ---");
313 await pracownik2.przeprowadźKonserwacjęBudynku("BLD-001", "Elektryka");
314
315 // Sprawdzamy liczbę aktywnych użyć
316 const rejestr = RejestrWykorzystaniaZasobów.pobierzInstancję();
317 console.log(`\nLiczba aktywnych użyć: ${rejestr.liczbaAktywnychUżyć()}`);
318
319 // Sprawdzamy historię zasobu
320 console.log("\nHistoria dinozaura DINO-001:");
321 const historiaDino = rejestr.pobierzHistorięZasobu("DINO-001", "dinozaur");
322 console.log(`Liczba wpisów: ${historiaDino.length}`);
323 historiaDino.forEach((wpis, indeks) => {
324 console.log(`[${indeks + 1}] ${wpis.użytkownik}: ${new Date(wpis.czasRozpoczęcia).toLocaleTimeString()} - ${wpis.czasZakończenia ? new Date(wpis.czasZakończenia).toLocaleTimeString() : 'w toku'}`);
325 });
326}
327
328// Uruchomienie testu
329testŚledzeniaZasobów();W tym przykładzie stworzyliśmy fabrykę dekoratorów do śledzenia wykorzystania zasobów w Parku Jurajskim. Fabryka ŚledzioneWykorzystanieZasobu tworzy dekoratory, które automatycznie rejestrują rozpoczęcie i zakończenie korzystania z zasobu, a także mogą sprawdzać jego dostępność. Na jej podstawie stworzyliśmy uproszczone dekoratory dla często używanych typów zasobów, takie jak ŚledzioneWykorzystanieDinozaura, ŚledzioneWykorzystanieZagrody, ŚledzioneWykorzystaniePojazdu i ŚledzioneWykorzystanieSprzętu.
Łączenie fabryk dekoratorów
Jedną z największych zalet fabryk dekoratorów jest możliwość ich łączenia, co pozwala na tworzenie bardziej złożonych i elastycznych rozwiązań.
1// Łączenie fabryk dekoratorów
2function PełneŚledzenie(typZasobu: TypZasobu, poziomLogowania: PoziomLogowania = "Info") {
3 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
4 // Najpierw zastosuj logowanie
5 LogowanieSystemu({
6 podsystem: typZasobu === "dinozaur" ? "Dinozaury" :
7 typZasobu === "zagroda" ? "Zagrody" :
8 typZasobu === "pojazd" ? "Transport" :
9 typZasobu === "sprzęt" ? "Wyposażenie" : "System",
10 poziom: poziomLogowania,
11 czasWykonania: true
12 })(target, propertyKey, descriptor);
13
14 // Następnie zastosuj śledzenie zasobów
15 return ŚledzioneWykorzystanieZasobu({
16 typZasobu,
17 sprawdźDostępność: typZasobu === "pojazd" || typZasobu === "sprzęt"
18 })(target, propertyKey, descriptor);
19 };
20}
21
22// Przykład użycia połączonych fabryk
23class ZarządzanieParkiemV2 {
24 użytkownik: string;
25 kontekstBezpieczeństwa?: KontekstBezpieczeństwa;
26
27 constructor(użytkownik: string, poziomUprawnień: PoziomUprawnień) {
28 this.użytkownik = użytkownik;
29 this.kontekstBezpieczeństwa = { użytkownik, poziomUprawnień };
30 }
31
32 // Łączymy śledzenie zasobów, logowanie i uprawnienia
33 @DlaPracowników()
34 @PełneŚledzenie("dinozaur", "Info")
35 opiekujSięDinozaurem(idDinozaura: string): boolean {
36 console.log(`Opieka nad dinozaurem ${idDinozaura}`);
37 return true;
38 }
39
40 // Łączymy śledzenie zasobów, logowanie i uprawnienia z wyższym poziomem
41 @DlaKierowników()
42 @PełneŚledzenie("zagroda", "Ostrzeżenie")
43 modyfikujUstawieniaZagrody(idZagrody: string, ustawienia: any): boolean {
44 console.log(`Modyfikacja ustawień zagrody ${idZagrody}`);
45 return true;
46 }
47
48 // Łączymy wszystkie potrzebne dekoratory dla krytycznej funkcji
49 @DlaAdministratorów()
50 @PełneŚledzenie("sprzęt", "Krytyczny")
51 konfigurujSystemyBezpieczeństwa(idSprzętu: string, parametry: any): boolean {
52 console.log(`Konfiguracja systemów bezpieczeństwa za pomocą ${idSprzętu}`);
53 return true;
54 }
55}
56
57// Test łączenia fabryk dekoratorów
58function testŁączeniaFabryk() {
59 console.log("=== Test łączenia fabryk dekoratorów ===");
60
61 const pracownik = new ZarządzanieParkiemV2("Mary", PoziomUprawnień.Pracownik);
62 const kierownik = new ZarządzanieParkiemV2("Bob", PoziomUprawnień.Kierownik);
63 const admin = new ZarządzanieParkiemV2("Alice", PoziomUprawnień.Administrator);
64
65 // Test pracownika
66 console.log("\n--- Test pracownika ---");
67 try {
68 pracownik.opiekujSięDinozaurem("DINO-002"); // Powinno działać
69 pracownik.modyfikujUstawieniaZagrody("ZAG-002", { temperatura: 30 }); // Powinno dać błąd
70 } catch (błąd) {
71 console.error(błąd.message);
72 }
73
74 // Test kierownika
75 console.log("\n--- Test kierownika ---");
76 try {
77 kierownik.modyfikujUstawieniaZagrody("ZAG-002", { temperatura: 30 }); // Powinno działać
78 kierownik.konfigurujSystemyBezpieczeństwa("SEC-001", { poziom: "wysoki" }); // Powinno dać błąd
79 } catch (błąd) {
80 console.error(błąd.message);
81 }
82
83 // Test administratora
84 console.log("\n--- Test administratora ---");
85 try {
86 admin.konfigurujSystemyBezpieczeństwa("SEC-001", { poziom: "wysoki" }); // Powinno działać
87 } catch (błąd) {
88 console.error(błąd.message);
89 }
90}
91
92// Uruchomienie testu
93testŁączeniaFabryk();W tym przykładzie pokazujemy, jak można łączyć różne fabryki dekoratorów, tworząc kompleksowe rozwiązania. Funkcja PełneŚledzenie łączy dekoratory logowania i śledzenia zasobów, a następnie jest używana razem z dekoratorami uprawnień.
Najlepsze praktyki przy tworzeniu fabryk dekoratorów
1. Zachowaj spójność struktury
Staraj się, aby wszystkie fabryki dekoratorów miały podobną strukturę i konwencję nazewnictwa. Dzięki temu, kod będzie bardziej czytelny i przewidywalny.
1// Spójna struktura fabryk dekoratorów
2function LogowanieDanych(opcje: OpcjeLogowania) { /* ... */ }
3function WalidacjaDanych(opcje: OpcjeWalidacji) { /* ... */ }
4function ŚledzenieDanych(opcje: OpcjeŚledzenia) { /* ... */ }2. Używaj odpowiednich typów dla parametrów
Zawsze określaj dokładne typy dla parametrów fabryki, aby zapewnić bezpieczeństwo typów i wsparcie IDE.
1// Dobre typowanie
2interface OpcjeWalidacji<T> {
3 walidator: (wartość: T) => boolean;
4 komunikat: string;
5}
6
7function Waliduj<T>(opcje: OpcjeWalidacji<T>) {
8 //
9}3. Zapewnij sensowne wartości domyślne
Fabryki dekoratorów powinny mieć rozsądne wartości domyślne dla opcji, które nie są krytyczne.
1function LogujOperację(opcje: Partial<OpcjeLogowania> = {}) {
2 const {
3 poziom = "Info",
4 szczegółowy = false,
5 czasWykonania = false
6 } = opcje;
7
8 //
9}4. Twórz uproszczone wersje dla często używanych przypadków
Dla często używanych konfiguracji, twórz specjalizowane fabryki, które ułatwią używanie dekoratorów.
1// Specjalizowane wersje dla różnych poziomów logowania
2function LogujDebug(szczegółowy: boolean = false) {
3 return LogujOperację({ poziom: "Debug", szczegółowy });
4}
5
6function LogujInfo() {
7 return LogujOperację({ poziom: "Info" });
8}
9
10function LogujOstrzeżenie() {
11 return LogujOperację({ poziom: "Ostrzeżenie", czasWykonania: true });
12}
13
14function LogujBłąd() {
15 return LogujOperację({ poziom: "Błąd", szczegółowy: true, czasWykonania: true });
16}5. Dokumentuj swoje fabryki
Dobrze udokumentowane fabryki dekoratorów ułatwiają ich używanie.
1/**
2 * Tworzy dekorator, który waliduje parametr metody.
3 *
4 * @param opcje - Opcje walidacji
5 * @param opcje.walidator - Funkcja sprawdzająca poprawność wartości
6 * @param opcje.komunikat - Komunikat błędu w przypadku niepoprawnej wartości
7 * @param opcje.transformacja - Opcjonalna funkcja przekształcająca wartość
8 *
9 * @returns Dekorator parametru metody
10 *
11 * @example
12 * class Przykład {
13 * metoda(@Waliduj({ walidator: (x) => x > 0, komunikat: "Musi być dodatnia" }) liczba: number) {
14 * //
15 * }
16 * }
17 */
18function Waliduj<T>(opcje: OpcjeWalidacji<T>) {
19 //
20}6. Unikaj ukrytych zależności
Fabryki dekoratorów powinny być jawne w swoich zależnościach.
1// Źle - ukryta zależność
2function LogujDoPliku(ścieżkaPliku: string) {
3 // Bezpośrednio używa systemu plików
4 const fs = require('fs');
5 //
6}
7
8// Dobrze - jawna zależność
9function LogujDoPliku(logger: FileLogger) {
10 // Logger jest przekazywany jako parametr
11 //
12}7. Testuj swoje fabryki dekoratorów
Pisz testy jednostkowe dla swoich fabryk dekoratorów, aby upewnić się, że działają jak oczekiwano.
1describe('WalidujZakres', () => {
2 it('powinien przepuścić wartość w zakresie', () => {
3 //
4 });
5
6 it('powinien zgłosić błąd dla wartości poza zakresem', () => {
7 //
8 });
9
10 it('powinien ograniczyć wartość do zakresu, jeśli transformacja włączona', () => {
11 //
12 });
13});Przypadki użycia fabryk dekoratorów
Fabryki dekoratorów są szczególnie przydatne w następujących przypadkach:
- Frameworki i biblioteki - tworzenie konfigurowalnych dekoratorów dla użytkowników frameworka
- Aspekty przekrojowe - logowanie, walidacja, zabezpieczenia, które muszą być dostosowane do wielu przypadków
- Konfiguracja środowiskowa - dostosowanie zachowania dekoratorów do różnych środowisk (rozwój, produkcja)
- Zarządzanie zasobami - monitorowanie, śledzenie i kontrolowanie dostępu do zasobów
- Zbieranie metryk - konfigurowanie sposobu zbierania metryk na podstawie różnych parametrów
- Kontrola dostępu - tworzenie dekoratorów uprawnień dla różnych poziomów i ról
Podsumowanie
Fabryki dekoratorów w TypeScript są potężnym narzędziem, które pozwala na tworzenie elastycznych, konfigurowalnych i wielokrotnego użytku dekoratorów. Podobnie jak systemy w Parku Jurajskim muszą być dostosowane do różnych potrzeb i wymagań, ale zachowywać jednolite standardy bezpieczeństwa, fabryki dekoratorów umożliwiają tworzenie spójnych, ale dostosowanych do konkretnych przypadków użycia dekoratorów.
Dzięki fabrykom dekoratorów możemy:
- Poprawić ponowne wykorzystanie kodu - tworząc konfigurowalne szablony dekoratorów
- Zwiększyć czytelność - ukrywając złożoną logikę pod prostym, deklaratywnym interfejsem
- Zapewnić spójność - stosując te same wzorce i struktury w całej aplikacji
- Zwiększyć elastyczność - dostosowując dekoratory do różnych przypadków użycia
Fabryki dekoratorów to zaawansowana technika w TypeScripcie, która wymaga dobrego zrozumienia dekoratorów, ale oferuje ogromne korzyści w zakresie organizacji kodu, jego ponownego wykorzystania i czytelności. Gdy zostaną dobrze zaprojektowane, stają się niezwykle wartościowymi narzędziami w twoim arsenale programistycznym.
Kod do tej lekcji: index.ts
1// Fabryki dekoratorow - Konfigurowalne systemy Parku Jurajskiego
2// Fabryka dekoratorow = funkcja zwracajaca dekorator z parametrami
3
4// === FABRYKA: Kontrola dostepu wg roli ===
5function RequireRole(requiredRole: string) {
6 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
7 const originalMethod = descriptor.value;
8
9 descriptor.value = function (...args: any[]) {
10 const userRole = (this as any).currentUserRole || "guest";
11 if (userRole !== requiredRole && userRole !== "admin") {
12 console.error("[DOSTEP ZABRONIONY] " + propertyKey + " wymaga roli: " + requiredRole);
13 return null;
14 }
15 console.log("[AUTORYZACJA] " + userRole + " -> " + propertyKey + " OK");
16 return originalMethod.apply(this, args);
17 };
18
19 return descriptor;
20 };
21}
22
23// === FABRYKA: Limit czasu wykonania ===
24function Timeout(maxMs: number) {
25 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
26 const originalMethod = descriptor.value;
27
28 descriptor.value = function (...args: any[]) {
29 const start = Date.now();
30 const result = originalMethod.apply(this, args);
31 const elapsed = Date.now() - start;
32
33 if (elapsed > maxMs) {
34 console.warn("[TIMEOUT] " + propertyKey + " trwalo " + elapsed + "ms (limit: " + maxMs + "ms)");
35 }
36 return result;
37 };
38
39 return descriptor;
40 };
41}
42
43// === FABRYKA: Ponawianie operacji ===
44function Retry(attempts: number, delayLabel: string = "retry") {
45 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
46 const originalMethod = descriptor.value;
47
48 descriptor.value = function (...args: any[]) {
49 let lastError: Error | null = null;
50
51 for (let i = 1; i <= attempts; i++) {
52 try {
53 const result = originalMethod.apply(this, args);
54 if (i > 1) console.log("[" + delayLabel.toUpperCase() + "] Sukces po " + i + " probie");
55 return result;
56 } catch (error: any) {
57 lastError = error;
58 console.warn("[" + delayLabel.toUpperCase() + "] Proba " + i + "/" + attempts + " nieudana: " + error.message);
59 }
60 }
61
62 throw lastError;
63 };
64
65 return descriptor;
66 };
67}
68
69// === FABRYKA: Walidacja parametrow ===
70function ValidateParams(validators: Record<number, (val: any) => boolean>) {
71 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
72 const originalMethod = descriptor.value;
73
74 descriptor.value = function (...args: any[]) {
75 for (const [indexStr, validator] of Object.entries(validators)) {
76 const index = Number(indexStr);
77 if (!validator(args[index])) {
78 throw new Error("Walidacja parametru #" + index + " nieudana w " + propertyKey);
79 }
80 }
81 return originalMethod.apply(this, args);
82 };
83
84 return descriptor;
85 };
86}
87
88// === KLASA Z FABRYKAMI DEKORATOROW ===
89class ParkSecuritySystem {
90 currentUserRole: string = "admin";
91 private failCount: number = 0;
92
93 @RequireRole("admin")
94 @Timeout(100)
95 initiateLockdown(sector: string): string {
96 return "LOCKDOWN aktywowany w sektorze " + sector;
97 }
98
99 @RequireRole("scientist")
100 checkDNA(sampleId: string): string {
101 return "Analiza DNA probki " + sampleId + " zakonczona";
102 }
103
104 @Retry(3, "sensor")
105 readSensor(sensorId: string): number {
106 this.failCount++;
107 if (this.failCount % 3 !== 0) {
108 throw new Error("Sensor " + sensorId + " nie odpowiada");
109 }
110 return Math.floor(Math.random() * 100);
111 }
112
113 @ValidateParams({ 0: (v: any) => typeof v === "string" && v.length >= 3 })
114 registerDinosaur(name: string): string {
115 return "Zarejestrowano dinozaura: " + name;
116 }
117}
118
119// === TESTOWANIE ===
120const security = new ParkSecuritySystem();
121
122console.log("=== Fabryki dekoratorow ===\n");
123
124// Test RequireRole
125console.log(security.initiateLockdown("A1"));
126security.currentUserRole = "guest";
127security.checkDNA("DNA-001");
128security.currentUserRole = "admin";
129
130// Test Retry
131console.log("\n=== Odczyt sensora (z ponowieniem) ===");
132try {
133 const reading = security.readSensor("S-042");
134 console.log("Odczyt:", reading);
135} catch (e: any) {
136 console.error("Ostateczny blad:", e.message);
137}
138
139// Test ValidateParams
140console.log("\n=== Walidacja ===");
141console.log(security.registerDinosaur("Rex"));
142try {
143 security.registerDinosaur("AB"); // za krotka nazwa
144} catch (e: any) {
145 console.error("Blad:", e.message);
146}
147
148// TODO: Stworz fabryke @Cache(ttlMs) z czasem wygasniecia
149// TODO: Stworz fabryke @RateLimit(maxCalls, windowMs)Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Czym jest fabryka dekoratorów (decorator factory) w TypeScript?
2. Co się stanie, gdy w TypeScript zadeklarujesz dwa interfejsy o tej samej nazwie?
Zadania praktyczne w grze
- Edytor kodu
Stwórz dekorator Cache (zapamiętujący wyniki), Retry (ponawiający próby) i Deprecated (logujący ostrzeżenie) według instrukcji.
- Klikanie w kolejności
Kliknij w odpowiedniej kolejności: gdy klasa ma wiele dekoratorów (@A @B @C class X), w jakiej kolejności przebiegają etapy?
- Układanie w poziomie
Ułóż poprawną konfigurację w tsconfig.json włączającą dekoratory: