Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Klasy generyczne

"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.

Czym są klasy generyczne?

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 z wieloma parametrami typów

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"}`);

Rozszerzanie klas generycznych

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();

Parametry typów domyślnych

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");

Statyczne właściwości w klasach generycznych

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()}`); // 2

W tym przykładzie, statyczna zmienna

totalRegisteredDinos
jest wspólna dla wszystkich instancji
DinosaurRegistry
, niezależnie od ich parametrów typu.

Klasy generyczne a interfejsy generyczne

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}`);

Praktyczny przykład: System zarządzania assetami Parku Jurajskiego

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);

Kiedy używać klas generycznych?

Klasy generyczne są szczególnie przydatne w następujących sytuacjach:

  1. Kolekcje i kontenery danych - gdy potrzebujesz przechowywać i zarządzać elementami różnych typów
  2. Serwisy i repozytoria - gdy implementujesz wzorce projektowe, które powinny działać z różnymi typami danych
  3. Komponenty wielokrotnego użytku - gdy chcesz stworzyć elastyczne komponenty, które mogą być używane w różnych kontekstach
  4. Abstrakcje na różnych poziomach - gdy chcesz zdefiniować hierarchie klas, które mogą być specjalizowane dla różnych typów

Podsumowanie

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:

  • Mogą działać z różnymi typami danych
  • Zachowują pełne informacje o typach
  • Zapewniają wsparcie kompilatora i IDE (podpowiedzi, weryfikacja typów)
  • Są czytelne i zrozumiałe dla innych programistów

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.

Ir a CodeWorlds