Programowanie obiektowe (OOP) to paradygmat, w którym program modeluje się jako obiekty tworzone na podstawie klas. Opiera się na czterech filarach: dziedziczeniu (klasa potomna przejmuje pola i metody rodzica), abstrakcji (modelowanie tylko istotnych dla problemu cech), hermetyzacji, czyli enkapsulacji (ukrywanie wewnętrznych pól i metod, np. przez pola prywatne `#` w JavaScripcie) oraz polimorfizmie (różne klasy potomne inaczej realizują tę samą metodę). Razem pozwalają pisać kod, który się nie powtarza, dobrze odzwierciedla rozwiązywany problem i jest odporny na błędy.
Programowanie obiektowe (ang. object oriented programming, OOP) to sposób pisania programów, w którym budujesz model rozwiązywanego problemu z obiektów – małych jednostek łączących dane i operacje na nich. Opiera się na czterech filarach: dziedziczeniu, abstrakcji, hermetyzacji (enkapsulacji) i polimorfizmie. W tym artykule poznasz każdy z nich na przykładach w JavaScripcie i dowiesz się, czym różnią się od siebie – bo to właśnie te różnice najczęściej mylą początkujących.
Czym jest programowanie obiektowe?
Programowanie obiektowe to paradygmat programowania, czyli ogólny sposób myślenia o strukturze kodu (obok niego znasz np. programowanie proceduralne i funkcyjne). Zamiast pisać luźne funkcje i zmienne, grupujesz powiązane dane i zachowania w obiektach, a obiekty tworzysz z klas – czegoś w rodzaju szablonów.
Dzięki programowaniu zorientowanemu obiektowo możemy:
- zredukować ilość pisanego kodu,
- wprowadzić łatwe w zrozumieniu abstrakcje,
- grupować kod o podobnym zastosowaniu.
Jeśli zastanawiasz się, czy jesteś już gotowy na naukę OOP, zajrzyj do artykułu Czy jestem gotowy na OOP?. Jeśli interesuje Cię sam JavaScript, więcej o obiektach i klasach znajdziesz w tekście Programowanie obiektowe w JavaScript.
Jakie są 4 filary programowania obiektowego?
Cztery filary OOP to zestaw zasad, które razem sprawiają, że kod obiektowy jest czytelny i łatwy w rozwijaniu:
| Filar | Odpowiada na pytanie | Jak to wygląda w JavaScripcie |
|---|---|---|
| Dziedziczenie | „Jak nie powtarzać kodu?” | class B extends A |
| Abstrakcja | „Które cechy obiektu są istotne?” | wybór pól i metod w klasie |
| Hermetyzacja (enkapsulacja) | „Co ukryć przed resztą programu?” | pola prywatne #, konwencja _ |
| Polimorfizm | „Jak jedna metoda może działać różnie?” | nadpisanie metody w klasie potomnej |
Wszystkie przykłady poniżej opierają się na jednym motywie – lotniskach. Kod z całego artykułu możesz podejrzeć w repozytorium.
Co to jest dziedziczenie w programowaniu obiektowym?
Istotą programowania obiektowego jest stworzenie modelu rozwiązywanego problemu za pomocą obiektów i klas. Obiekty to złożone struktury danych, które grupują powiązane właściwości i metody. Przykładem może być poniższy warsawAirport, który zawiera zarówno właściwości, jak i metody (funkcje) związane z lotniskiem Chopina w Warszawie.
const warsawAirport = {
name: 'EPWA',
runways: [11, 15, 29, 33],
activeRunway: null,
communication: {
tower: 118.3,
ground: 121.9,
approach: 125.05,
},
setActiveRunway: function (runway) {
this.activeRunway = runway;
},
};
Załóżmy, że dla wszystkich lotnisk w Europie chcemy stworzyć podobne obiekty. Problematyczne byłoby definiowanie wszystkich właściwości i metod od nowa dla każdego lotniska. Z pomocą przychodzą klasy – szablony, na podstawie których tworzymy nowe obiekty. Można je sobie wyobrazić jako fabryki, np. zabawek czy samochodów. Teoretycznie rzecz ujmując, są to po prostu funkcje, które korzystają z konstruktorów i prototypów. Słowo kluczowe class to lukier składniowy, który pojawił się wraz z ES6. Rozważmy klasę Airport, dzięki której mógł powstać obiekt warsawAirport.
class Airport {
constructor(name, runways, activeRunway, communication) {
this.name = name;
this.runways = runways;
this.activeRunway = activeRunway;
this.communication = communication;
}
setActiveRunway(runway) {
this.activeRunway = runway;
}
}
Na podstawie tej klasy stworzymy dowolne lotnisko w taki sposób:
const warsawAirport = new Airport('EPWA', [11, 15, 29, 33], null, {
tower: 118.3,
ground: 121.9,
approach: 125.05
});
Dzięki klasom możemy tworzyć nie tylko obiekty, ale i kolejne klasy. Klasę-rodzica nazwiemy wtedy nadrzędną, a jej dziecko – potomną. Klasy potomne dziedziczą zarówno pola (właściwości), jak i akcje (metody) po rodzicu. Mogą różnić się dodatkowymi polami i akcjami. Tworzymy je za pomocą słowa kluczowego extends. Spójrz na klasę PolishAirport, która dziedziczy po Airport i jednocześnie wprowadza nowe pole country.
class PolishAirport extends Airport {
constructor(name, runways, activeRunway, communication) {
super(name, runways, activeRunway, communication);
this.country = 'Poland';
}
}
const krakowAirport = new PolishAirport('EPKK', [7, 25], 7, {
tower: 123.25,
ground: 118.1,
approach: 121.07
});
Sprawdzenie w konsoli krakowAirport pokaże wszystkie pola i metody z klasy Airport oraz pole country o wartości Poland. Metodę super(), której użyłem w konstruktorze, wywołujemy wtedy, gdy chcemy rozszerzyć jakąś klasę. Kod z tego rozdziału znajdziesz w repozytorium.
Czy dziedziczenie to zawsze dobry pomysł?
Nie. Dziedziczenie wyraża relację „jest rodzajem” (PolishAirport jest lotniskiem), ale łatwo nim przesadzić: głębokie drzewa klas są trudne do zmiany, bo modyfikacja rodzica wpływa na wszystkie dzieci. Dlatego w praktyce często zaleca się kompozycję zamiast dziedziczenia, czyli relację „ma” (lotnisko ma terminale), a nie „jest”. To jeden z najczęściej omawianych dylematów w świecie OOP, a różnice między relacjami „ma” a „jest” opisuję szerzej w artykule o diagramach klas UML.
💬 Rada mentora: Dziedziczenia używaj wtedy, gdy możesz powiedzieć „X jest Y” i zachowanie rodzica faktycznie pasuje do każdego dziecka. Gdy łapiesz się na tym, że klasa potomna ignoruje albo wyłącza część odziedziczonych metod, to znak, że lepiej złożyć obiekty z mniejszych elementów (kompozycja) niż budować kolejne piętro hierarchii.
Co to jest abstrakcja w programowaniu obiektowym?
Abstrakcja polega na modelowaniu tylko tych cech obiektu, które są istotne w danym kontekście. Duszą programowania obiektowego jest modelowanie pewnego wycinka rzeczywistości – to również istota abstrakcji. Wróćmy do przykładu: klasa Airport zawiera kilka pól i metod, a to niewiele danych jak na tak spory kompleks. Oprócz startów i lądowań dzieje się tam dużo innych rzeczy. Właśnie dlatego klasa Airport może wyglądać zupełnie inaczej w aplikacji do rezerwowania podróży, a inaczej w symulatorze lotu. W pierwszej przydadzą się informacje o poczekalniach, sklepach czy hotelach, w drugiej – o komunikacji radiowej i pasie startowym w użyciu. Zobrazuję to na klasach PassengerAirport oraz SimulatorAirport, które dziedziczą po Airport.
// PassengerAirport dziedziczy po klasie Airport
// i dodaje własne właściwości jak terminals i restaurants
class PassengerAirport extends Airport {
constructor(name, runways, terminals, restaurants) {
super(name, runways);
this.terminals = terminals;
this.restaurants = restaurants;
}
}
// SimulatorAirport dziedziczy po klasie Airport
// i dodaje własne właściwości jak activeRunway i communication
class SimulatorAirport extends Airport {
constructor(name, runways, activeRunway, communication) {
super(name, runways);
this.activeRunway = activeRunway;
this.communication = communication;
}
}
Zastosujmy nasze nowe klasy do stworzenia obiektów:
// Przykłady zastosowania
const newPassengerAirport = new PassengerAirport(
'EPGD',
[6, 24],
[
{ terminalName: 'A', capacity: 10000 },
{ terminalName: 'B', capacity: 5000 }
],
['McDonalds', 'KFC', 'Starbucks']
);
const newSimAirport = new SimulatorAirport('EPGD', [6, 24], 6, [
{ tower: 118.3, isOnline: true },
{ ground: 121.9, isOnline: false }
]);
Mimo że obie klasy dziedziczą po Airport, dzięki abstrakcji mają całkiem różne zastosowanie. To, jak modelujesz swój program, zależy tylko od Ciebie. Powyższy kod jest dostępny w repozytorium.
Co to jest hermetyzacja (enkapsulacja)?
Hermetyzacja, inaczej enkapsulacja (to dwie nazwy tego samego pojęcia), jest kolejnym filarem programowania obiektowego. Polega na tym, by ukryć pewne pola lub metody klasy, aby nie były widoczne dla użytkowników czy innych programistów. Z reszty elementów klasy można korzystać – stanowią one interfejs klasy.
Uwaga na mylące nazewnictwo: interfejs w tym znaczeniu (zestaw elementów, które klasa udostępnia na zewnątrz) to nie to samo co typ interface znany np. z TypeScriptu. Niestety to nazewnictwo weszło do powszechnego użytku w branży.
Wyobraź sobie, że ktoś z Twojego zespołu napisał bardzo skomplikowaną klasę Navigation. Ma ona dużo akcji i pól, które na siebie oddziałują, ale Ty wcale ich nie potrzebujesz. Dla przykładu może to być pole ze stałą systemu GLONASS (L1) o wartości 1602,0 MHz. Skoro kolega z zespołu nie chce, by ktokolwiek zmieniał jej wartość, może ją po prostu ukryć. Zaproponuje Ci za to metodę z interfejsu, getDistanceTo(x). Możliwe, że korzysta ona z ukrytej stałej L1, ale niekoniecznie musisz o tym wiedzieć, ani tym bardziej to modyfikować. Aby hermetyzować w JavaScripcie, możesz korzystać z pól prywatnych (#) i chronionych (_).
Pola prywatne w JavaScript
Pola prywatne weszły do kanonu JavaScriptu stosunkowo niedawno, wraz z ES2022. Takie pole jest dostępne i modyfikowalne tylko wewnątrz klasy, która je implementuje. Jeśli spróbujesz skorzystać z niego poza ciałem klasy, zobaczysz błąd. Spójrz na przykładową implementację pola prywatnego:
class Navigation {
// # jako pole prywatne
#L1 = 1602;
getDistanceTo(x) {
const position = this._getCurrentPosition();
return this._calculateDistanceToX(position, x, this.#L1);
}
}
const carNavigation = new Navigation();
// SyntaxError: Private field '#L1' must be declared in an enclosing class
console.log(carNavigation.#L1);
Pola chronione w JavaScript
Pola chronione, oznaczane podkreślnikiem (_), nadal nie są częścią JS i stanowią jedynie konwencję stosowaną przez programistów. Od pól prywatnych różni je to, że są dostępne zarówno wewnątrz klasy, jak i wewnątrz klas-dzieci powstałych przez dziedziczenie. Aby lepiej to zrozumieć, spójrz na przykład ilustrujący różnicę między polem chronionym a prywatnym:
class Navigation {
// # jako pole prywatne
#L1 = 1602;
// _ jako pole chronione
_L2 = 3218;
getDistanceTo(x) {
const position = this._getCurrentPosition();
return this._calculateDistanceToX(position, x, this.#L1);
}
}
class BikeNavigation extends Navigation {
getFieldValues() {
console.log(this._L2); // 3218
console.log(this.#L1); // Error Private field '#L1'
}
}
const bikeNavigation = new BikeNavigation();
bikeNavigation.getFieldValues();
Jaka jest różnica między abstrakcją a hermetyzacją?
To jedno z najczęściej zadawanych pytań o OOP – i nic dziwnego, bo oba filary dotyczą „ukrywania”, tylko z innego powodu:
| Abstrakcja | Hermetyzacja (enkapsulacja) | |
|---|---|---|
| Na czym polega | wybór, co w ogóle modelujesz | ukrycie, jak to działa w środku |
| Poziom | projekt (co jest istotne w klasie) | implementacja (które pola są prywatne) |
| Efekt | prostszy model | chroniony stan i stabilny interfejs |
| Przykład z artykułu | PassengerAirport kontra SimulatorAirport | prywatne pole #L1 w Navigation |
Prosty sposób, by to zapamiętać: abstrakcja decyduje, co pokazać w modelu, a hermetyzacja pilnuje, żeby nikt nie grzebał w tym, co ukryte. Bardzo często jednej klasy dotyczą obie rzeczy naraz, dlatego tak łatwo je pomylić.
Co to jest polimorfizm w programowaniu obiektowym?
Polimorfizm oznacza, że różne obiekty lub klasy powstałe jako dzieci klasy-rodzica mogą wykonywać tę samą operację na swój sposób. Wyobraź sobie klasy KrakowAirport oraz WarsawAirport, które dziedziczą po Airport. Dzięki temu mogą skorzystać z metody initApproach(), dostępnej w rodzicu. Mogą ją zostawić taką, jaka jest, albo nadpisać. Spójrz na przykład:
class Airport {
navigationalAid = 'GPS';
initApproach() {
console.log(`Approach initiated with ${this.navigationalAid}`);
}
}
class WarsawAirport extends Airport {}
class KrakowAirport extends Airport {
// polimorfizm metody initApproach()
initApproach() {
this.navigationalAid = 'ILS';
console.log(`Approach initiated with ${this.navigationalAid}`);
}
}
const airport = new Airport();
const warsawAirport = new WarsawAirport();
const krakowAirport = new KrakowAirport();
airport.initApproach(); // Approach initiated with GPS
warsawAirport.initApproach(); // Approach initiated with GPS
krakowAirport.initApproach(); // Approach initiated with ILS
Mimo że użyliśmy tej samej metody initApproach(), w dwóch potomnych obiektach otrzymaliśmy dwa różne wyniki jej działania. Kod wywołujący nie musi wiedzieć, z jakim konkretnie lotniskiem ma do czynienia – wystarczy, że wie, że ma ono metodę initApproach().
Czy programowanie obiektowe jest możliwe w JavaScripcie?
Tak, ale JavaScript nie powstał jako język obiektowy w klasycznym sensie, co rodzi kilka problemów: dynamiczne typowanie zmiennych oraz brak klas abstrakcyjnych i interfejsów. Programiści JS znaleźli jednak rozwiązanie w postaci konwencji (umownych rozwiązań). Z czasem ECMA dostrzegła potencjał drzemiący w programowaniu obiektowym w JS, więc kolejne wersje języka (ES6 i ES2022) wprowadziły rozwiązania upodabniające go do Javy czy Pythona, takie jak klasy czy pola prywatne. Kolejnym przełomem było powstanie TypeScriptu, który rozwiązuje znaczną część problemów czystego JavaScriptu i rozszerza jego możliwości.
Podsumowanie
Znasz już fundamenty programowania obiektowego: cztery filary OOP. Dzięki temu możesz pisać kod, który:
- nie powtarza się (dziedziczenie i polimorfizm),
- obrazuje problem, który chcesz rozwiązać (abstrakcja),
- jest prosty i odporny na błędy (hermetyzacja),
- a do tego przewidywalny w działaniu (polimorfizm).
Dzięki tej wiedzy łatwiej zrozumiesz filozofię działania wzorców projektowych – propozycji rozwiązań problemów spotykanych podczas tworzenia kodu, które bazują na programowaniu obiektowym. Nie są one gotowym algorytmem do skopiowania, tylko wysokopoziomowym opisem rozwiązania: porównałbym je do podręcznika ekonomicznej jazdy, a nie instrukcji budowy samochodu. Powiemy sobie o nich w następnych artykułach.
Jeśli chcesz zobaczyć, jak relacje między klasami przedstawia się na diagramach, przeczytaj Podstawy UML – diagramy klas.
Źródła
- Aleksander Shvets, Wzorce projektowe. Nowoczesny podręcznik, Refactoring.Guru, 2022
- https://piecioshka.pl/blog/2020/06/26/uml-diagram-klas.html
- https://www.ibm.com/docs/en/cloud-paks/cp-data/4.0?topic=language-object-oriented-programming
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
- http://es6-features.org/#ClassDefinition
- https://javascript.info/private-protected-properties-methods
- https://github.com/mateuszjansowa/article-patterns-js-introduction


