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.

CEL LEKCJI

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”.
WPROWADZENIE

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.

POZNAJ SKŁADNIĘ

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

src/index.ts — osobny przykład
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());
wynik w konsoli
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ć.

src/index.ts — osobny przykład
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());
wynik w konsoli
Nadchodzi Gron
Gron rzuca kamieniem

protected pozwala potomkowi korzystać z imienia, ale zabrania jego bezpośredniego odczytu z zewnątrz w TypeScripcie.

PRZYKŁAD Z OMÓWIENIEM

Wspólna kolejka ataków

Kod wykonujący turę zna tylko typ Przeciwnik. Nie musi sprawdzać, czy aktualny obiekt jest smokiem czy goblinem.

src/index.ts — osobny przykład
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);
wynik w konsoli
Gron: 5
Iskra: 20
Łącznie: 25

Jak program dochodzi do wyniku

  1. Abstrakcyjna baza wymaga metody obrazenia z wynikiem number.
  2. Dwie klasy implementują tę samą umowę na różne sposoby.
  3. Tablica przechowuje oba obiekty pod wspólnym typem.
  4. 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.

EKSPERYMENT

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.

src/index.ts — osobny przykład
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());
wynik w konsoli
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.

CZĘSTE BŁĘDY

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

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.

ZAD 1Powitanie kapitana★☆☆

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
src/index.ts — osobny przykład
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());
wynik w konsoli
Ala [kapitan]

Wersja pochodna rozbudowuje bazową.

ZAD 2Wspólny cennik★★☆

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
src/index.ts — osobny przykład
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(", "));
wynik w konsoli
15, 25

Wspólny kod nie potrzebuje if sprawdzającego klasę.

ZAD 3Troll z siłą★★☆

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
src/index.ts — osobny przykład
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());
wynik w konsoli
14

Stan pozostaje ukryty dla kodu korzystającego z obiektu.

ZAD 4Dron ma kamerę★★★

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
src/index.ts — osobny przykład
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());
wynik w konsoli
Zwiad: Zapisano kadr

Klasy współpracują, ale nie udają tej samej kategorii rzeczy.

PODSUMOWANIE

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