Interfejsy i polimorfizm — wymieniaj współpracowników

Raport można wypisać lub zachować do sprawdzenia. Kod turnieju nie musi wiedzieć, jak działa odbiorca raportu. Wystarczy wspólna umowa.

CEL LEKCJI

Po tej lekcji potrafisz

  • Użyjesz implements do kontroli klasy.
  • Przekażesz obiekt przez interfejs do konstruktora.
  • Zastąpisz prawdziwą zależność prostym obiektem testowym.
WPROWADZENIE

Ta sama wiadomość, dwa miejsca docelowe

Na lekcji wynik wypisujemy w konsoli. W sprawdzianie chcemy zebrać komunikaty i porównać je z oczekiwaniami. Jeśli turniej sam tworzy konkretną konsolę, trudno ją podmienić. Lepiej przekazać mu obiekt, który umie zapisać tekst.

POZNAJ SKŁADNIĘ

Małe kroki, konkretne efekty

implements sprawdza umowę

Interfejs opisuje publiczne możliwości obiektu. implements nie dopisuje kodu metod: kompilator sprawdza, czy klasa dostarcza to, co obiecała.

src/index.ts — osobny przykład
interface Nadawca { wyslij(tekst: string): void }
class Konsola implements Nadawca {
  wyslij(tekst: string): void { console.log(tekst); }
}
const n: Nadawca = new Konsola();
n.wyslij("Start turnieju");
wynik w konsoli
Start turnieju

Samo implements nie nadaje też automatycznie typów parametrom implementacji. W klasie wpisujemy tekst: string.

Zgodność wynika z kształtu

TypeScript zwykle porównuje strukturę: jakie pola i metody obiekt posiada. Zwykły literał obiektowy również może spełniać interfejs, bez klasy i implements.

src/index.ts — osobny przykład
interface Zrodlo { odczytaj(): number }
function podwoj(z: Zrodlo): number { return z.odczytaj() * 2; }
const prosteZrodlo = { odczytaj: () => 7 };
console.log(podwoj(prosteZrodlo));
wynik w konsoli
14

Przy klasach z private lub protected dochodzą reguły pochodzenia tych pól. W publicznych interfejsach tej lekcji wystarczy zgodny kształt.

PRZYKŁAD Z OMÓWIENIEM

Turniej z wymiennym dziennikiem

Dziennik ma jedno zadanie: przyjąć komunikat. Turniej otrzymuje go przez konstruktor. To ręczne przekazanie zależności, bez kontenera i frameworka.

src/index.ts — osobny przykład
interface Dziennik { zapisz(tekst: string): void }
class Pamiec implements Dziennik {
  wpisy: string[] = [];
  zapisz(tekst: string): void { this.wpisy.push(tekst); }
}
class Turniej {
  constructor(private dziennik: Dziennik) {}
  zakoncz(zwyciezca: string): void { this.dziennik.zapisz("Wygrywa " + zwyciezca); }
}
const pamiec = new Pamiec();
const turniej = new Turniej(pamiec);
turniej.zakoncz("Alfa");
turniej.zakoncz("Beta");
console.log(pamiec.wpisy.join(" | "));
wynik w konsoli
Wygrywa Alfa | Wygrywa Beta

Jak program dochodzi do wyniku

  1. Dziennik nie narzuca, gdzie trafi tekst.
  2. Pamiec implementuje metodę, dopisując wiadomość do tablicy.
  3. Turniej przechowuje zależność pod typem interfejsu.
  4. Na końcu odczytujemy dwa komunikaty — możemy sprawdzić rezultat bez przechwytywania konsoli.

Zanim przejdziesz dalej

Czy Turniej musi extends Pamiec?

Sprawdź swoje rozumowanie

Nie. Turniej korzysta z dziennika, a nie jest dziennikiem. Konstruktor wyraża tę współpracę przez kompozycję.

EKSPERYMENT

Zmień kod i zobacz, co się stanie

Łączenie publicznych możliwości

Interfejs może rozszerzać inny interfejs. Opisuje wtedy sumę wymaganych elementów. To nadal opis typu, nie dziedziczenie gotowego kodu.

