Dziedziczenie i klasy abstrakcyjne
Dwa rodzaje przeciwników mają wspólne imię, ale atakują inaczej. Zobacz, co warto współdzielić i kiedy lepsza będzie zwykła współpraca obiektów.
Po tej lekcji potrafisz
- Użyjesz extends, super i override.
- Zdefiniujesz klasę abstrakcyjną oraz metodę wymaganą w potomkach.
- Rozpoznasz różnicę między relacją „jest” i „ma”.
Bestiariusz ma wspólne zasady
Goblin i smok są przeciwnikami. Każdy ma imię i potrafi przygotować opis ataku, ale treść jest różna. Wspólny kod umieścimy w bazie, a różnice w klasach pochodnych.
Małe kroki, konkretne efekty
extends i wywołanie super
Klasa pochodna otrzymuje dostępne elementy klasy bazowej. Jeśli ma własny konstruktor, przed użyciem this wywołuje super, żeby zainicjalizować część bazową.
class Postac {
constructor(public imie: string) {}
opis(): string { return this.imie; }
}
class Mag extends Postac {
constructor(imie: string, public mana: number) { super(imie); }
override opis(): string { return super.opis() + ": mana " + this.mana; }
}
console.log(new Mag("Luna", 50).opis());
Luna: mana 50
override pokazuje, że celowo zastępujemy istniejącą metodę. super.opis() wywołuje wersję bazową. Nadpisana metoda musi zachować zgodność typów.
abstract to niegotowy przepis
Klasy abstrakcyjnej nie tworzymy przez new. Może zawierać gotowe metody i deklaracje metod, które konkretni potomkowie muszą uzupełnić.
abstract class Przeciwnik {
constructor(protected imie: string) {}
abstract atak(): string;
przedstaw(): string { return "Nadchodzi " + this.imie; }
}
class Goblin extends Przeciwnik {
override atak(): string { return this.imie + " rzuca kamieniem"; }
}
const g = new Goblin("Gron");
console.log(g.przedstaw());
console.log(g.atak());
Nadchodzi Gron
Gron rzuca kamieniem
protected pozwala potomkowi korzystać z imienia, ale zabrania jego bezpośredniego odczytu z zewnątrz w TypeScripcie.
Wspólna kolejka ataków
Kod wykonujący turę zna tylko typ Przeciwnik. Nie musi sprawdzać, czy aktualny obiekt jest smokiem czy goblinem.
abstract class Przeciwnik {
constructor(public readonly imie: string) {}
abstract obrazenia(): number;
}
class Goblin extends Przeciwnik {
override obrazenia(): number { return 5; }
}
class Smok extends Przeciwnik {
override obrazenia(): number { return 20; }
}
const kolejka: Przeciwnik[] = [new Goblin("Gron"), new Smok("Iskra")];
let suma = 0;
for (const p of kolejka) {
const cios = p.obrazenia();
suma += cios;
console.log(p.imie + ": " + cios);
}
console.log("Łącznie: " + suma);
Gron: 5
Iskra: 20
Łącznie: 25
Jak program dochodzi do wyniku
- Abstrakcyjna baza wymaga metody obrazenia z wynikiem number.
- Dwie klasy implementują tę samą umowę na różne sposoby.
- Tablica przechowuje oba obiekty pod wspólnym typem.
- Wywołana zostaje metoda rzeczywistego obiektu. To przykład polimorfizmu.
Zanim przejdziesz dalej
Czy dodanie Trolla musi zmienić pętlę liczącą obrażenia?
Sprawdź swoje rozumowanie
Nie, jeśli Troll spełnia kontrakt Przeciwnik. Dodajesz klasę i obiekt do kolejki, a pętla korzysta z tej samej metody.
Zmień kod i zobacz, co się stanie
Kompozycja: robot ma silnik
Dziedziczenie pasuje do relacji „jest rodzajem”. Robot nie jest silnikiem — ma silnik. W takiej sytuacji przekaż obiekt współpracujący do konstruktora.
class Silnik {
uruchom(): string { return "Silnik pracuje"; }
}
class Robot {
constructor(private silnik: Silnik) {}
rusz(): string { return this.silnik.uruchom() + ", robot rusza"; }
}
console.log(new Robot(new Silnik()).rusz());
Silnik pracuje, robot rusza
Nie buduj długiej hierarchii tylko po to, żeby dostać jedną metodę. Przekazywanie zależności do konstruktora wykorzystamy ponownie w kolejnej lekcji.
Gdy wynik zaskakuje
| Pomyłka | Co się dzieje i jak to poprawić |
|---|---|
| new na klasie abstract | Wybierz konkretną klasę pochodną z pełną implementacją. |
| this przed super w konstruktorze potomka | Najpierw zainicjalizuj bazę przez super(…). |
| Niezgodny wynik nadpisanej metody | Jeśli baza obiecuje number, potomek nie może zamiast tego zwracać string. |
Teraz Twoja kolej
Rozwiązanie jest jedną z możliwości. Zanim je odkryjesz, uruchom własną wersję dla podanych danych i przynajmniej jednego przypadku brzegowego.
Gracz ma opis zwracający imię. Kapitan ma przez override dopisać „ [kapitan]”, używając super.opis().
Mała podpowiedź
Nie powielaj odczytu imienia.
Przykładowe rozwiązanie z wyjaśnieniem
class Gracz {
constructor(public imie: string) {}
opis(): string { return this.imie; }
}
class Kapitan extends Gracz {
override opis(): string { return super.opis() + " [kapitan]"; }
}
console.log(new Kapitan("Ala").opis());
Ala [kapitan]
Wersja pochodna rozbudowuje bazową.
Abstrakcyjny Bilet wymaga cena(). Uczniowski zwraca 15, Zwykly 25. Wypisz ceny z tablicy Bilet[].
Mała podpowiedź
Metoda abstrakcyjna ma deklarację bez ciała.
Przykładowe rozwiązanie z wyjaśnieniem
abstract class Bilet { abstract cena(): number; }
class Uczniowski extends Bilet { override cena(): number { return 15; } }
class Zwykly extends Bilet { override cena(): number { return 25; } }
const bilety: Bilet[] = [new Uczniowski(), new Zwykly()];
console.log(bilety.map(b => b.cena()).join(", "));
15, 25
Wspólny kod nie potrzebuje if sprawdzającego klasę.
Baza Wrog przechowuje protected sila. Troll zwraca obrażenia równe sila * 2. Dla siły 7 oczekujemy 14.
Mała podpowiedź
Odczyt protected jest dostępny w potomku.
Przykładowe rozwiązanie z wyjaśnieniem
abstract class Wrog {
constructor(protected sila: number) {}
abstract cios(): number;
}
class Troll extends Wrog { override cios(): number { return this.sila * 2; } }
console.log(new Troll(7).cios());
14
Stan pozostaje ukryty dla kodu korzystającego z obiektu.
Zastosuj kompozycję: Kamera.zdjecie zwraca „Zapisano kadr”, Dron.zwiad korzysta z kamery przekazanej do konstruktora.
Mała podpowiedź
Dron nie powinien extends Kamera.
Przykładowe rozwiązanie z wyjaśnieniem
class Kamera { zdjecie(): string { return "Zapisano kadr"; } }
class Dron {
constructor(private kamera: Kamera) {}
zwiad(): string { return "Zwiad: " + this.kamera.zdjecie(); }
}
console.log(new Dron(new Kamera()).zwiad());
Zwiad: Zapisano kadr
Klasy współpracują, ale nie udają tej samej kategorii rzeczy.
Co zabierasz do następnej lekcji
- extends tworzy klasę pochodną; super odwołuje się do bazy.
- abstract wymaga implementacji przed utworzeniem konkretnego obiektu.
- Kompozycja jest dobrym wyborem, gdy obiekt ma współpracownika zamiast być jego odmianą.