Wprowadzenie do Programowania Obiektowego (OOP): Fundamenty Nowoczesnego Tworzenia Oprogramowania
W dynamicznie ewoluującym świecie tworzenia oprogramowania, Programowanie Obiektowe (OOP – Object-Oriented Programming) ugruntowało swoją pozycję jako jeden z najbardziej wpływowych i powszechnie stosowanych paradygmatów. Od jego początków w latach 60. XX wieku, szczególnie z językiem Simula, aż po współczesne środowiska deweloperskie, OOP rewolucjonizuje sposób, w jaki myślimy o strukturze kodu, zarządzaniu złożonością i efektywnym projektowaniu systemów. W przeciwieństwie do programowania proceduralnego, które koncentruje się na sekwencji instrukcji i oddzieleniu danych od funkcji operujących na nich, OOP wprowadza koncepcję „obiektu” jako centralnej jednostki. Obiekt to spójna struktura, która łączy w sobie zarówno dane (zwane atrybutami lub stanem), jak i operacje (zwane metodami lub zachowaniem), które mogą na tych danych działać.
Ta fundamentalna zmiana podejścia ma kolosalne znaczenie. Pozwala ona na znacznie bardziej intuicyjne modelowanie świata rzeczywistego w kodzie, gdzie byty takie jak „samochód”, „użytkownik” czy „dokument” mogą być reprezentowane jako samodzielne, interaktywne jednostki. Komunikacja między tymi obiektami odbywa się poprzez wywoływanie ich metod, co sprzyja tworzeniu modułowego, łatwiejszego do zrozumienia i utrzymania kodu. Zaletą takiego podejścia jest nie tylko klarowność, ale przede wszystkim możliwość wielokrotnego wykorzystania komponentów. Raz zaprojektowany i przetestowany obiekt może być użyty w różnych częściach aplikacji, a nawet w innych projektach, co znacząco przyspiesza proces deweloperski i redukuje liczbę błędów.
Dzięki swojej elastyczności i skalowalności, programowanie obiektowe stało się podstawą dla wielu popularnych języków, takich jak Java, C++, Python czy C#. Umożliwia ono budowanie skomplikowanych systemów, od zaawansowanych aplikacji desktopowych, przez rozbudowane serwisy internetowe, aż po systemy wbudowane i rozwiązania z zakresu sztucznej inteligencji. Zrozumienie podstawowych zasad i zastosowań OOP jest dziś nieodzowne dla każdego, kto aspiruje do bycia skutecznym i nowoczesnym programistą.
Filary OOP: Cztery Kluczowe Zasady Programowania Obiektowego
Programowanie obiektowe opiera się na czterech podstawowych filarach, które stanowią o jego sile i elastyczności. Są to abstrakcja, enkapsulacja, dziedziczenie i polimorfizm. Głębokie zrozumienie tych zasad jest kluczem do efektywnego wykorzystywania paradygmatu OOP i tworzenia solidnego, rozszerzalnego oprogramowania.
Abstrakcja
Abstrakcja w OOP to proces ukrywania skomplikowanych detali implementacyjnych i prezentowania jedynie niezbędnej funkcjonalności. Jej celem jest umożliwienie programistom skupienia się na tym, co obiekt robi (jego interfejsie), zamiast na tym, jak to robi (jego wewnętrznej implementacji). Dzięki temu, zamiast operować na nisko-poziomowych szczegółach, twórcy oprogramowania mogą pracować na wyższym poziomie koncepcyjnym, co znacznie ułatwia zarządzanie złożonością systemu.
Wyobraźmy sobie interfejs użytkownika telewizora. Użytkownik naciska przycisk „Zmień kanał”, nie zastanawiając się nad tym, jakie dokładnie obwody elektroniczne są aktywowane, aby to nastąpiło. Abstrahuje od tych detali, koncentrując się na efekcie. W OOP, poprzez tworzenie interfejsów (w sensie programistycznym) lub klas abstrakcyjnych, definiujemy zestaw operacji, które obiekt ma udostępniać, bez narzucania, jak dokładnie te operacje mają być zrealizowane. To promuje modułowość i ułatwia tworzenie zaawansowanych systemów, zwiększając zarówno efektywność, jak i elastyczność kodu. Kluczową korzyścią jest to, że zmiany w wewnętrznej implementacji obiektu nie wpływają na inne części systemu, o ile jego zewnętrzny interfejs pozostaje niezmieniony.
Enkapsulacja
Enkapsulacja, często nazywana również hermetyzacją, to zasada polegająca na łączeniu danych (atrybutów) i metod (zachowań) operujących na tych danych w ramach jednej jednostki – obiektu – oraz na ograniczaniu bezpośredniego dostępu do wewnętrznego stanu tego obiektu. Głównym celem enkapsulacji jest ochrona spójności danych obiektu przed nieautoryzowanymi lub niekontrolowanymi modyfikacjami z zewnątrz. Obiekt udostępnia kontrolowany interfejs (publiczne metody), za pomocą którego inne obiekty mogą z nim wchodzić w interakcje.
W praktyce oznacza to, że zmienne członkowskie klasy są zazwyczaj deklarowane jako prywatne (`private` w Javie, C++ czy C#), co uniemożliwia bezpośredni dostęp spoza klasy. Dostęp do nich lub ich modyfikacja odbywa się poprzez publiczne metody dostępu (tzw. „gettery” i „settery”). Dzięki temu twórca klasy może kontrolować, w jaki sposób dane są odczytywane i zapisywane, np. poprzez walidację danych wejściowych w setterach. Enkapsulacja podnosi poziom bezpieczeństwa aplikacji, upraszcza jej utrzymanie i minimalizuje ryzyko wystąpienia błędów wynikających z nieprzewidzianych zmian stanu obiektu. Jest to fundament, który pozwala na ukrywanie szczegółów implementacji, co jest ściśle powiązane z abstrakcją.
Dziedziczenie
Dziedziczenie to jeden z najpotężniejszych mechanizmów w OOP, który umożliwia tworzenie nowych klas (klas pochodnych lub podklas) na bazie już istniejących (klas bazowych lub nadklas). Nowa klasa dziedziczy atrybuty i metody klasy bazowej, co prowadzi do znaczącego wzrostu efektywności i przejrzystości kodu, a przede wszystkim redukuje konieczność jego duplikowania.
Idea dziedziczenia jest prosta: zamiast pisać ten sam kod od nowa dla podobnych obiektów, definiujemy wspólną funkcjonalność w klasie bazowej, a następnie tworzymy bardziej specyficzne podklasy, które rozszerzają lub modyfikują tę funkcjonalność. Na przykład, klasa „Pojazd” może definiować podstawowe cechy takie jak prędkość, paliwo oraz metody takie jak „uruchomSilnik()” czy „przyspiesz()”. Klasy „Samochód”, „Motocykl” czy „Ciężarówka” mogą dziedziczyć z „Pojazdu”, jednocześnie dodając swoje unikalne właściwości (np. liczba drzwi w „Samochodzie”) i specyficzne metody.
Dziedziczenie wspiera hierarchiczną organizację klas, od ogólnych do szczegółowych, co ułatwia zarządzanie skomplikowanymi systemami. Wpływa to na zwiększenie elastyczności aplikacji i wspiera współdzielenie zachowań, ale wymaga przemyślanego planowania. Niewłaściwe zastosowanie dziedziczenia, np. tworzenie zbyt głębokiej lub nieintuicyjnej hierarchii, może prowadzić do wzrostu złożoności i trudności w utrzymaniu kodu (problem „klasy-godzilli” lub „dziedziczenia implementacji”). Dlatego często zaleca się „kompozycję zamiast dziedziczenia” jako alternatywę w pewnych scenariuszach.
Polimorfizm
Polimorfizm, co dosłownie oznacza „wielopostaciowość”, to zasada w OOP umożliwiająca traktowanie obiektów różnych klas w jednolity sposób za pomocą wspólnych interfejsów lub metod. Jest to cecha, która pozwala na wywoływanie tych samych metod na różnych obiektach, a specyficzna implementacja metody jest wybierana dynamicznie w czasie wykonywania programu, w zależności od rzeczywistego typu obiektu.
Najczęściej spotykanymi formami polimorfizmu są:
- Polimorfizm przez dziedziczenie (przesłanianie metod): Klasa pochodna może przesłonić (nadpisać) metodę odziedziczoną z klasy bazowej, zmieniając jej zachowanie. Kiedy obiekt klasy bazowej jest używany jako referencja do obiektu klasy pochodnej, wywołanie tej metody spowoduje wykonanie wersji z klasy pochodnej.
- Polimorfizm przez interfejsy: Różne klasy mogą implementować ten sam interfejs, co oznacza, że muszą implementować wszystkie metody zdefiniowane w tym interfejsie. Pozwala to na traktowanie obiektów tych różnych klas jako obiektów typu interfejsu.
Kontynuując przykład ze „Zwierzęciem”: jeśli mamy klasę bazową „Zwierzę” z metodą „dźwięk()”, to klasy pochodne takie jak „Pies” i „Kot” mogą implementować tę metodę na swój unikalny sposób („hau hau” i „miau miau”). Dzięki polimorfizmowi, możemy stworzyć listę obiektów typu „Zwierzę” (zawierającą psy i koty) i wywołać na każdym z nich metodę „dźwięk()” bez wiedzy o ich dokładnym typie, a program automatycznie zadba o wywołanie odpowiedniej implementacji. Polimorfizm znacząco zwiększa elastyczność i rozszerzalność systemów, ułatwiając dodawanie nowych klas dziedziczących bez konieczności modyfikowania istniejącego kodu, który z nich korzysta. Jest to fundamentalne narzędzie do budowania elastycznych i łatwych w utrzymaniu aplikacji.
Obiekty i Klasy w OOP: Serce Paradygmatu
Zrozumienie kluczowych pojęć, jakimi są klasy i obiekty, jest absolutnie niezbędne do opanowania programowania obiektowego. Te dwie koncepcje stanowią fundament, wokół którego zbudowany jest cały paradygmat OOP. Można je postrzegać jako szablon i jego konkretne zastosowanie, co pozwala na hierarchiczne i modularne podejście do organizacji kodu.
Definicja i Rola Klas
Klasa w programowaniu obiektowym pełni funkcję abstrakcyjnego wzorca, szablonu lub projektu, który określa strukturę i zachowanie obiektów. Nie jest to konkretny byt, lecz raczej definicja tego, co dany typ obiektów będzie reprezentował. Klasy definiują zestaw atrybutów (cech, danych), które obiekty tego typu będą posiadać, oraz metody (funkcje, zachowania), które będą mogły wykonywać.
Na przykład, klasa „Samochód” może definiować atrybuty takie jak `marka`, `model`, `rok_produkcji`, `kolor` oraz metody takie jak `uruchomSilnik()`, `hamuj()`, `przyspiesz()`. Klasa sama w sobie nie jest konkretnym samochodem; jest jedynie schematem, według którego samochody mogą być tworzone. Rolą klas jest więc:
- Definiowanie struktury danych: Określają, jakie dane będą przechowywane przez obiekty.
- Definiowanie zachowań: Wskazują, jakie operacje mogą być wykonywane na tych danych lub przez obiekt.
- Tworzenie abstrakcji: Pozwalają na grupowanie powiązanych danych i funkcji w logiczne jednostki.
- Umożliwianie ponownego użycia kodu: Raz zdefiniowana klasa może być wielokrotnie wykorzystywana do tworzenia wielu obiektów.
- Wspieranie modularności: Dzielą program na mniejsze, zarządzalne części, co ułatwia rozwój i utrzymanie.
W językach programowania, klasa jest de facto typem danych złożonych, który programista sam definiuje.
Instancje Klas jako Obiekty
Obiekt to konkretna instancja klasy. Jest to namacalny, rzeczywisty byt w pamięci komputera, utworzony na podstawie definicji klasy. Podczas gdy klasa jest abstrakcyjnym planem, obiekt jest jego konkretną realizacją. Każdy obiekt posiada swój własny, unikalny stan (wartości atrybutów) i może wykonywać metody zdefiniowane w swojej klasie.
Kontynuując przykład klasy „Samochód”:
- Możemy stworzyć obiekt `mojSamochod` na podstawie klasy „Samochód”, ustawiając mu atrybuty: `marka = „Toyota”`, `model = „Corolla”`, `rok_produkcji = 2020`, `kolor = „Czerwony”`.
- Następnie możemy stworzyć inny obiekt `samochodKlienta` na podstawie tej samej klasy „Samochód”, ale z innymi wartościami: `marka = „Honda”`, `model = „Civic”`, `rok_produkcji = 2022`, `kolor = „Niebieski”`.
Oba obiekty są instancjami tej samej klasy „Samochód”, ale mają odmienny stan. Jednocześnie oba posiadają te same metody, np. `uruchomSilnik()`, które mogą wywołać. Tworzenie obiektów z klas nazywane jest instancjonowaniem. Każdy obiekt ma swój własny cykl życia – jest tworzony (konstruktor), używany, a następnie niszczony (destruktor lub garbage collector), zwalniając zajmowaną pamięć. W ten sposób programowanie obiektowe pozwala na intuicyjne odwzorowanie rzeczywistości w kodzie, poprzez integrację stanu (danych) z zachowaniem (funkcjami) w spójne i zarządzalne jednostki.
Wzorce Projektowe OOP: Sprawdzone Rozwiązania dla Złożonych Systemów
Kiedy programiści zaczęli powszechnie stosować programowanie obiektowe, szybko zauważyli, że pewne problemy projektowe pojawiają się cyklicznie w różnych kontekstach. Zamiast za każdym razem odkrywać koło na nowo, opracowano standardowe, sprawdzone rozwiązania, które nazwano wzorcami projektowymi (design patterns). Wzorce te, skodyfikowane w słynnej książce „Design Patterns: Elements of Reusable Object-Oriented Software” autorstwa „Gang of Four” (GoF) – Erich Gamma, Richard Helm, Ralph Johnson i John Vlissides – stanowią katalog najlepszych praktyk w projektowaniu oprogramowania obiektowego.
Wzorce projektowe nie są gotowymi fragmentami kodu, które wystarczy wkleić. Są to raczej abstrakcyjne szablony, koncepcje rozwiązań, które należy dostosować do specyfiki konkretnego problemu i języka programowania. Ich głównym celem jest:
- Poprawa struktury kodu: Pomagają w tworzeniu bardziej modułowego, elastycznego i łatwiejszego do zrozumienia kodu.
- Zwiększenie ponownego użycia kodu: Dostarczają sprawdzonych schematów, które można zastosować w wielu miejscach.
- Ułatwienie komunikacji między deweloperami: Wzorce stanowią wspólny język i zbiór referencji do typowych rozwiązań.
- Zapewnienie skalowalności i elastyczności: Systemy zbudowane w oparciu o wzorce są łatwiejsze do rozbudowy i modyfikacji.
Wzorce GoF są zazwyczaj klasyfikowane na trzy główne kategorie:
- Wzorce kreacyjne (Creational Patterns): Zajmują się sposobem tworzenia obiektów, abstrakcjonując proces instancjonowania. Przykłady to:
- Singleton: Zapewnia, że klasa ma tylko jedną instancję, i udostępnia globalny punkt dostępu do niej.
- Fabryka Abstrakcyjna (Abstract Factory): Dostarcza interfejs do tworzenia rodzin powiązanych lub zależnych obiektów bez określania ich konkretnych klas.
- Budowniczy (Builder): Oddziela konstrukcję złożonego obiektu od jego reprezentacji, umożliwiając tworzenie różnych reprezentacji przy użyciu tego samego procesu konstruowania.
- Metoda Fabrykująca (Factory Method): Definiuje interfejs do tworzenia obiektu, ale pozwala podklasom decydować, która klasa ma być instancjonowana.
- Wzorce strukturalne (Structural Patterns): Dotyczą kompozycji klas i obiektów, tworząc większe struktury. Przykłady to:
- Adapter: Pozwala obiektom z niekompatybilnymi interfejsami współpracować ze sobą.
- Fasada (Facade): Zapewnia uproszczony interfejs do złożonego podsystemu.
- Dekorator (Decorator): Dynamicznie dodaje nowe funkcjonalności do obiektów bez modyfikowania ich struktury.
- Kompozyt (Composite): Komponuje obiekty w struktury drzewiaste, traktując obiekty indywidualne i kompozycje w jednolity sposób.
- Wzorce behawioralne (Behavioral Patterns): Skupiają się na komunikacji między obiektami i przypisywaniu odpowiedzialności. Przykłady to:
- Obserwator (Observer): Definiuje zależność jeden do wielu między obiektami, tak że gdy jeden obiekt zmienia stan, wszystkie jego zależności są powiadamiane i automatycznie aktualizowane.
- Strategia (Strategy): Definiuje rodzinę algorytmów, hermetyzuje każdy z nich i sprawia, że są one wymienne.
- Polecenie (Command): Hermetyzuje żądanie jako obiekt, pozwalając na parametryzowanie klientów różnymi żądaniami, kolejkowanie lub logowanie żądań.
- Iterator: Zapewnia sposób dostępu do elementów obiektu zbiorczego sekwencyjnie bez ujawniania jego wewnętrznej reprezentacji.
Stosowanie wzorców projektowych jest nieocenione przy tworzeniu skalowalnych, elastycznych i łatwych w utrzymaniu aplikacji. Ich właściwe wdrożenie prowadzi do powstania bardziej przejrzystego i trwałego kodu, znacząco ułatwiając zarządzanie złożonością projektów oprogramowania.
Języki Programowania Obiektowego: Przegląd i Zastosowania
Programowanie obiektowe (OOP) jest wspierane przez szerokie spektrum języków programowania, z których każdy oferuje unikalne cechy i jest dostosowany do różnych potrzeb deweloperskich. Choć podstawowe zasady OOP są wspólne, ich implementacja może się różnić. Poniżej przedstawiamy przegląd najpopularniejszych języków obiektowych.
C++
C++ to jeden z pionierskich i najbardziej wpływowych języków programowania, w pełni wspierających paradygmat obiektowy. Powstały jako rozszerzenie języka C, C++ łączy wysoką wydajność i precyzyjną kontrolę nad sprzętem (odziedziczoną z C) z potężnymi abstrakcjami obiektowymi, takimi jak klasy, dziedziczenie, polimorfizm i enkapsulacja.
Możliwości języka pozwalają programistom na integrowanie kodu i danych w tzw. struktury klasowe. Kluczowe cechy OOP w C++ to:
- Klasy: Pozwalają na definiowanie własnych typów danych, które mogą zawierać atrybuty i metody.
- Dziedziczenie: Umożliwia tworzenie hierarchii klas, co promuje ponowne wykorzystanie kodu i upraszcza zarządzanie dużymi projektami.
- Polimorfizm: Realizowany poprzez funkcje wirtualne i abstrakcyjne klasy, co pozwala na dynamiczne wiązanie wywołań metod w czasie wykonania.
- Enkapsulacja: Kontrolowana za pomocą modyfikatorów dostępu (`public`, `protected`, `private`), które decydują o widoczności składowych klasy.
C++ jest często wybierany do projektów wymagających ekstremalnej wydajności i kontroli nad zasobami, takich jak systemy operacyjne, silniki gier, aplikacje czasu rzeczywistego, systemy wbudowane czy symulacje naukowe. Jego bogaty zestaw funkcji i szerokie wsparcie dla nowoczesnych technik programistycznych czynią go uniwersalnym narzędziem dla profesjonalistów dążących do maksymalizacji wydajności swoich rozwiązań.
Java
Java to czołowy język programowania obiektowego, słynący ze swojej przenośności i niezawodności. Został zaprojektowany z myślą o zasadzie „napisz raz, uruchom wszędzie” (Write Once, Run Anywhere – WORA), co oznacza, że kod Javy, skompilowany do bytecode’u, może być uruchamiany na dowolnej platformie posiadającej wirtualną maszynę Javy (JVM), bez konieczności modyfikacji.
Java jest językiem czysto obiektowym (z drobnymi wyjątkami typów prymitywnych), co oznacza, że niemal wszystko w Javie jest obiektem. Język ten oferuje:
- Solidne wsparcie dla OOP: W pełni implementuje abstrakcję (poprzez interfejsy i klasy abstrakcyjne), enkapsulację (modyfikatory dostępu), dziedziczenie (pojedyncze dziedziczenie implementacji, wielokrotne dziedziczenie interfejsów) i polimorfizm.
- Automatyczne zarządzanie pamięcią: Dzięki wbudowanemu garbage collectorowi programiści nie muszą ręcznie zwalniać pamięci, co zmniejsza ryzyko wycieków pamięci i ułatwia rozwój.
- Bogactwo bibliotek i frameworków: Dostępność rozbudowanych bibliotek (np. Swing, AWT do GUI) i frameworków (np. Spring, Hibernate do aplikacji korporacyjnych) znacząco przyspiesza rozwój.
Java jest szeroko stosowana w aplikacjach webowych (backend), mobilnych (Android), systemach korporacyjnych, Big Data, chmurze i wielu innych. Jest też często wybierana jako pierwszy język programowania w edukacji ze względu na swoją przejrzystość i dojrzałe ekosystem.
Python
Python to niezwykle wszechstronny, interpretowany język programowania, który doskonale wspiera paradygmat obiektowy, choć nie jest językiem „czysto” obiektowym w takim sensie jak Java (pozwala na programowanie proceduralne i funkcyjne). Jego główną zaletą jest wyjątkowa przejrzystość i czytelność kodu, co sprawia, że nauka Pythona jest przystępna nawet dla początkujących.
Kluczowe elementy OOP w Pythonie:
- Klasy i obiekty: Definicje klas są intuicyjne, a instancje tworzy się przez wywołanie konstruktora.
- Atrybuty i metody: Łączą stan (atrybuty) oraz zachowania (metody) w jednym miejscu, ułatwiając zarządzanie złożonymi systemami.
- Dziedziczenie: Wspierane jest wielokrotne dziedziczenie, choć zaleca się ostrożność.
- Polimorfizm: Realizowany poprzez „duck typing” – jeśli obiekt zachowuje się jak kaczka (ma odpowiednie metody), to jest kaczką, niezależnie od jego formalnego typu.
- Enkapsulacja: W Pythonie opiera się bardziej na konwencji niż na ścisłych mechanizmach kontroli dostępu (brak modyfikatorów `private` czy `protected`, choć istnieją konwencje nazewnictwa z użyciem podkreśleń).
Python jest niezwykle popularny w dziedzinach takich jak data science, sztuczna inteligencja, uczenie maszynowe, rozwój webowy (frameworki takie jak Django, Flask), automatyzacja i skrypty systemowe. Jego elastyczność i ogromny ekosystem bibliotek czynią go idealnym wyborem zarówno dla projektów akademickich, jak i komercyjnych rozwiązań technologicznych.
Ruby
Ruby to dynamiczny, w pełni obiektowy język programowania, który kładzie nacisk na prostotę i produktywność programisty. Jego składnia jest elegancka i bardzo intuicyjna, często porównywana do angielskiego, co umożliwia tworzenie klarownego i zwięzłego kodu. Każda wartość w Ruby (nawet liczby) jest obiektem, a wszystkie metody są wirtualne, co czyni go językiem bardzo elastycznym i ułatwiającym metaprogramowanie.
Ruby jest głęboko zakorzenione w filozofii OOP:
- Czysto obiektowy: Wszystko jest obiektem.
- Dziedziczenie i mixiny: Wspiera pojedyncze dziedziczenie klas, ale dla wielokrotnego dziedziczenia zachowań oferuje mechanizm „mixinów” (modułów, które można dołączyć do klasy).
- Dynamiczny polimorfizm: Naturalnie wspierany przez dynamiczne typowanie.
- Enkapsulacja: Realizowana za pomocą słów kluczowych `public`, `protected`, `private`.
Ruby zyskało ogromną popularność dzięki frameworkowi Ruby on Rails, który znacznie przyspieszył rozwój aplikacji internetowych poprzez automatyzację wielu zadań i promowanie konwencji nad konfiguracją. Język ten jest ceniony za swoją elegancję, dużą produktywność i aktywną społeczność.
JavaScript
JavaScript to wszechobecny język programowania, który wspiera paradygmat obiektowy, choć jego model obiektowy jest unikalny i oparty na dziedziczeniu prototypowym, a nie na tradycyjnych klasach (przed ES6). Od wprowadzenia ES6 (ECMAScript 2015), JavaScript zyskał składnię klasową, która choć wygląda jak tradycyjne klasy, jest w rzeczywistości „cukrem składniowym” nad istniejącym modelem prototypowym.
Kluczowe aspekty OOP w JavaScript:
- Dziedziczenie prototypowe: Obiekty mogą bezpośrednio dziedziczyć właściwości i metody z innych obiektów (ich prototypów).
- Składnia klasowa (ES6+): Ułatwia tworzenie obiektów i ich hierarchii w sposób bardziej zbliżony do innych języków OOP.
- Enkapsulacja i polimorfizm: Mogą być wdrażane, choć z pewnymi niuansami wynikającymi z dynamicznego charakteru języka i braku ścisłych modyfikatorów dostępu (dostępne są jednak prywatne pola klas w now

