Jakiś czas temu, przy okazji opisywania podstaw Rails, starałem się wyjaśnić czym są klasy i obiekty. Jednak takie mieszanie poziomów ogólności w jednym wpisie może sprawiać, że całość wyda się mało przystępna dla początkujących i nudna dla obeznanych z tematem. Napiszę więc krótko o programowaniu obiektowym dla wszystkich tych, którzy znają już jakieś inne imperatywne, ale nieobiektowe języki programowania, a chcą dowiedzieć się, czym są obiekty i klasy.
Klasy i obiekty
Zmienne i ich typy
Wiemy, że 31337
to liczba całkowita, a "siała baba mak"
to łańcuch znakowy.
Programując, na przykład w języku C, musimy deklarować, jakiego typu dane
(ang. data type) będą przechowywane pod adresem w pamięci przypisanym do
tworzonej zmiennej. W tym celu korzystamy na przykład z konstrukcji int a =
31337;
, gdzie int
jest nazwą typu, a
zmiennej, zaś 31337
jej początkową
wartością. Podanie typu jest potrzebne kompilatorowi, aby tłumacząc kod na język
maszynowy, wiedział ile przestrzeni powinien zarezerwować na tę „szufladkę”
służącą do przechowywania informacji.
W językach średniego i niskiego poziomu często musimy przejmować się pamięcią zajmowaną przez zmienne. Dzieje się tak dlatego, że im niższego poziomu jest język, tym bliżej mu do maszyny. Oznacza to, że programista musi uwzględniać np. kolejności bajtów w długich liczbach czy dbać, aby po użyciu struktur danych zajmowana przez nie pamięć została zwolniona. W językach wysokiego poziomu zajmuje się tym kompilator lub interpreter.
Zmiennych używa się w instrukcjach i wyrażeniach realizowanych przez program. Gdy programista pisze kod, stara się przenieść jakiś problem z rzeczywistego świata do komputera, aby korzystając z jego mocy obliczeniowej, szybciej go rozwiązać. Jednak mając tylko kilka typów danych, które zostały pomyślane bardziej z myślą o maszynie niż człowieku, trzeba się czasem porządnie nagimnastykować. Na przykład: jeżeli zamierzamy stworzyć program przechowujący dane osobowe, takie jak wiek, imię i nazwisko oraz płeć, to programując na średnim czy niskim poziomie, musimy utworzyć osobną zmienną przechowującą wiek (liczba całkowita o długości jednego bajtu), osobną na łańcuch tekstowy z imieniem i nazwiskiem (tablica bajtów zakończona znakiem o kodzie 0), a osobną na zmienną mówiącą, czy osoba jest płci męskiej czy żeńskiej (zmienna typu boolean o długości jednego bitu, choć w praktyce może być przezroczyście rzutowana na jeden bajt).
W języku C mamy akurat pewne ułatwienie w postaci tzw. struktur – typu danych pozwalającego grupować obiekty innych typów. Dzięki temu program staje się bardziej czytelny i łatwiej zarządzać informacjami, ale wciąż trzeba pamiętać o osobnym przetwarzaniu każdej informacji. W końcu zechcemy pewnie zdefiniować tablicę osób, czyli tak naprawdę tablicę wskaźników do miejsc, w których znajdują się struktury zawierające wspomniane pola. Jeszcze tylko zarządzanie tą tablicą, przydzielanie i zwalnianie pamięci, podstawowe operacje wyszukiwania, dodawania i kasowania… brzmi nieco koszmarnie.
Klasa i obiekt
Ktoś pomyślał kiedyś, że znacznie łatwiej byłoby, gdyby dało się tworzyć własne typy danych i to nie w celu zadowolenia komputera, ale lepszego operowania na abstrakcyjnych informacjach. Języki pozwalające tworzyć takie typy danych nazywa się właśnie językami obiektowymi, ponieważ zamiast zmiennych występują tam obiekty (ang. objects), a zamiast typów klasy (ang. classes). Na obiekty mówi się też egzemplarze klasy lub instancje klasy.
Oczywiście zmienne również istnieją w językach obiektowych, ale często przechowują odwołania do obiektów, a nie bezpośrednio jakieś pojedyncze wartości.
Wyobraźmy sobie, że zamiast typu oznaczającego liczbę całkowitą i osobnego typu
przechowującego pojedyncze znaki możemy skorzystać z typu Osoba
, który pozwala
określić jednocześnie wiek, imię i nazwisko, a także płeć. Oczywiście nie
uświadczymy takiego gotowego typu danych, jednak możemy go samodzielnie
stworzyć.
Żeby zapamiętać do czego służy klasa, wystarczy skojarzyć sobie, że dokonujemy klasyfikacji danych jakiegoś typu, w taki sposób, iż wyróżniamy wspólne dla nich operacje i struktury w pamięci zdolne je pomieścić. Z kolei obiekt można sobie wyobrazić jako coś bardziej namacalnego, co jest tworzone w oparciu o wytyczne zawarte w klasie.
Składowe
Klasa posiada składowe, to znaczy, że podczas jej definiowania określamy z jakich elementów będzie złożona. Część z nich służy do przechowywania danych, a część do operowania na nich.
W większości popularnych, obiektowych języków programowania istnieje operator pozwalający na dostęp do danej składowej klasy – nazywa się on operatorem wyboru składowej lub operatorem kropki. Po nazwie obiektu stawia się kropkę, a po niej podaje nazwę składowej, którą chcemy uzyskać. Jeżeli składową jest na przykład definiowana w klasie funkcja, to dla danego obiektu tej klasy zostanie ona wywołana, a jej wynik zwrócony.
W języku Ruby, podobnie jak w innych językach obiektowych, składowymi są pola i metody.
Pola
Pola (ang. fields), nazywane czasem zmiennymi instancyjnymi (ang. instance variables) lub zmiennymi egzemplarza (bo należą do egzemplarza klasy, czyli do obiektu), są składowymi służącymi do przechowywania danych obiektu.
Jeśli przykładowa klasa Osoba
ma określać ile ktoś ma lat i jakiej jest płci,
to możemy stworzyć odpowiednie pola o wskazujących na zawartość nazwach:
Klasa może mieć tyle pól, ile chcemy. W języku Ruby nie trzeba deklarować, jakich typów będą pola (czy raczej, jakich klas obiektami one będą). Nie musimy nawet ich wprost podawać, opisując klasę.
Pola osadzone są w obiektach. Co to oznacza? Materializowane są dopiero w momencie utworzenia jakiegoś opisywanego za pomocą klasy egzemplarza. Nie można też uzyskać bezpośrednio dostępu do danego pola obiektu, posługując się operatorem wyboru składowej. Wychodzi więc na to, że zmienne instancyjne w języku Ruby są prywatną sprawą obiektów, w których istnieją.
Jeśli w metodzie operującej na obiekcie spróbujemy odczytać jakieś pole, którego
wartość nie została ustawiona, to zwrócony zostanie obiekt nil
.
Zmienne instancyjne obiektów w Rubym charakteryzują się tym, że przed ich
nazwami występuje znak @
, np. @wiek
.
Metody
W językach obiektowych rozwiązano problem polegający na rozdzieleniu specyficznych operacji od danych, których te operacje dotyczą, wprowadzając metody. Metoda (ang. method) to nic innego jak funkcja składowa klasy, która służy do dokonywania operacji na jej obiektach i ma dostęp do innych składowych (zarówno metod, jak i pól).
Ponieważ w języku Ruby zmienne instancyjne widoczne są tylko dla metod klasy,
więc to właśnie metody są jedynym sposobem na to, aby można było odpytać
obiekt o jego właściwości, np. obiekt klasy Osoba
o wiek czy płeć. Podobnie
tworzy się też metody, które będą ustawiały wartości pewnych pól. Oto przykład
klasy zawierającej takie metody:
Taka klasa ma tylko jedną zmienną instancyjną (@wiek
), która powoływana jest
do życia po wywołaniu metody wiek=
. Ta dziwna, zakończona operatorem przypisania
nazwa funkcji ma specjalne znaczenie syntaktyczne. Zostanie ona wywołana za
każdym razem, gdy w programie pojawi się wywołanie tego typu:
Jest ono równoważne zapisowi:
Możemy też stworzyć metodę ustaw_wiek
, jednak wtedy będzie to mniej
przejrzyste w zapisie.
Metody zlokalizowane są w klasie, a nie w obiekcie, chociaż fakt ten jest przed nami ukrywany. Byłoby to marnotrawieniem pamięci, gdyby każdy tworzony obiekt miał na wyposażeniu kopie wszystkich funkcji składowych. Rozwiązano to więc w ten sposób, że w przypadku metod, inaczej niż w przypadku pól, w instancji klasy (czyli w obiekcie) znajdują się tylko odwołania do miejsc w klasie, pod którymi rezydują funkcje składowe.
Można powiedzieć, że metody obiektu są pewnego rodzaju interfejsem pozwalającym mu komunikować się z innymi obiektami.
Akcesory
Wyobraźmy sobie klasę, która ma 5 czy więcej pól, przy czym pierwsze trzy powinno dać się bez przeszkód zapisywać i odczytywać, przedostanie tylko ustawiać, a ostatnie jedynie odczytywać.
W stosunku do poprzedniego przykładu usunąłem słowo kluczowe return
, ponieważ
w Rubym można je stosować, ale nie trzeba – metoda i tak zwraca rezultat
ostatnio wykonywanego wyrażenia. Poza tym użyłem średnika jako separatora, żeby
zmieścić definicję każdej z metod w jednej linii.
Jednak twórcy Ruby’ego pomyśleli o takich częstych operacjach i w związku z tym istnieją słowa kluczowe pozwalające szybciej zdefiniować tzw. akcesory (ang. accessors), w tym settery (metody do ustawiania pól) i gettery (metody do odczytywania ich zawartości). Oto jak można łatwiej zakodować poprzednio zdefiniowane zachowanie:
Skrót attr
bierze się stąd, że pola nazywa się też czasem atrybutami
klasy. Dwukropek przed nazwami pól (podawanymi jako argumenty) zmienia
następujący po nim łańcuch tekstowy w tzw. symbol.
Symbole w Rubym to wydajne łańcuchy znakowe. Są takie, ponieważ dokonywana jest ich internalizacja, która w skrócie polega na przechowywaniu w jednym pamięciowym miejscu łańcuchów, których wartości są takie same.
Słowo kluczowe, np. attr_reader
, jest specjalną metodą wyższego poziomu, która
przyjmuje tablicę symboli i dla każdego z nich definiuje w klasie metodę służącą
do odczytu zmiennej egzemplarza (czyli pola obiektu) o danej nazwie.
Lokalizacja pól i metod
W języku Ruby, podobnie jak w innych językach obiektowych, składowymi są pola i metody. Pola przypisane są do obiektu tworzonego na podstawie klasy, a metody przypisane są do klasy, choć można używać ich w obiektach.
Oczywiście można powiedzieć, że „klasa ma pola”, choć tak naprawdę dopiero metody instancyjne zawierają odwołania do pól obiektu, które powstaną, gdy zostanie on stworzony.
Zapamiętajmy więc ogólną zasadę:
Obiekty służą do przechowywania zmiennych instancyjnych,
a w klasach można przechowywać nie tylko zmienne, ale również metody.
Ta druga część zdania jest prawdziwa dlatego, że w Rubym klasa też jest obiektem, czyli tworząc klasę, tak naprawdę stwarzamy pewien specyficzny egzemplarz, z tym że na wyższym poziomie abstrakcji. Oczywiście nie działa to w drugą stronę i nie każdy obiekt jest klasą.
Życie obiektu
Nie napisałem jeszcze, jak tworzy się nowy obiekt jakiejś klasy. Służy do tego
metoda klasowa new
:
Co się stało? Po pierwsze: po lewej stronie wyrażenia przypisania stworzona
została zmienna o nazwie obiekt
. Będzie ona zawierała referencję, czyli
odniesienie do miejsca w pamięci, pod którym umieszczony zostanie nowo powstały
obiekt klasy Osoba
.
Z kolei po prawej stronie wyrażenia mamy stałą o nazwie Osoba
, która wcześniej
została skojarzona z miejscem w pamięci, w którym rezyduje zaprezentowana
wcześniej definicja klasy.
W języku Ruby pierwotne wskazania na klasy są przechowywane w stałych, a stałe w tym języku można poznać po tym, że ich nazwy zapisujemy zawsze rozpoczynając je wielką literą. Wynika z tego, że nazwy klas też podlegają tej regule.
Po nazwie stałej jest wywołanie metody new
. Powinno to nas zastanowić, bo po
pierwsze nie była tworzona żadna taka metoda, a po drugie nie mamy do czynienia
z obiektem, ale z klasą. Czy to nie jest czasem odwołanie do metody, która
zawsze „siedzi w klasie”? Otóż nie. Wyjaśnię to później, natomiast teraz ważne
jest, aby wiedzieć, że metoda ta tworzy nowy obiekt klasy, dla której ją
wywołano i zwraca referencję wskazującą na ten obiekt. Jak łatwo się domyślić,
wynik powędruje do zmiennej obiekt
, która będzie reprezentowała go
w programie.
Konstruktor
Obiekty posiadają tzw. konstruktory (ang. constructors), czyli metody, które
wykonywane są za każdym razem, gdy instancje zaczynają istnieć. W języku Ruby
konstruktorem jest metoda o nazwie initialize
. Zwykle ustawia się w niej
domyślne wartości pól i wykonuje inne potrzebne rzeczy.
Dla każdego nowo tworzonego obiektu będzie wywołana metoda initialize
. Możemy
przekazać jej argumenty, jeśli tego potrzebujemy, ale nie wywołując jej wprost,
lecz korzystając z tzw. metody klasowej new
, która przekaże podane jej
argumenty właśnie konstruktorowi.
Metody klasowe
Powiem trochę więcej o tej dziwnej i ciekawej zarazem konstrukcji:
Zauważmy, że wywołujemy tutaj metodę klasy, a nie metodę obiektu. Takie
metody nazywamy metodami klasowymi, a w niektórych językach statycznymi
metodami. W definicji klasy Osoba
trudno więc szukać metody new
.
Metoda klasowa przydaje się wtedy, gdy mamy do czynienia z operacjami, które nie
muszą mieć dostępu do pól konkretnych obiektów, jednak działają w odniesieniu do
nowego typu danych. Na przykład w klasie Osoba
mogłaby zostać zdefiniowana
metoda klasowa, która dla każdej osoby, nie ważne jakiej, jest w stanie
stwierdzić płeć na podstawie imienia.
Inne zastosowanie klasowych metod to tworzenie ułatwień obsługi polegających na
dodawaniu własnych słów kluczowych, z których można korzystać podczas
definiowania klasy. Metody klasowe można bowiem wywoływać w jej ciele, a efektem
ich działania może być np. zdefiniowanie metody (tak jak dzieje się
to w przypadku konstrukcji w stylu attr_accessor
).
Z wnętrza metody instancyjnej możemy dobrać się do metody klasowej:
Możemy też skorzystać z niej w bardziej praktyczny sposób, np. aby ustawiać płeć na podstawie imienia, jeśli jeszcze płci nie określono. Dla czytelności stworzę całą klasę.
Metody instancyjne, czyli te które można wywoływać dla obiektów, pamiętane są wewnątrz klasy, gdzie więc zapamiętywane są metody klasowe? Specjalnie dla nich tworzona jest dodatkowa, ukryta przed programistą, dodatkowa klasa zwana w Rubym metaklasą (ang. metaclass) lub klasą własną opisującą inną klasę (ang. singleton class of a class). Jest to wirtualny „towarzysz” każdej klasy, generowany wtedy, gdy pojawi się taka potrzeba. O metaklasach postaram się napisać w kolejnej części.
Wiemy już, czym jest metoda new
, ale nie wiemy kiedy została zdefiniowana.
Zajmiemy się tym później, przy okazji omawiania dziedziczenia, ważnej cechy
języków obiektowych.
Zmienne klasowe
Skoro istnieją klasowe metody, to konsekwentnie korzystać możemy również z klasowych zmiennych (ang. class variables), czyli z atrybutów niezależnych od obiektów, ale związanych z daną klasą. Dzięki nim da się zapamiętywać dane, które są wspólne dla wszystkich egzemplarzy. Na przykład możemy używać zmiennej klasowej, żeby pamiętać liczbę wszystkich ludzi.
W metodzie initialize
wywoływanej za każdym razem, gdy tworzony jest nowy
obiekt, znajduje się wyrażenie, które zwiększa wartość zmiennej klasowej @@suma
o jeden. W związku z tym zawierała ona będzie liczbę odpowiadającą liczbie
stworzonych (a właściwie zainicjowanych) obiektów klasy Osoba
.
Zmienne klasowe (lub innymi słowy pola klasowe) w języku Ruby przechowywane są
w obiekcie klasy, a jak widać na powyższym listingu, aby z nich skorzystać
używa się dwóch znakow @
umieszczonych przed nazwą.
Gdy interpreter języka widzi zapis @@zmienna
bezpośrednio w kontekście klasy,
to odwołuje się do zmiennej o tej nazwie, składowanej w klasie. Jeśli natomiast
konstrukcja taka zostanie wywołana w kontekście instancji (np. w uruchomionej,
korzystając z obiektu, metodzie instancyjnej), to poszukiwana jest klasa tego
konkretnego egzemplarza i tam odnajdywana jest zmienna. Dlatego zmienne klasowe
można zapisywać tak samo w metodach instancyjnych, metodach klasowych i w samej
klasie.
Niektórzy radzą, żeby tam gdzie się da unikać korzystania ze zmiennych klasowych, ponieważ są zaprzeczeniem zasady hermetyzacji.
Zmienne instancyjne klasy
Zmienne instancyjne klasy lub klasowe zmienne instancyjne (ang. class instance variables, class-level instance variables) kojarzą się z jakimś dziwnym wynalazkiem podobnym do świnki morskiej (ni to świnka, ni to morska). Jednak przypominając sobie slogan „w Rubym wszystko jest obiektem”, ma to sens.
Jeżeli klasa jest również obiektem, a więc tak samo, jak każdy obiekt, może mieć własne zmienne instancyjne – taka nazwa może być wieloznaczna, bo mocno zależy od kontekstu, dlatego używa się dopełniacza „klasy”. Jeżeli traktujemy klasę tak jak obiekt, to będą to po prostu zmienne instancyjne tego obiektu, jeśli natomiast mamy na myśli nieco szerszą perspektywę, w tym obiekty tej klasy, to warto dodać, że chodzi o zmienne instancyjne tej klasy, a nie po prostu o zmienne instancyjne (w domyśle: obiektów tej klasy).
Zmienne instancyjne klasy różnią się od zmiennych klasowych tym, że nie można mieć do nich bezpośredniego dostępu w kontekście obiektu (np. w metodzie instancyjnej wywołanej dla obiektu). Widać je tylko w samej klasie i w metodach klasowych.
I jeszcze jeden przykład, który warto pamiętać, ucząc się języka Ruby:
Podsumowanie
Klasa to nowy typ danych, pewien rodzaj wzorca, który jest matrycą dla powoływanych do życia obiektów stworzonych w oparciu o niego. Może ona zawierać składowe, czyli pola i metody, ale tak naprawdę sama klasa nie służy do przechowywania danych – informacje którymi chcemy zarządzać z użyciem klasy zaczynają „żyć” w pamięci dopiero, gdy na podstawie klasy stworzymy obiekt.
Klasa jest czymś subtelnym, niczym idea na zorganizowanie danych. Z kolei obiekt jest materializacją klasy, jej wyrazem w pamięci zajmującym określone miejsce. Można też użyć prostszej analogii, która jest łatwa do zapamiętania:
Klasa to foremka,
a jej instancje to ciastka wypiekane w tej foremce.
Na koniec małe zestawienie elementów programowania obiektowego i ich cech:
Element | Inne nazwy | Zastosowanie | Przykład |
---|---|---|---|
klasa | wzorzec | tworzenie typu danych | class Osoba; end |
metaklasa | klasa własna opisująca klasę, klasa typu singleton opisująca klasę | uzupełnianie właściwości typu danych | class Osoba << self |
obiekt | egzemplarz, instancja | przechowywanie danych | osoba = Osoba.new |
klasa własna obiektu | klasa własna opisująca obiekt, klasa typu singleton opisująca obiekt | uzupełnianie właściwości obiektu | class Osoba; end |
zmienna instancyjna | pole, zmienna egzemplarza | przechowywanie danych obiektu | class Osoba |
zmienna klasowa | pole klasy, zmienna statyczna klasy | przechowywanie danych klasy | class Osoba |
zmienna instancyjna klasy | zmienna metaklasy, pole metaklasy | przechowywanie danych metaklasy | class Osoba |
metoda instancyjna | metoda, funkcja składowa | operowania na polach obiektu | class Osoba |
metoda klasowa | funkcja statyczna, statyczna funkcja składowa, metoda statyczna | operowania na danych niezwiązanych z obiektem | class Osoba |
metoda singletonowa | metoda własna, metoda własna egzemplarza | definiowania operacji dla konkretnego obiektu | cap = Osoba.new |
Element | Opisywany przez… | Dostępny w… | Składowany w… |
---|---|---|---|
klasa | klasę Class i ew. metaklasę | programie | pamięci przeznaczonej na klasy i obiekty |
metaklasa | klasę Class | klasie i programie | pamięci przeznaczonej na klasy i obiekty |
obiekt | klasę i ew. klasę własną | programie | pamięci przeznaczonej na klasy i obiekty |
klasa własna obiektu | klasę | obiekcie i programie | pamięci przeznaczonej na klasy i obiekty |
zmienna instancyjna | – | metodach instancyjnych | obiekcie |
zmienna klasowa | – | klasie i metodach instancyjnych oraz klasowych | klasie |
zmienna instancyjna klasy | – | klasie i metodach klasowych | klasie |
metoda instancyjna | – | obiekcie | klasie |
metoda klasowa | – | klasie | metaklasie |
metoda singletonowa | – | obiekcie | klasie własnej obiektu |