src/index.ts — osobny przykład
interface Nazwany { nazwa: string }
interface Produkt extends Nazwany { cena: number }
interface Promocja { rabat: number }
type Oferta = Produkt & Promocja;
const oferta: Oferta = { nazwa: "Pad", cena: 100, rabat: 10 };
console.log(oferta.nazwa, oferta.cena - oferta.rabat);
wynik w konsoli
Pad 90

Znak & oznacza przecięcie typów: wartość musi spełniać oba opisy. Nie myl go z unią |, która dopuszcza warianty. Sprzeczne wymagania mogą stworzyć typ niemożliwy do spełnienia.

CZĘSTE BŁĘDY

Gdy wynik zaskakuje

Pomyłka Co się dzieje i jak to poprawić
implements traktowane jak extends implements kontroluje kształt, ale nie dziedziczy implementacji.
Interfejs użyty przez instanceof Interfejs nie istnieje w runtime. instanceof działa m.in. z konstruktorami klas.
Tworzenie zależności wewnątrz każdej metody Przekazanie jej do konstruktora ułatwia zmianę implementacji i testowanie.
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 1Wspólny opis★☆☆

Interfejs Opisywalny wymaga opis(): string. Niech Gra implementuje go i zwróci „Gra: Kosmos”.

Mała podpowiedź

Metoda w klasie ma zgodny typ wyniku.

Przykładowe rozwiązanie z wyjaśnieniem
src/index.ts — osobny przykład
interface Opisywalny { opis(): string }
class Gra implements Opisywalny { opis(): string { return "Gra: Kosmos"; } }
console.log(new Gra().opis());
wynik w konsoli
Gra: Kosmos

implements sprawdza obecność i zgodność metody.

ZAD 2Atrapa kostki★★☆

Funkcja runda przyjmuje Kostka z rzut(): number i zwraca wynik + 2. Podstaw obiekt zawsze zwracający 4.

Mała podpowiedź

Do sprawdzenia nie potrzeba losowości.

Przykładowe rozwiązanie z wyjaśnieniem
src/index.ts — osobny przykład
interface Kostka { rzut(): number }
function runda(k: Kostka): number { return k.rzut() + 2; }
console.log(runda({ rzut: () => 4 }));
wynik w konsoli
6

Stały wynik daje powtarzalny test reguły.

ZAD 3Dwa sposoby powiadamiania★★☆

W tablicy Powiadomienie[] umieść dwie implementacje opis(): string — „Dźwięk” i „Tekst”. Wypisz obie jedną pętlą.

Mała podpowiedź

Literały obiektowe mogą spełniać interfejs.

Przykładowe rozwiązanie z wyjaśnieniem
src/index.ts — osobny przykład
interface Powiadomienie { opis(): string }
const lista: Powiadomienie[] = [{ opis: () => "Dźwięk" }, { opis: () => "Tekst" }];
for (const p of lista) { console.log(p.opis()); }
wynik w konsoli
Dźwięk
Tekst

Polimorfizm nie wymaga dziedziczenia po wspólnej klasie.

ZAD 4Trener z zegarem testowym★★★

Zegar ma teraz(): number. Trener zapisuje czas startu i zwraca czas od startu. W teście uruchom przy 100, sprawdź przy 145. Jednostką są sekundy.

Mała podpowiedź

Zegar testowy może czytać zwykłą zmienną.

Przykładowe rozwiązanie z wyjaśnieniem
src/index.ts — osobny przykład
interface Zegar { teraz(): number }
class Trener {
  private start: number;
  constructor(private zegar: Zegar) { this.start = zegar.teraz(); }
  uplynelo(): number { return this.zegar.teraz() - this.start; }
}
let czas = 100;
const t = new Trener({ teraz: () => czas });
czas = 145;
console.log(t.uplynelo());
wynik w konsoli
45

Nie czekamy 45 sekund. Testujemy obliczenie, kontrolując zależność.

PODSUMOWANIE

Co zabierasz do następnej lekcji

  • Interfejs opisuje możliwości; implements sprawdza implementację.
  • Zgodny obiekt nie musi być instancją klasy.
  • Zależność przekazana do konstruktora łatwo podmienić.