Lekcja 8 Polimorfizm w JavaScript

Polimorfizm w obiektach JavaScript – o co chodzi i jak to wygląda w praktyce?

Polimorfizm brzmi „mądrze”, ale w programowaniu oznacza bardzo praktyczną rzecz:
możesz traktować różne obiekty w ten sam sposób, bo mają wspólny „interfejs” (np. metodę o tej samej nazwie),
a mimo to zachowują się inaczej. W JavaScripcie polimorfizm spotkasz bardzo często, nawet jeśli nie używasz klas.

1) Najkrótsza definicja

Polimorfizm = jeden sposób użycia (ta sama metoda), wiele zachowań (różne implementacje).
Przykład: wywołujesz obj.render() na różnych obiektach i każdy renderuje po swojemu.

2) Polimorfizm w JS bez interfejsów (duck typing)

JavaScript nie wymaga formalnych interfejsów. Liczy się to, że obiekt „wygląda jak trzeba”.
Jeśli przekazujesz do funkcji obiekt, który ma metodę area(), to funkcja może z niej skorzystać —
nieważne, czy to koło, prostokąt czy coś jeszcze.

const Circle = r => ({
  area() { return Math.PI * r * r; }
});
const Rectangle = (w, h) => ({
  area() { return w * h; }
});
function printArea(shape) {
  console.log(shape.area()); // działa dla różnych obiektów, byle miały area()
}
printArea(Circle(2));
printArea(Rectangle(3, 4));

Taki styl nazywa się często duck typing: „jeśli coś kwacze jak kaczka, to traktuj to jak kaczkę”.
Czyli: jeśli obiekt ma metodę area(), to traktujemy go jako „coś, co ma pole”.

3) Polimorfizm w klasach: nadpisywanie metod (override)

Jeśli używasz klas i dziedziczenia, polimorfizm najczęściej objawia się przez
nadpisywanie (override) metod. Ta sama metoda (speak()) istnieje w klasie bazowej,
ale klasy pochodne implementują ją inaczej.

class Animal {
  speak() { return "…"; }
}
class Dog extends Animal {
  speak() { return "Hau!"; }
}
class Cat extends Animal {
  speak() { return "Miau!"; }
}
const animals = [new Dog(), new Cat(), new Animal()];
console.log(animals.map(a => a.speak())); // ["Hau!", "Miau!", "…"]

Ważne: kod wywołujący speak() nie musi wiedzieć, czy ma do czynienia z psem czy kotem.
Wywołuje metodę w ten sam sposób, a wynik zależy od konkretnego obiektu.

4) Polimorfizm bez dziedziczenia: kompozycja (często lepsza w JS)

W JavaScripcie wiele projektów woli kompozycję (składanie obiektów) zamiast rozbudowanego dziedziczenia.
Zamiast robić extends, doklejasz obiektom wspólne zachowania.
Nadal masz polimorfizm: różne obiekty udostępniają tę samą metodę, np. save().

const withSave = state => ({
  save() { console.log("Zapisuję:", state); }
});
const User = name => ({ name, ...withSave({ name }) });
const Document = title => ({ title, ...withSave({ title }) });
function saveAll(items) {
  items.forEach(i => i.save());
}
saveAll([User("Ala"), Document("Umowa")]);

Zwróć uwagę: saveAll nie interesuje się typem obiektu. Ważne jest tylko to, że każdy element ma metodę save().

5) Dlaczego polimorfizm jest przydatny?

  • Mniej ifów: zamiast if (typ === ...) wywołujesz metodę i tyle.
  • Łatwiejsze rozszerzanie: dodajesz nowy typ obiektu z tą samą metodą, a reszta kodu działa bez zmian.
  • Czystszy kod: logika „jak coś zrobić” siedzi w obiekcie, a nie w setce warunków.
  • Lepsze testowanie: możesz łatwo podmieniać obiekty (np. mocki) byle miały ten sam interfejs.

6) Typowa pułapka: „interfejs” tylko umowny

Ponieważ JS nie wymusza interfejsów, łatwo przekazać obiekt, który nie ma wymaganej metody.
Dlatego w praktyce często:

  • pilnuje się konwencji (np. każdy „renderer” ma render()),
  • dodaje się walidację lub sensowne błędy,
  • albo używa TypeScript, który potrafi interfejsy sprawdzać.
function renderAll(items) {
  items.forEach(i => {
    if (typeof i.render !== "function") {
      throw new Error("Obiekt nie ma metody render()");
    }
    i.render();
  });
}

Podsumowanie

Polimorfizm w JavaScripcie to wspólny sposób użycia obiektów (ta sama metoda), które zachowują się różnie.
Najczęściej spotkasz go jako: duck typing (obiekt „ma metodę, więc pasuje”), override w klasach,
albo kompozycję (doklejanie zachowań). To jeden z najważniejszych sposobów, by pisać kod prostszy i łatwiejszy do rozbudowy.

Koszyk