"W Parku Jurajskim każdy budynek i infrastruktura muszą być odpowiednio oznaczone i zabezpieczone," wyjaśnia John Hammond, spacerując po nowo wybudowanym Centrum Gości. "Niektóre obszary są dostępne dla wszystkich, inne tylko dla personelu z odpowiednimi uprawnieniami, a jeszcze inne wymagają specjalnych protokołów bezpieczeństwa. Bez odpowiedniego systemu oznaczeń i zabezpieczeń, byłby chaos."
W TypeScripcie, podobnie jak w Parku Jurajskim, potrzebujemy sposobu na oznaczanie i modyfikowanie klas bez ingerencji w ich wewnętrzną implementację. Dekoratory klas są jak specjalne oznaczenia i protokoły bezpieczeństwa, które możemy nałożyć na klasy, aby dodać im dodatkowe funkcjonalności lub cechy.
Dekoratory klas pozwalają na modyfikowanie lub rozszerzanie klas w sposób deklaratywny. Są one szczególnie przydatne, gdy chcemy:
Dekorator klasy to po prostu funkcja, która przyjmuje konstruktor klasy jako argument i może go zmodyfikować, rozszerzyć lub całkowicie zastąpić.
Dekoratory klas umieszczamy nad definicją klasy, poprzedzając je symbolem
@ :1@dekorator
2class MojaKlasa {
3 // implementacja klasy
4}Gdzie
dekorator to funkcja:1function dekorator(target: Function) {
2 // Modyfikacja lub rozszerzenie klasy
3}Zacznijmy od prostego dekoratora klasy, który będzie oznaczał obiekty wymagające monitorowania w Parku Jurajskim:
1// Dekorator klasy
2function Monitorowany(target: Function) {
3 // Dodaj właściwość do prototypu klasy
4 target.prototype.monitorowany = true;
5
6 // Dodaj metodę do prototypu klasy
7 target.prototype.rozpocznijMonitoring = function() {
8 console.log(`Rozpoczęto monitoring: ${this.nazwa || this.constructor.name}`);
9 };
10}
11
12// Użycie dekoratora
13@Monitorowany
14class ZagrodaDinozaurow {
15 nazwa: string;
16
17 constructor(nazwa: string) {
18 this.nazwa = nazwa;
19 }
20
21 dodajDinozaura(dinozaur: string) {
22 console.log(`Dodano ${dinozaur} do zagrody ${this.nazwa}`);
23 }
24}
25
26// Użycie klasy z dekoratorem
27const zagrodaRaptorów = new ZagrodaDinozaurow("Velociraptor");
28zagrodaRaptorów.dodajDinozaura("Blue");
29
30// Teraz możemy używać właściwości i metod dodanych przez dekorator
31if ((zagrodaRaptorów as any).monitorowany) {
32 (zagrodaRaptorów as any).rozpocznijMonitoring();
33}W tym przykładzie, dekorator
Monitorowany dodaje właściwość monitorowany i metodę rozpocznijMonitoring do prototypu klasy ZagrodaDinozaurow. Dzięki temu, wszystkie instancje tej klasy będą miały dostęp do tych dodatkowych funkcjonalności.Możemy również tworzyć dekoratory, które przyjmują parametry. W takim przypadku, nasza funkcja dekoratora musi zwrócić inną funkcję, która będzie właściwym dekoratorem:
1// Dekorator fabryka
2function PoziomBezpieczenstwa(poziom: number) {
3 // Zwracamy właściwy dekorator klasy
4 return function(target: Function) {
5 // Dodajemy właściwość do klasy (nie do prototypu)
6 target.prototype.poziomBezpieczenstwa = poziom;
7
8 // Dodajemy metodę weryfikującą dostęp
9 target.prototype.sprawdzDostep = function(poziomPracownika: number): boolean {
10 return poziomPracownika >= poziom;
11 };
12
13 // Możemy też nadpisać konstruktor
14 const oryginalnyKonstruktor = target;
15
16 // Funkcja zastępująca konstruktor
17 function nowyKonstruktor(...args: any[]) {
18 console.log(`Tworzenie obiektu z poziomem bezpieczeństwa: ${poziom}`);
19
20 // Wywołanie oryginalnego konstruktora
21 const instancja = new (oryginalnyKonstruktor as any)(...args);
22
23 // Można dodać dodatkową logikę
24 if (poziom >= 4) {
25 console.log("UWAGA: Tworzenie obiektu o wysokim poziomie bezpieczeństwa!");
26 }
27
28 return instancja;
29 }
30
31 // Kopiujemy właściwości prototypu
32 nowyKonstruktor.prototype = oryginalnyKonstruktor.prototype;
33
34 // Zwracamy nowy konstruktor
35 return nowyKonstruktor as any;
36 };
37}
38
39// Użycie dekoratora z parametrem
40@PoziomBezpieczenstwa(4)
41class LaboratoriumGenetyczne {
42 nazwa: string;
43
44 constructor(nazwa: string) {
45 this.nazwa = nazwa;
46 console.log(`Utworzono laboratorium: ${nazwa}`);
47 }
48
49 klonujDinozaura(gatunek: string) {
50 console.log(`Klonowanie ${gatunek} w laboratorium ${this.nazwa}`);
51 }
52}
53
54// Użycie klasy z dekoratorem
55const laboratorium = new LaboratoriumGenetyczne("Hammond Lab");
56
57// Sprawdzanie poziomu bezpieczeństwa
58console.log(`Poziom bezpieczeństwa: ${(laboratorium as any).poziomBezpieczenstwa}`);
59
60// Weryfikacja dostępu
61const poziomPracownika = 3;
62if ((laboratorium as any).sprawdzDostep(poziomPracownika)) {
63 console.log("Dostęp przyznany do laboratorium");
64 laboratorium.klonujDinozaura("Velociraptor");
65} else {
66 console.log("Dostęp zabroniony! Wymagany wyższy poziom bezpieczeństwa.");
67}W tym przykładzie, dekorator
PoziomBezpieczenstwa przyjmuje parametr określający wymagany poziom dostępu. Dodaje on właściwość poziomBezpieczenstwa i metodę sprawdzDostep do klasy, a także nadpisuje konstruktor, aby dodać dodatkowe logowanie.Możemy zastosować wiele dekoratorów do jednej klasy. W takim przypadku, dekoratory są stosowane od dołu do góry (od ostatniego do pierwszego):
1// Dekorator rejestrujący w systemie
2function Rejestrowany(target: Function) {
3 console.log(`Rejestracja klasy: ${target.name}`);
4 (target as any).zarejestrowany = true;
5}
6
7// Dekorator dodający logowanie
8function Logowany(prefiks: string) {
9 return function(target: Function) {
10 // Zapisujemy oryginalne metody
11 const oryginalneMetody: Record<string, Function> = {};
12
13 // Iterujemy po metodach prototypu
14 for (const propertyName of Object.getOwnPropertyNames(target.prototype)) {
15 const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propertyName);
16
17 // Sprawdzamy, czy to metoda i czy nie jest konstruktorem
18 if (descriptor && typeof descriptor.value === 'function' && propertyName !== 'constructor') {
19 oryginalneMetody[propertyName] = descriptor.value;
20
21 // Nadpisujemy metodę, dodając logowanie
22 Object.defineProperty(target.prototype, propertyName, {
23 value: function(...args: any[]) {
24 console.log(`${prefiks}: Wywołanie ${propertyName} z argumentami: ${JSON.stringify(args)}`);
25 const rezultat = oryginalneMetody[propertyName].apply(this, args);
26 console.log(`${prefiks}: ${propertyName} zwróciło: ${JSON.stringify(rezultat)}`);
27 return rezultat;
28 }
29 });
30 }
31 }
32 };
33}
34
35// Użycie wielu dekoratorów
36@Rejestrowany
37@PoziomBezpieczenstwa(3)
38@Logowany("[System kontroli]")
39class SystemKontroliBram {
40 stan: "otwarte" | "zamknięte" = "zamknięte";
41
42 constructor() {
43 console.log("Inicjalizacja systemu kontroli bram.");
44 }
45
46 otwórzBramy(): boolean {
47 console.log("Otwieranie bram parku...");
48 this.stan = "otwarte";
49 return true;
50 }
51
52 zamknijBramy(): boolean {
53 console.log("Zamykanie bram parku...");
54 this.stan = "zamknięte";
55 return true;
56 }
57
58 sprawdźStan(): string {
59 return `Bramy są ${this.stan}`;
60 }
61}
62
63// Użycie klasy z wieloma dekoratorami
64const kontrolaBram = new SystemKontroliBram();
65kontrolaBram.otwórzBramy();
66console.log(kontrolaBram.sprawdźStan());
67kontrolaBram.zamknijBramy();W tym przykładzie, klasa
SystemKontroliBram ma zastosowane trzy dekoratory:@Rejestrowany - rejestruje klasę w systemie@PoziomBezpieczenstwa(3) - ustawia poziom bezpieczeństwa 3@Logowany("[System kontroli]") - dodaje logowanie do wszystkich metod klasyPoniżej przedstawiamy bardziej rozbudowany przykład systemu zarządzania Parkiem Jurajskim, wykorzystujący różne dekoratory klas:
1// Podstawowy interfejs dla wszystkich zasobów parku
2interface ZasobParku {
3 id: string;
4 nazwa: string;
5 lokalizacja: string;
6 status: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria";
7}
8
9// Rejestr zasobów parku
10class RejestrZasobów {
11 private static rejestr: Map<string, ZasobParku[]> = new Map();
12
13 static rejestrujZasób(typ: string, zasób: ZasobParku): void {
14 if (!this.rejestr.has(typ)) {
15 this.rejestr.set(typ, []);
16 }
17
18 this.rejestr.get(typ)!.push(zasób);
19 console.log(`Zarejestrowano zasób: ${zasób.nazwa} (${typ})`);
20 }
21
22 static pobierzZasoby(typ: string): ZasobParku[] {
23 return this.rejestr.get(typ) || [];
24 }
25
26 static pobierzWszystkieZasoby(): Map<string, ZasobParku[]> {
27 return new Map(this.rejestr);
28 }
29}
30
31// Dekorator do rejestracji zasobów
32function RejestrujZasób(typ: string) {
33 return function<T extends { new(...args: any[]): ZasobParku }>(konstruktor: T) {
34 return class extends konstruktor {
35 constructor(...args: any[]) {
36 super(...args);
37 RejestrZasobów.rejestrujZasób(typ, this);
38 }
39 };
40 };
41}
42
43// Dekorator do wymuszania audytu
44function WymagaAudytu(częstotliwośćDni: number) {
45 return function(target: Function) {
46 target.prototype.ostatniAudyt = new Date();
47 target.prototype.częstotliwośćAudytu = częstotliwośćDni;
48
49 target.prototype.wymagaAudytu = function(): boolean {
50 const teraz = new Date();
51 const różnicaCzasu = teraz.getTime() - this.ostatniAudyt.getTime();
52 const różnicaDni = różnicaCzasu / (1000 * 3600 * 24);
53
54 return różnicaDni >= this.częstotliwośćAudytu;
55 };
56
57 target.prototype.przeprowadźAudyt = function(): void {
58 console.log(`Przeprowadzono audyt dla: ${this.nazwa}`);
59 this.ostatniAudyt = new Date();
60 };
61 };
62}
63
64// Dekorator dodający śledzenie historii statusu
65function ŚledźStatusy(target: Function) {
66 // Dodaj tablicę historii statusów
67 target.prototype.historiaStatusów = [];
68
69 // Zapisz oryginalną metodę ustawiania statusu
70 const oryginalnySetStatus = Object.getOwnPropertyDescriptor(
71 target.prototype, 'status'
72 )?.set;
73
74 // Nadpisz setter dla statusu
75 if (oryginalnySetStatus) {
76 Object.defineProperty(target.prototype, 'status', {
77 set: function(nowyStatus: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria") {
78 const staryCzas = new Date();
79 const staryStatus = this._status;
80
81 // Wywołaj oryginalny setter
82 oryginalnySetStatus.call(this, nowyStatus);
83
84 // Zapisz zmianę w historii
85 this.historiaStatusów.push({
86 czasZmiany: staryCzas,
87 z: staryStatus,
88 na: nowyStatus
89 });
90
91 console.log(`Zmieniono status ${this.nazwa}: ${staryStatus || 'brak'} -> ${nowyStatus}`);
92 },
93 get: Object.getOwnPropertyDescriptor(target.prototype, 'status')?.get
94 });
95 }
96}
97
98// Dekorator Singleton
99function Singleton<T extends { new(...args: any[]): any }>(konstruktor: T) {
100 // Zapisz oryginalny konstruktor
101 const oryginalnyKonstruktor = konstruktor;
102
103 // Zastąp konstruktor funkcją, która kontroluje tworzenie instancji
104 const nowyKonstruktor: any = function(...args: any[]) {
105 if (!nowyKonstruktor.instancja) {
106 nowyKonstruktor.instancja = new oryginalnyKonstruktor(...args);
107 } else {
108 console.warn(`Singleton ${konstruktor.name} już istnieje. Zwracanie istniejącej instancji.`);
109 }
110
111 return nowyKonstruktor.instancja;
112 };
113
114 // Skopiuj prototyp
115 nowyKonstruktor.prototype = oryginalnyKonstruktor.prototype;
116
117 // Przechowaj oryginalny konstruktor
118 nowyKonstruktor.oryginalnyKonstruktor = oryginalnyKonstruktor;
119
120 return nowyKonstruktor;
121}
122
123// Implementacja różnych zasobów parku
124
125@RejestrujZasób("zagroda")
126@WymagaAudytu(7) // Audyt co 7 dni
127@ŚledźStatusy
128class ZagrodaDinozaurów implements ZasobParku {
129 id: string;
130 nazwa: string;
131 lokalizacja: string;
132 private _status: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" = "aktywny";
133
134 // Właściwości specyficzne dla zagrody
135 rodzajDinozaurów: string;
136 pojemność: number;
137 aktualnaLiczba: number;
138
139 constructor(
140 id: string,
141 nazwa: string,
142 lokalizacja: string,
143 rodzajDinozaurów: string,
144 pojemność: number
145 ) {
146 this.id = id;
147 this.nazwa = nazwa;
148 this.lokalizacja = lokalizacja;
149 this.rodzajDinozaurów = rodzajDinozaurów;
150 this.pojemność = pojemność;
151 this.aktualnaLiczba = 0;
152 }
153
154 get status(): "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" {
155 return this._status;
156 }
157
158 set status(nowyStatus: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria") {
159 this._status = nowyStatus;
160 }
161
162 dodajDinozaura(): boolean {
163 if (this.aktualnaLiczba < this.pojemność && this.status === "aktywny") {
164 this.aktualnaLiczba++;
165 console.log(`Dodano dinozaura do zagrody ${this.nazwa}. Aktualna liczba: ${this.aktualnaLiczba}`);
166 return true;
167 } else {
168 console.log(`Nie można dodać dinozaura do zagrody ${this.nazwa}. Zagroda pełna lub nieaktywna.`);
169 return false;
170 }
171 }
172
173 usuńDinozaura(): boolean {
174 if (this.aktualnaLiczba > 0) {
175 this.aktualnaLiczba--;
176 console.log(`Usunięto dinozaura z zagrody ${this.nazwa}. Aktualna liczba: ${this.aktualnaLiczba}`);
177 return true;
178 } else {
179 console.log(`Nie można usunąć dinozaura z zagrody ${this.nazwa}. Zagroda pusta.`);
180 return false;
181 }
182 }
183}
184
185@RejestrujZasób("budynek")
186@WymagaAudytu(30) // Audyt co 30 dni
187@ŚledźStatusy
188class BudynekParku implements ZasobParku {
189 id: string;
190 nazwa: string;
191 lokalizacja: string;
192 private _status: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" = "aktywny";
193
194 // Właściwości specyficzne dla budynku
195 rodzajBudynku: "publiczny" | "administracja" | "laboratorium" | "utrzymanie";
196 powierzchnia: number;
197
198 constructor(
199 id: string,
200 nazwa: string,
201 lokalizacja: string,
202 rodzajBudynku: "publiczny" | "administracja" | "laboratorium" | "utrzymanie",
203 powierzchnia: number
204 ) {
205 this.id = id;
206 this.nazwa = nazwa;
207 this.lokalizacja = lokalizacja;
208 this.rodzajBudynku = rodzajBudynku;
209 this.powierzchnia = powierzchnia;
210 }
211
212 get status(): "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" {
213 return this._status;
214 }
215
216 set status(nowyStatus: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria") {
217 this._status = nowyStatus;
218 }
219
220 otwórz(): void {
221 if (this.status !== "aktywny") {
222 console.log(`Nie można otworzyć budynku ${this.nazwa}. Status: ${this.status}`);
223 return;
224 }
225
226 console.log(`Otwarto budynek ${this.nazwa}`);
227 }
228
229 zamknij(): void {
230 console.log(`Zamknięto budynek ${this.nazwa}`);
231 }
232}
233
234@Singleton
235@RejestrujZasób("system")
236class SystemBezpieczeństwa implements ZasobParku {
237 id: string;
238 nazwa: string;
239 lokalizacja: string;
240 private _status: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" = "aktywny";
241
242 // Właściwości specyficzne dla systemu bezpieczeństwa
243 protokółAwaryjny: "normalny" | "ostrzeżenie" | "zagrożenie" | "ewakuacja" = "normalny";
244
245 constructor() {
246 this.id = "SYS-SEC-001";
247 this.nazwa = "Główny System Bezpieczeństwa";
248 this.lokalizacja = "Centrum kontroli";
249 }
250
251 get status(): "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" {
252 return this._status;
253 }
254
255 set status(nowyStatus: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria") {
256 this._status = nowyStatus;
257
258 if (nowyStatus !== "aktywny" && this.protokółAwaryjny === "normalny") {
259 console.warn("UWAGA: System bezpieczeństwa nie jest aktywny!");
260 this.ustawProtokoł("ostrzeżenie");
261 }
262 }
263
264 ustawProtokoł(protokół: "normalny" | "ostrzeżenie" | "zagrożenie" | "ewakuacja"): void {
265 this.protokółAwaryjny = protokół;
266 console.log(`Ustawiono protokół bezpieczeństwa: ${protokół}`);
267
268 // Implementacja działań w zależności od protokołu
269 switch (protokół) {
270 case "normalny":
271 console.log("Wszystkie systemy działają normalnie.");
272 break;
273 case "ostrzeżenie":
274 console.log("Zwiększono czujność personelu. Sprawdzenie wszystkich systemów.");
275 break;
276 case "zagrożenie":
277 console.log("UWAGA: Wykryto zagrożenie! Personel na pozycje! Przygotowanie do ewakuacji.");
278 break;
279 case "ewakuacja":
280 console.log("UWAGA! UWAGA! Natychmiastowa ewakuacja parku! To nie jest ćwiczenie!");
281 break;
282 }
283 }
284
285 sprawdźStatus(): void {
286 console.log(`Status systemu bezpieczeństwa: ${this.status}`);
287 console.log(`Aktualny protokół: ${this.protokółAwaryjny}`);
288 }
289}
290
291// Użycie klas z dekoratorami
292
293// Tworzenie zagrody
294const zagrodaT = new ZagrodaDinozaurów(
295 "ZAG-001",
296 "Zagroda T-Rex",
297 "Sektor A",
298 "Tyrannosaurus Rex",
299 2
300);
301
302zagrodaT.dodajDinozaura();
303zagrodaT.status = "w konserwacji";
304zagrodaT.status = "aktywny";
305
306// Tworzenie budynku
307const centrumGości = new BudynekParku(
308 "BUD-001",
309 "Centrum Gości",
310 "Wejście główne",
311 "publiczny",
312 2500
313);
314
315centrumGości.otwórz();
316centrumGości.status = "w konserwacji";
317centrumGości.status = "aktywny";
318
319// Tworzenie systemu bezpieczeństwa (Singleton)
320const systemBezpieczeństwa1 = new SystemBezpieczeństwa();
321systemBezpieczeństwa1.sprawdźStatus();
322systemBezpieczeństwa1.ustawProtokoł("ostrzeżenie");
323
324// Próba utworzenia drugiego systemu bezpieczeństwa
325const systemBezpieczeństwa2 = new SystemBezpieczeństwa();
326// Zauważ, że systemBezpieczeństwa1 === systemBezpieczeństwa2
327
328// Sprawdzenie audytów
329console.log(`Zagroda T-Rex wymaga audytu: ${(zagrodaT as any).wymagaAudytu()}`);
330console.log(`Centrum Gości wymaga audytu: ${(centrumGości as any).wymagaAudytu()}`);
331
332// Przeprowadzenie audytu
333if ((zagrodaT as any).wymagaAudytu()) {
334 (zagrodaT as any).przeprowadźAudyt();
335}
336
337// Pobranie wszystkich zasobów z rejestru
338console.log("Lista zasobów w parku:");
339const wszystkieZasoby = RejestrZasobów.pobierzWszystkieZasoby();
340for (const [typ, zasoby] of wszystkieZasoby) {
341 console.log(`Typ: ${typ}, Liczba: ${zasoby.length}`);
342 zasoby.forEach(zasób => {
343 console.log(` - ${zasób.nazwa} (${zasób.status})`);
344 });
345}
346
347// Sprawdzenie historii statusów
348console.log("Historia statusów zagrody T-Rex:");
349(zagrodaT as any).historiaStatusów.forEach((zmiana: any) => {
350 console.log(` ${zmiana.czasZmiany.toLocaleString()}: ${zmiana.z || 'początkowy'} -> ${zmiana.na}`);
351});Ten rozbudowany przykład demonstruje zaawansowane zastosowanie dekoratorów klas:
@RejestrujZasób - automatycznie rejestruje zasoby w centralnym rejestrze@WymagaAudytu - dodaje funkcjonalność śledzenia terminów audytów@ŚledźStatusy - dodaje historię zmian statusu@Singleton - zapewnia, że można utworzyć tylko jedną instancję klasyDekoratory klas są szczególnie przydatne:
Dekoratory klas mają również pewne ograniczenia i problemy:
as any, aby uniknąć problemów z TypeScriptemDekoratory klas w TypeScripcie są potężnym narzędziem, które pozwala na dodawanie, modyfikowanie lub zastępowanie funkcjonalności klas w sposób deklaratywny, bez ingerencji w ich wewnętrzną implementację. Podobnie jak specjalne protokoły bezpieczeństwa w Parku Jurajskim pomagają utrzymać porządek i kontrolę, dekoratory klas pomagają organizować i rozszerzać kod w sposób przejrzysty i systematyczny.
Dekoratory klas są szczególnie przydatne w dużych aplikacjach, gdzie często trzeba implementować funkcjonalności "w poprzek" wielu klas, takie jak logowanie, autoryzacja, walidacja czy rejestracja komponentów. Dzięki nim, możemy tworzyć bardziej modularny, elastyczny i łatwiejszy w utrzymaniu kod.
Pamiętaj jednak, że jak wszystkie potężne narzędzia, dekoratory klas powinny być używane z rozwagą. Nadużywanie ich może prowadzić do trudnego w debugowaniu i zrozumieniu kodu. Zawsze używaj ich, gdy rzeczywiście rozwiązują problem, a nie tylko dlatego, że wyglądają elegancko.