"Każda zagroda w Parku Jurajskim musi być dostosowana do specyficznego gatunku dinozaura," wyjaśnia Dr. Ellie Sattler, kierowniczka działu badań botanicznych. "Ale mimo różnic, wszystkie zagrody działają według tych samych zasad - kontrola wejść, monitorowanie warunków środowiskowych, zautomatyzowane systemy karmienia. To, co różni zagrodę dla welociraptora od zagrody dla triceratopsa, to nie jej struktura, ale parametry."
Tak samo działają klasy generyczne w TypeScript. Pozwalają na tworzenie wielokrotnie używalnych komponentów, które mogą pracować z różnymi typami danych, przy jednoczesnym zachowaniu pełnej kontroli typów. Jeśli w funkcjach generycznych zobaczyliśmy podstawową ideę generyczności, to klasy generyczne pokazują pełny potencjał tego mechanizmu.
Klasy generyczne to szablony klas, które mogą działać z różnymi typami danych. Podobnie jak funkcje generyczne, używają one parametrów typu (np.
<T>), ale pozwalają na wykorzystanie tych parametrów w całej klasie - w polach, metodach i konstruktorach.Oto prosta klasa generyczna reprezentująca zagrodę dla dinozaurów:
1class DinosaurEnclosure<T> {
2 private inhabitants: T[];
3
4 constructor() {
5 this.inhabitants = [];
6 }
7
8 addInhabitant(dino: T): void {
9 this.inhabitants.push(dino);
10 console.log("Dodano nowego mieszkańca zagrody!");
11 }
12
13 getInhabitants(): T[] {
14 return [...this.inhabitants]; // Zwracamy kopię, aby uniknąć bezpośredniej modyfikacji
15 }
16
17 countInhabitants(): number {
18 return this.inhabitants.length;
19 }
20}Powyższa klasa może być użyta do stworzenia zagrody dla dowolnego typu dinozaurów:
1// Definicje typów dinozaurów
2interface Velociraptor {
3 name: string;
4 speed: number;
5 clawLength: number;
6}
7
8interface Triceratops {
9 name: string;
10 hornLength: number;
11 weight: number;
12}
13
14// Tworzenie wyspecjalizowanych zagród
15const raptorEnclosure = new DinosaurEnclosure<Velociraptor>();
16const triceratopsEnclosure = new DinosaurEnclosure<Triceratops>();
17
18// Dodawanie mieszkańców do zagród
19raptorEnclosure.addInhabitant({
20 name: "Blue",
21 speed: 70,
22 clawLength: 15
23});
24
25triceratopsEnclosure.addInhabitant({
26 name: "Topsy",
27 hornLength: 80,
28 weight: 9000
29});
30
31// Sprawdzanie mieszkańców
32console.log(`Liczba welociraptorów: ${raptorEnclosure.countInhabitants()}`);
33console.log(`Liczba triceratopsów: ${triceratopsEnclosure.countInhabitants()}`);
34
35// TypeScript zapewnia bezpieczeństwo typów
36// To nie skompiluje się:
37// raptorEnclosure.addInhabitant({
38// name: "Invalid",
39// hornLength: 50,
40// weight: 7000
41// });Klasy generyczne mogą używać wielu parametrów typów, co pozwala na jeszcze większą elastyczność:
1class ParkResearchLab<TSpecimen, TResult> {
2 private specimens: TSpecimen[] = [];
3 private results: Map<string, TResult> = new Map();
4
5 addSpecimen(id: string, specimen: TSpecimen): void {
6 this.specimens.push(specimen);
7 console.log(`Dodano próbkę #${id}`);
8 }
9
10 recordResult(specimenId: string, result: TResult): void {
11 this.results.set(specimenId, result);
12 console.log(`Zapisano wynik dla próbki #${specimenId}`);
13 }
14
15 getResult(specimenId: string): TResult | undefined {
16 return this.results.get(specimenId);
17 }
18}
19
20// Przykład użycia z konkretnymi typami
21interface BloodSample {
22 dinosaurId: string;
23 extractionDate: Date;
24 volume: number;
25}
26
27interface BloodTestResult {
28 oxygenLevel: number;
29 whiteBloodCellCount: number;
30 isHealthy: boolean;
31}
32
33const bloodLab = new ParkResearchLab<BloodSample, BloodTestResult>();
34
35bloodLab.addSpecimen("BLD-001", {
36 dinosaurId: "TREX-01",
37 extractionDate: new Date(2023, 4, 15),
38 volume: 250
39});
40
41bloodLab.recordResult("BLD-001", {
42 oxygenLevel: 95,
43 whiteBloodCellCount: 7500,
44 isHealthy: true
45});
46
47const result = bloodLab.getResult("BLD-001");
48console.log(`Wynik badania krwi: ${result?.isHealthy ? "Zdrowy" : "Wymaga uwagi"}`);Klasy generyczne mogą być również rozszerzane, co pozwala na tworzenie bardziej specjalizowanych implementacji:
1class MonitoredEnclosure<T> extends DinosaurEnclosure<T> {
2 private lastCheckTime: Date | null = null;
3 private securityLevel: "low" | "medium" | "high";
4
5 constructor(securityLevel: "low" | "medium" | "high" = "medium") {
6 super(); // Wywołanie konstruktora klasy bazowej
7 this.securityLevel = securityLevel;
8 }
9
10 performSecurityCheck(): void {
11 this.lastCheckTime = new Date();
12 console.log(`Kontrola bezpieczeństwa wykonana o ${this.lastCheckTime.toLocaleTimeString()}`);
13 console.log(`Poziom zabezpieczeń: ${this.securityLevel}`);
14 console.log(`Liczba mieszkańców: ${this.countInhabitants()}`);
15 }
16
17 getLastCheckTime(): Date | null {
18 return this.lastCheckTime;
19 }
20
21 upgradeSecurityLevel(): void {
22 if (this.securityLevel === "low") {
23 this.securityLevel = "medium";
24 } else if (this.securityLevel === "medium") {
25 this.securityLevel = "high";
26 }
27 console.log(`Zaktualizowano poziom zabezpieczeń do: ${this.securityLevel}`);
28 }
29}
30
31// Użycie rozszerzonej klasy
32const monitoredRaptorEnclosure = new MonitoredEnclosure<Velociraptor>("high");
33
34monitoredRaptorEnclosure.addInhabitant({
35 name: "Delta",
36 speed: 65,
37 clawLength: 14
38});
39
40monitoredRaptorEnclosure.performSecurityCheck();Podobnie jak funkcje mogą mieć parametry domyślne, klasy generyczne mogą mieć domyślne typy dla swoich parametrów typu:
1// Domyślny interfejs dinozaura
2interface DefaultDinosaur {
3 name: string;
4 species: string;
5 age: number;
6}
7
8// Klasa z domyślnym parametrem typu
9class DinosaurTracker<T = DefaultDinosaur> {
10 private trackedDinos: T[] = [];
11
12 trackDinosaur(dino: T, location: string): void {
13 this.trackedDinos.push(dino);
14 console.log(`Rozpoczęto śledzenie dinozaura w lokalizacji: ${location}`);
15 }
16
17 getTrackedCount(): number {
18 return this.trackedDinos.length;
19 }
20}
21
22// Użycie z domyślnym typem
23const defaultTracker = new DinosaurTracker();
24defaultTracker.trackDinosaur({
25 name: "Rexy",
26 species: "Tyrannosaurus Rex",
27 age: 7
28}, "Sektor B");
29
30// Użycie z niestandardowym typem
31interface AquaticDinosaur {
32 name: string;
33 underwaterSpeedKmh: number;
34 maxDiveDepthMeters: number;
35}
36
37const aquaticTracker = new DinosaurTracker<AquaticDinosaur>();
38aquaticTracker.trackDinosaur({
39 name: "Nessie",
40 underwaterSpeedKmh: 40,
41 maxDiveDepthMeters: 500
42}, "Jezioro Centralne");Ważne jest, aby zrozumieć, że parametry typu w klasach generycznych odnoszą się tylko do właściwości instancji i metod. Statyczne właściwości i metody są współdzielone między wszystkimi instancjami klasy, niezależnie od ich parametrów typu:
1class DinosaurRegistry<T> {
2 private static totalRegisteredDinos: number = 0;
3 private registeredDinos: T[] = [];
4
5 registerDinosaur(dino: T): void {
6 this.registeredDinos.push(dino);
7 DinosaurRegistry.totalRegisteredDinos++;
8 console.log(`Zarejestrowano nowego dinozaura. Łącznie: ${DinosaurRegistry.totalRegisteredDinos}`);
9 }
10
11 static getTotalRegisteredCount(): number {
12 return DinosaurRegistry.totalRegisteredDinos;
13 }
14}
15
16const rexRegistry = new DinosaurRegistry<Velociraptor>();
17const triRegistry = new DinosaurRegistry<Triceratops>();
18
19rexRegistry.registerDinosaur({
20 name: "Charlie",
21 speed: 68,
22 clawLength: 12
23});
24
25triRegistry.registerDinosaur({
26 name: "Cera",
27 hornLength: 75,
28 weight: 8500
29});
30
31console.log(`Łączna liczba zarejestrowanych dinozaurów: ${DinosaurRegistry.getTotalRegisteredCount()}`); // 2W tym przykładzie, statyczna zmienna
totalRegisteredDinos jest wspólna dla wszystkich instancji DinosaurRegistry, niezależnie od ich parametrów typu.Klasy generyczne często współpracują z interfejsami generycznymi, tworząc kompleksowy system typów:
1// Interfejs generyczny
2interface Repository<T> {
3 add(item: T): void;
4 getById(id: string): T | undefined;
5 getAll(): T[];
6 update(id: string, item: T): boolean;
7 delete(id: string): boolean;
8}
9
10// Implementacja klasy generycznej zgodnie z interfejsem generycznym
11class DinosaurRepository<T extends { id: string }> implements Repository<T> {
12 private items: Map<string, T> = new Map();
13
14 add(item: T): void {
15 this.items.set(item.id, item);
16 }
17
18 getById(id: string): T | undefined {
19 return this.items.get(id);
20 }
21
22 getAll(): T[] {
23 return Array.from(this.items.values());
24 }
25
26 update(id: string, item: T): boolean {
27 if (this.items.has(id)) {
28 this.items.set(id, item);
29 return true;
30 }
31 return false;
32 }
33
34 delete(id: string): boolean {
35 return this.items.delete(id);
36 }
37}
38
39// Użycie repozytorium z konkretnym typem
40interface DinosaurRecord {
41 id: string;
42 name: string;
43 species: string;
44 dateOfBirth: Date;
45}
46
47const dinoRepo = new DinosaurRepository<DinosaurRecord>();
48
49dinoRepo.add({
50 id: "DINO-001",
51 name: "Rex",
52 species: "Tyrannosaurus",
53 dateOfBirth: new Date(2018, 3, 10)
54});
55
56const rex = dinoRepo.getById("DINO-001");
57console.log(`Znaleziony dinozaur: ${rex?.name}, Gatunek: ${rex?.species}`);Rozbudujmy teraz naszą wiedzę o klasy generyczne, implementując kompleksowy system zarządzania zasobami (assetami) Parku Jurajskiego:
1// Podstawowy interfejs dla wszystkich assetów parku
2interface ParkAsset {
3 id: string;
4 name: string;
5 status: "operational" | "maintenance" | "offline";
6 lastUpdated: Date;
7}
8
9// Różne typy assetów
10interface Vehicle extends ParkAsset {
11 type: "jeep" | "boat" | "helicopter";
12 passengerCapacity: number;
13 currentFuel: number;
14}
15
16interface Facility extends ParkAsset {
17 type: "visitor" | "research" | "security";
18 location: { x: number, y: number };
19 capacity: number;
20}
21
22interface PowerSystem extends ParkAsset {
23 type: "main" | "backup" | "sector";
24 outputCapacity: number;
25 currentLoad: number;
26}
27
28// Generyczny interfejs dla zarządzania assetami
29interface AssetManager<T extends ParkAsset> {
30 registerAsset(asset: T): void;
31 getAsset(id: string): T | undefined;
32 updateStatus(id: string, status: T["status"]): boolean;
33 getOperationalAssets(): T[];
34}
35
36// Implementacja generycznej klasy zarządzającej assetami
37class JurassicParkAssetManager<T extends ParkAsset> implements AssetManager<T> {
38 private assets: Map<string, T> = new Map();
39 private statusChangeLog: { assetId: string, oldStatus: string, newStatus: string, timestamp: Date }[] = [];
40
41 constructor(private assetType: string) {
42 console.log(`Zainicjalizowano system zarządzania assetami typu: ${assetType}`);
43 }
44
45 registerAsset(asset: T): void {
46 if (this.assets.has(asset.id)) {
47 throw new Error(`Asset o ID ${asset.id} już istnieje`);
48 }
49
50 this.assets.set(asset.id, {
51 ...asset,
52 lastUpdated: new Date()
53 });
54
55 console.log(`Zarejestrowano nowy asset: ${asset.name} (ID: ${asset.id})`);
56 }
57
58 getAsset(id: string): T | undefined {
59 return this.assets.get(id);
60 }
61
62 updateStatus(id: string, status: T["status"]): boolean {
63 const asset = this.assets.get(id);
64
65 if (!asset) {
66 console.warn(`Nie znaleziono assetu o ID ${id}`);
67 return false;
68 }
69
70 const oldStatus = asset.status;
71
72 // Aktualizacja statusu
73 const updatedAsset = {
74 ...asset,
75 status,
76 lastUpdated: new Date()
77 };
78
79 this.assets.set(id, updatedAsset);
80
81 // Logowanie zmiany
82 this.statusChangeLog.push({
83 assetId: id,
84 oldStatus,
85 newStatus: status,
86 timestamp: new Date()
87 });
88
89 console.log(`Zaktualizowano status assetu ${asset.name} z "${oldStatus}" na "${status}"`);
90 return true;
91 }
92
93 getOperationalAssets(): T[] {
94 return Array.from(this.assets.values())
95 .filter(asset => asset.status === "operational");
96 }
97
98 getStatusChangeHistory(id?: string): typeof this.statusChangeLog {
99 if (id) {
100 return this.statusChangeLog.filter(log => log.assetId === id);
101 }
102 return [...this.statusChangeLog];
103 }
104
105 generateReport(): string {
106 const totalAssets = this.assets.size;
107 const operational = this.getOperationalAssets().length;
108 const maintenance = Array.from(this.assets.values())
109 .filter(asset => asset.status === "maintenance").length;
110 const offline = Array.from(this.assets.values())
111 .filter(asset => asset.status === "offline").length;
112
113 return `
114RAPORT STATUSU ASSETÓW: ${this.assetType.toUpperCase()}
115======================================
116Łączna liczba: ${totalAssets}
117Operacyjne: ${operational} (${Math.round(operational / totalAssets * 100)}%)
118W konserwacji: ${maintenance} (${Math.round(maintenance / totalAssets * 100)}%)
119Offline: ${offline} (${Math.round(offline / totalAssets * 100)}%)
120======================================
121Wygenerowano: ${new Date().toLocaleString()}
122 `;
123 }
124}
125
126// Użycie systemu zarządzania assetami z różnymi typami
127const vehicleManager = new JurassicParkAssetManager<Vehicle>("Pojazdy");
128const facilityManager = new JurassicParkAssetManager<Facility>("Obiekty");
129const powerManager = new JurassicParkAssetManager<PowerSystem>("Zasilanie");
130
131// Rejestracja przykładowych assetów
132vehicleManager.registerAsset({
133 id: "VEH-001",
134 name: "Explorer 04",
135 status: "operational",
136 lastUpdated: new Date(),
137 type: "jeep",
138 passengerCapacity: 5,
139 currentFuel: 75
140});
141
142vehicleManager.registerAsset({
143 id: "VEH-002",
144 name: "Explorer 05",
145 status: "maintenance",
146 lastUpdated: new Date(),
147 type: "jeep",
148 passengerCapacity: 5,
149 currentFuel: 30
150});
151
152facilityManager.registerAsset({
153 id: "FAC-001",
154 name: "Visitor Center",
155 status: "operational",
156 lastUpdated: new Date(),
157 type: "visitor",
158 location: { x: 100, y: 200 },
159 capacity: 200
160});
161
162powerManager.registerAsset({
163 id: "PWR-001",
164 name: "Main Power Grid",
165 status: "operational",
166 lastUpdated: new Date(),
167 type: "main",
168 outputCapacity: 10000,
169 currentLoad: 6500
170});
171
172// Symulacja awarii i aktualizacji statusów
173vehicleManager.updateStatus("VEH-001", "maintenance");
174powerManager.updateStatus("PWR-001", "offline");
175facilityManager.updateStatus("FAC-001", "maintenance");
176
177// Po naprawie
178setTimeout(() => {
179 vehicleManager.updateStatus("VEH-001", "operational");
180 powerManager.updateStatus("PWR-001", "operational");
181 facilityManager.updateStatus("FAC-001", "operational");
182
183 // Generowanie raportów
184 console.log(vehicleManager.generateReport());
185 console.log(powerManager.generateReport());
186
187 // Historia zmian statusu
188 console.log("Historia zmian statusu głównego zasilania:");
189 console.log(powerManager.getStatusChangeHistory("PWR-001"));
190}, 3000);Klasy generyczne są szczególnie przydatne w następujących sytuacjach:
Klasy generyczne są potężnym narzędziem w TypeScript, które pozwala na tworzenie elastycznego, bezpiecznego pod względem typów kodu. Zapewniają one doskonałą równowagę między reużywalnością a bezpieczeństwem typów, umożliwiając tworzenie komponentów, które:
Podobnie jak w Parku Jurajskim, gdzie infrastruktura musi być elastyczna, aby obsłużyć różne gatunki dinozaurów, klasy generyczne pozwalają nam budować systemy, które są zarówno elastyczne, jak i bezpieczne - idealne połączenie dla nowoczesnego rozwoju oprogramowania.
W następnym ćwiczeniu przyjrzymy się, jak możemy dodatkowo udoskonalić nasze generyki, używając ograniczeń generycznych (constraints), aby zapewnić, że nasze generyczne klasy i funkcje działają tylko z typami, które spełniają określone wymagania.