Obiektowość
Poprzedni rozdział | Następny rozdział
W dotychczas zaprezentowanym materiale przedstawione zostały te własności języka, które występują również w innych, obecnie używanych językach skryptowych.
Tym co czyni Ruby wyjątkowym językiem, jest silny nacisk jaki jego twórca, czyli Yukihiro Matsumoto położył na obiektowość. Na wstępie wspomniałem, że Ruby jest językiem w pełni obiektowym, co znaczy tyle, że każda wartość, która może być przypisana do zmiennej lub stałej, jest obiektem.
Podstawowe pytanie brzmi zatem: czym są obiekty w Ruby? Wydawać by się mogło, że programowanie zorientowane obiektowo jest pojęciem tak dobrze znanym, że nie wymaga żadnej dalszej klaryfikacji. Tym niemniej w Ruby obiektowość jest rozumiana trochę inaczej niż w pozostałych językach programowania, a w szczególności w najpopularniejszym języku tego rodzaju, czyli Javie.
W tej części przewodnika postaram się przedstawić podstawowe zagadnienia obiektowości w Ruby. Natomiast w części następnej przedstawione zostaną zaawansowane własności języka, które zdecydowanie wyróżniają go na tle innych, popularnych języków programowania.
Obiekty
W Ruby obiektem jest wszystko to co jest wynikiem ewaluacji dowolnego wyrażenia, innymi słowy to, co może pojawić się po prawej stronie instrukcji przypisania. Zasadniczo obiekty posiadają pewien stan, który można utożsamić z wartościami ich atrybutów (zwanych również polami). Wartości atrybutów nie są jednak dostępne bezpośrednio, lecz jedynie poprzez metody, które pozwalają zmieniać stan obiektu. W Ruby, w przeciwieństwie do Javy, czy C++, nie można manipulować bezpośrednio na wartościach atrybutów. Tym niemniej nieraz spotkamy się z wyrażeniami, które z pozoru wyglądają, jakby bezpośrednio modyfikowały wartość jakiegoś atrybutu, np.
person = Person.new person.name = "Aaron" #=> "Aaron" his_name = person.name #=> "Aaron"
Przypisanie pojawiające się w drugiej linii kodu wygląda jakby modyfikowało
wartość atrybutu name
obiektu person
. W czwartej linii
kodu pojawia się natomiast odwołanie do wartości atrybutu name
, które
również może wyglądać jak jego bezpośrednie odczytanie. Tym niemniej wyrażenia to
są de facto wywołaniami metod, która
odpowiednio ustawiają oraz odczytują wartość atrybutu name
obiektu person
. Nie wchodząc w szczegóły implementacji tego mechanizmu
(które omówione są dalej) widzimy, że Ruby pozwala na bardzo wygodne
manipulowanie stanem obiektów, przy jednoczesnym zachowaniu jednego
z podstawowych mechanizmów języków obiektowych, czyli enkapsulacji.
Drugą istotną cechą obiektów jest to, że są one instancjami pewnej klasy. W rzeczywistości problem jest nieco bardziej złożony, niż może to się wydawać na pierwszy rzut oka, ale zostanie on omówiony dopiero w rozdziale zatytułowanym “zagadnienia zaawansowane”. Czym zatem są klasy w Ruby?
Klasy
W poprzednim punkcie wskazaliśmy, że do stanu obiektu można dostać się jedynie poprzez metody. Stwierdzenie to mogłoby prowadzić do mylnego wniosku, że z obiektami bezpośrednio stowarzyszone są metody. Oczywiści nic takiego nie ma miejsca – metody mogą być związane jedynie z klasami, które… również są obiektami! Zatem, aby uniknąć sprzeczności, należy ostatecznie stwierdzić, że z obiektami, które nie są klasami, stowarzyszony jest jedynie stan, ukryty w ich atrybutach. Metody mogą natomiast być związane jedynie z tymi obiektami, które są klasami.
Definiowanie klas
Czy istnieją jeszcze jakieś inne różnice pomiędzy zwykłymi obiektami
a klasami? Otóż tak. Pierwsza z nich dotyczy sposobu tworzenia
tych obiektów. W rozdziale dotyczącym typów danych widzieliśmy, że
chociaż wszystkie obiekty mogą być tworzone za pomocą wyrażenia
NazwaKlasy.new
, to niektóre typy danych, np. łańcuchy mogą być
tworzone w nieco bardziej wygodny i naturalny sposób.
Dla klas sytuacja jest podobna – chociaż możemy tworzyć je wykorzystując
następującą konstrukcję:
Person = Class.new
to język dostarcza konstrukcji pozwalających na znacznie wygodniejsze
definiowanie klas. W szczególności powyższa klasa może być zdefiniowana
z wykorzystaniem słowa kluczowego class
w sposób następujący:
class Person end
Na pierwszy rzut oka definicja ta jest bardziej rozwlekła. Z drugiej jednak strony pozwala na znacznie łatwiejsze definiowanie podstawowych własności klasy, jakie są stowarzyszone z nią metody.
Zanim jednak przystąpimy do omówienia sposobu definiowania metod musimy jeszcze wskazać pozostałe właściwości klasa. Kolejna różnica, którą można było zauważyć już w poprzednich przykładach, dotyczy sposobu tworzenia klas – są one przypisywane do stałych, które w języku Ruby odróżniają się tym od zmiennych, że pierwsza litera ich nazwy jest wielka.
PI = 3.141 pi = 3.141
Zatem w powyższym kodzie występują dwie operacja przypisania, spośród których pierwsza przypisuje wartość do stałej, a druga do zmiennej. Różnica wynika tylko i wyłącznie z tego, że w pierwszej instrukcji nazwa zaczyna się od dużej litery.
Nazwa klasy musi być stałą, zatem w konstrukcji class NazwaKlasy
NazwaKlasy
musi zaczynać się od wielkiej litery. Dzięki temu, po zdefiniowaniu
klasy możemy odwołać się do niej w dowolnym kontekście leksykalnym.
Nie zmienia to jednak faktu, że w Ruby można również tworzyć klasy
anonimowe, wykorzystując właśnie konstrukcję nowa_klasa = Class.new
. Rozwiązanie
takie nie jest zbyt często stosowane, ale może być przydatne, jeśli
nie chcemy zanieczyszczać przestrzeni nazw programu.
Tworzenie obiektów
Kolejna właściwość klas dotyczy klasy, której są one instancjami – jest
to oczywiście klasa Class
. Wśród wielu metod definiowanych
przez tę klasę, jedne zasługuje na szczególną uwagę – metoda new
.
Otóż metoda ta pozwala tworzyć nowe obiekty danej klasy. Metoda
ta jest oczywiście wywoływana jak każda inna metoda – poprzez
operator . (kropka):
class Person end person = Person.new #=> #<Person:0xb7955314>
W powyższym przykładzie najpierw definiowana jest klasa Person
, następnie
tworzony jest obiekt tej klasy, który przypisywany jest do zmiennej person
.
Uwidoczniona domyślna reprezentacja obiektu składa się z nazwy klasy
oraz wewnętrznego, unikalnego identyfikatora obiektu.
Powyższy wywód dokładnie ilustruje to jak rozumiana jest obiektowość
w Ruby – klasy są obiektami, przez co np. sposób tworzenia nowych
obiektów jest “w pełni obiektowy”, tzn. wykorzystywana jest do tego
definiowana w klasie Class
metoda new
.
Nie ma tutaj żadnej magii – tworzenie
nowego obiektu nie różni się (przynajmniej pod względem składni)
od wywołania dowolnej innej metody. To, że przypomina ono znane
np. z Javy new Person()
wynika jedynie z tego, że nazwy klas,
jako stałe muszą zaczynać się od wielkiej litery. Zwodnicze
jest również podobieństwo słowa new
, które w języku Java
jest
słowem kluczowym, a w Ruby jest po prostu nazwą jednej z metod!
Bardzo ważną metodą związaną z new
jest initialize
, która
pozwala zainicjować nowotworzony obiekt. Jest ona wywoływana za każdym
razem, gdy tworzony jest nowy obiekt za pomocą new
. Dzięki temu
możemy w niej ustawić wartości odpowiednich atrybutów tworzonego obiektu.
Jest to znany z języków obiektowych konstruktor, który pozwala uniknąć
błędów wynikających z użycia jakiejś struktury danych, przed inicjalizacją
jej atrybutów.
Metoda initialize
może przyjmować dowolną ilość argumentów, które
przekazywane są do niej z metody new
:
class Person def initialize(name, surname) puts name + " " + surname end end person = Person.new("Franz", "Kafka") # Franz Kafka #=> #<Person:0xb7926690>
W powyższym przykładzie widzimy w jaki sposób obie metody współdziałają
przy tworzenie nowego obiektu. Oczywiste jest, że metoda new
powoduje
wywołanie metody initialize
. Zastanawiające może być jednak to, że
przekazanie niewłaściwej liczby argumentów do metody new
powoduje
wywołanie wyjątku ArgumentError
:
class Person def initialize(name, surname) puts name + " " + surname end end person = Person.new("Franz") # ArgumentError: wrong number of arguments (1 for 2)
Skąd metoda new
, która zdefiniowana jest w klasie Class
może
wiedzieć ile argumentów wymaga metoda initialize
, która zdefiniowana
jest w klasie Person
? Odpowiedź jest prosta – nie wie. Metoda new
zdefiniowana w Class
akceptuje dowolną liczbę argumentów i przekazuje
wszystkie do initialize
. Jeśli ich ilość jest niewłaściwa, to
zgłaszany jest wyjątek.
Z metodą initialize
związana jest jeszcze jedna ciekawa właściwość -
w przeciwieństwie do innych metod, które domyślnie są publiczne, jest
ona metodą prywatną. Co nie zmienia faktu, że można ją upublicznić
i wywoływać na już stworzonych obiektach (nie jest to jednak praktyka
zalecana).
Dziedziczenie
Jedną z zasadniczych cech języków obiektowych jest występowanie mechanizmu
dziedziczenia. Mechanizm ten w założeniu pozwala na wielokrotne,
łatwe użytkowanie tego samego kodu. Jeśli manipulujemy pewną grupą
obiektów wykorzystując do tego identyczne metody, możemy
zgrupować je w pewnej klasie, z której będą dziedziczyć inne klasy.
Typowy przykład pochodzi z graficznych interfejsów użytkownika:
definiowana jest klasa Figure
dostarczająca metod
pozwalających np. na dokonywanie translacji o wektor oraz
kilka dziedziczących z niej klas,
np. Circle
, Triangle
, Square
, z których każda implementuje
metodę draw
rysującą figurę należącą do danej klasy, w specyficzny
dla niej sposób.
W Ruby występuje mechanizm dziedziczenia jednokrotnego. Innymi słowy
każda klasa może dziedziczyć co najwyżej z jednej klasy i jeśli nie podamy
jej explicite, to dziedziczy ona z klasy Object
, czyli korzenia
drzewa dziedziczenia. Aby wskazać, że dana klasa dziedziczy z innej
klasy niż Object
należy w definicji klasy po jej nazwie dodać znak mniejszości
oraz nazwę klasy nadrzędnej.
class Person end
class Player < Person end
W powyższym przykładzie dla klasy Person
nie została jawnie określona
jej klasa nadrzędna, zatem przyjmowana jest klasa Object
. Natomiast
klasa Player
dziedziczy z klasy Person
.
Ponieważ klasy stowarzyszone są z metodami, to dziedzicznie sprowadza się do tego, że w danej klasie dostępne są wszystkie metody zdefiniowane we wszystkich klasach nadrzędnych wobec niej. Jeśli kilka klas w hierarchii dziedziczenia definiuje metodę o tej samej nazwie, to wybierana jest ta metoda, która leży w klasie znajdującej się najbliżej klasy danego obiektu (w szczególności może to być metoda zdefiniowana w tej klasie):
class Person def foo puts "person" end end class Player < Person def foo puts "player" end end p1 = Person.new p1.foo # "person" p2 = Player.new p2.foo # "player"
W powyższym przykładzie zarówno klasa Person
jak i Player
definiuje
metodę foo
. Wywołanie tej metody dla obiektu klasy Person
powoduje
wykorzystanie definicji z klasy Person
, natomiast
wywołanie jej dla obiektu klasy Player
, powoduje wykorzystanie
definicji z klasy Player
.
Aby odwołać się do definicji znajdującej się w klasie nadrzędnej
korzystamy ze słowa kluczowego super
, które powoduje wywołanie
metody o tej samej nazwie, tyle, że w klasie nadrzędnej:
class Person def foo puts "person" end end class Player < Person def foo super puts "player" end end p2 = Player.new p2.foo # "person" # "player"
W powyższym przykładzie w definicji metody foo
znajdującej się
w klasie Player
najpierw wywoływana jest metoda foo
z klasy
nadrzędnej.
Mechanizm jednokrotnego dziedziczenia, pozwala zachować prostą hierarchię dziedziczenia, a także uniknąć tzw. problemu diamentu, pojawiającego się w językach pozwalających na wielokrotne dziedziczenie. Tym niemniej czasami ograniczenia wprowadzane przez jednokrotne dziedziczenie powodują, że kod, który mógłby być wielokrotnie wykorzystany, musi być ponownie implementowany lub przynajmniej muszą istnieć metody delegujące wywołania o identycznej semantyce do innych klas.
Moduły
Rozwiązaniem tego problemu w języku Ruby jest koncepcja modułów, czyli
zbiorów metod, które mogą być dołączane (mixin, wmiksowane) do klas
za pomocą słowa kluczowego include
. Z założenia, metody
definiowane w modułach nie powinny modyfikować stanu obiektu. Mogą
natomiast odwoływać się do metod zdefiniowanych w danej
klasie.
Typowym przykładem wykorzystania modułów jest moduł Comparable
, który
dostarcza metod pozwalających na porównywanie obiektów danej klasy.
Klasa, która chce skorzystać z metod modułu Comparable
musi
definiować jedynie metodę <=>(another)
, która zwraca 0
jeśli porównywany obiekt jest takie same
(w kontekście danego porównania), 1 jeśli dany obiekt jest większy od
porównywanego oraz -1 jeśli jest mniejszy. Jeśli te warunki są spełnione, to
moduł Comparable
dostarcza metod <, >, <=, ==, >=
oraz
between?
.
class Player include Comparable attr_reader :score def initialize(score) @score = score end def <=>(other) self.score <=> other.score end end p1 = Player.new(10) p2 = Player.new(20) p1 < p2 #=> true
W powyższym przykładzie definiowana jest klasa Player
, do której dołączany
jest (za pomocą słowa kluczowego include
) moduł Comparable
. Klasa
ta definiuje metodę <=>(other)
w taki sposób, że porównywane są wartości
atrybutu score
(szczegóły związane z dostępem do atrybutów opisane
są w następnym punkcie). Dzięki temu obiekty klasy Player
mogą być
porównywane np. za pomocą operatora mniejszości, co widzimy w ostatniej
linijce przykładu.
Wyjaśnienie wymaga sytuacja, w której kilka modułów definiuje metodę o tej samej nazwie, bądź klasa nadrzędna definiuje taką samą metodę jak dołączony moduł. W pierwszym przypadku wywołana zostanie metoda zdefiniowana w module, który został dołączony jako ostatni. Podobnie w drugim przypadku – wywołana zostania metoda dołączonego modułu. Zatem można powiedzieć, że dołączone moduły mają pierwszeństwo przed klasami nadrzędnymi, a moduły dołączone później przed modułami dołączonymi wcześniej. Oczywiście jeśli dana klasa również definiuje metodę taką jak dołączony moduł, to metoda zdefiniowana w klasie przesłoni metodę modułu.
Na zakończenie warto dodać, że klasa Class
dziedziczy z klasy Module
(zatem każda klasa jest zarazem modułem), a moduły poza tym, że pozwalają
na dołączanie metod do klas, tworzą również osobną przestrzeń nazw.
Dzięki temu, jeśli tworzymy np. jakąś bibliotekę, która ma być
wykorzystywana w nieznanym środowisku, możemy definicje wszystkich
klas zawrzeć w module (lub modułach) posiadających unikalną nazwę.
Dzięki temu można uniknąć sytuacji, w której definicje dwóch klas posiadających
odmienną semantykę lecz identyczne nazwy, kolidują ze sobą.
Aby odwołać się do klasy, która leży wewnątrz jakiegoś modułu, należy jej nazwę poprzedzić nazwą modułu oraz dwoma dwukropkami:
module Apohllo class Node # definicja klasy end end node = Apohllo::Node.new
W powyższym przykładzie widzimy odwołanie do klasy Node
, która leży
wewnątrz przestrzeni nazw (modułu) Apohllo
.
Atrybuty
Na początku tego rozdziału napisaliśmy, że obiektu posiadają stan, a klasy definiują metody. Dotychczas dosyć szczegółowo opisaliśmy wysokopoziomowe zagadnienia związane z klasami (np. dziedziczenie), ale nie wspomnieliśmy słowem ani o zmianie stanu pojedynczego obiektu, ani nie opisaliśmy dokładnie sposobu definiowania metod. Nadszedł najwyższy czas aby przedstawić te podstawowe zagadnienia.
Atrybuty instancyjne
Atrybuty są własnościami obiektów, które pozwalają na przechowywanie ich stanu. Najbardziej typowym przypadkiem atrybutów, są atrybuty instancyjne, czyli atrybuty związane z obiektem, który nie jest klasą. Atrybuty takie odróżniane są od zmiennych za pomocą pojedynczego zmaku @, który pojawia się przed nazwą atrybutu. Typowy przykład wykorzystania atrybutów przedstawiony jest poniżej:
class Person def initialize(name, surname) @name = name @surname = surname end def name @name end def surname @surname end end p1 = Person.new("Frank","Sinatra") p1.name #=> "Frank" p1.surname #=> "Sinatra"
W powyższym przykładzie definiowana jest klasa Person
, w której konstruktorze
inicjalizowane są dwie zmienne klasowe @name
oraz @surname
, na podstawie
parametrów przekazanych do konstruktora. Ponadto definiowane są dwie metody
name
oraz surname
, które zwracają wartości tych atrybutów.
Jak widzimy w powyższym przykładzie wykorzystane atrybuty nie muszą być
nigdzie deklarowane (jak to ma miejsce np. w Javie).
Standardowo wszystkie niezainicjowane atrybuty posiadają wartość nil
, zatem
nie musimy obawiać się, że odwołanie do atrybutu, do którego nie przypisano
wcześniej żadnej wartości skutkuje pojawieniem się błędu NameError
.
Definiowanie metod pozwalających na odczytanie lub zapisanie wartości
atrybutów instancyjnych jest zjawiskiem tak powszechnym, że twórca języka
Ruby czyli Matz, uznał za stosowne wprowadzenie pewnych pomocniczych metod, które
pozwoliłyby na realizację tego zadania w sposób bardziej zwięzły.
Metody o których mowa (zwane również makrami) to attr_reader, attr_writer
,
attr_accessor
oraz attr
.
Każda z nich ma nieco inną semantykę, ale skoncentrujemy
się na trzech pierwszych, gdyż wywołania ostatniej mogą być z powodzeniem
zastąpione wywołaniami jednej z trzech pierwszych.
Metoda attr_reader
akceptuje listę symboli i dla każdego z nich tworzy
metody pozwalające na odczytanie wartości atrybutu o takiej samej
nazwie jak nazwa symbolu. Metoda attr_writer
ma podobną składnię,
z tą różnicą, że tworzy metody pozwalające na ustawienie wartości
odpowiednich atrybutów. Ostatnia metoda jest jakby sumą dwóch wcześniej
opisanych metod, gdyż tworzy metody pozwalające zarówno na odczytanie,
jak i ustawienie wartości odpowiednich atrybutów.
class Program attr_reader :java_code attr_writer :perl_code attr_accessor :ruby_code def initialize(java) @java_code = java end def run puts "In Ruby" eval "#{@ruby_code}" puts "In Perl" Kernel.system "perl -e '#{@perl_code}'" end end hello_world = Program.new(<<-END class Hello { public static void main(String[] args){ System.out.println("Hello world!"); } } END ) hello_world.java_code = "class Nothing{}" # NoMethodError: undefined method `java_code=' for... hello_world.perl_code = 'print "Hello world!\n"' hello_world.ruby_code = "puts 'Hello world!'" puts hello_world.java_code #class Hello { # public static void main(String[] args){ # System.out.println("Hello world!"); # } #} puts hello_world.perl_code # NoMethodError: undefined method `perl_code' for #<Program:0xb789be8c> puts hello_world.ruby_code # puts 'Hello world!' hello_world.run # In Ruby # Hello world! # In Perl # Hello world!
W powyższym (nieco zagmatwanym) przykładzie definiujemy klasę
Program
, która potrafi reprezentować kod programu w językach: Perl,
Ruby oraz Java. Ponieważ praca z Javą powoduje czasami powstawanie
olbrzymich ilości kodu, który trudno jest pielęgnować uznaliśmy, że
kod w Javie będzie tylko do odczytu. Z kolei Perl czasami zwany
jest “write only languge” (język tylko do zapisu), dlatego też
kod w Perlu może być w naszym programie tylko zapisywany, ale
nie odczytywany. Na koniec zaś – kod w Ruby może być zarówno
odczytywany jak i zapisywany. Metoda run
pozwala uruchomić
kod zapisany w Ruby za pomocą wywołania eval
oraz kod
zapisany w Perlu, za pomocą wywołania systemowego.
Atrybuty klasowe
Drugi typ atrybutów, który często spotykany jest w obiektowych
językach programowania to tzw. atrybuty klasowe. W przybliżeniu
można powiedzieć, że atrybuty te są dostępne dla wszystkich obiektów
danej klasy, ale stowarzyszone są z tylko z klasą. Tym niemniej błędne
byłoby przypuszczenie, że są to atrybuty instancyjne obiektu klasy Class
(czyli obiektu danej klasy). Sprawa jest nieco zagmatwana i zostanie
wyjaśniona w następnym rozdziale. To co należy zapamiętać o zmiennych
klasowych w języku Ruby, to fakt, że są one współdzielone przez
wszystkie klasy w hierarchii dziedziczenia.
Do zmiennych klasowych odwołujemy się poprzedzając ich nazwę dwoma znakami @@, np.
class Figure @@instances = 0 def initialize(shape) @shape = shape @@instances += 1 @id = @@instances end def to_s "[#{@id}] #{@shape}" end end f1 = Figure.new("oval") puts f1 # [1] oval f2 = Figure.new("triangle") puts f2 # [2] triangle
W powyższym przykładzie definiujemy klasę Figure
, która posiada zmienną
klasową @@instances
, w której przechowywana jest liczba instancji klasy
Figure
. Klasa ta definiuje również metodę to_s
, która wywoływana jest
w momencie, gdy potrzebna jest reprezentacja obiektu w postaci łańcucha
znaków. Widzimy, że pierwsza stworzona figura ma identyfikator o
wartości 1, natomiast druga – o wartości 2. Nieco bardziej interesujący
jest przykład następny:
class Figure @@instances = 0 def initialize @@instances += 1 @id = @@instances end def to_s "[#{@id}] " + self.class.to_s end end class Oval < Figure @@instances = 0 end class Triangle < Figure @@instances = 0 end oval = Oval.new puts oval # [1] Oval triangle = Triangle.new puts triangle # [2] Triangle
W powyższym przykładzie definiowane są trzy klasy: Figure, Oval
i Triangle
.
W klasie Figure
definiowana jest zmienna klasowa @@instances
, która,
podobnie jak w poprzednim przykładzie, służy do inicjowania identyfikatorów
obiektów. Ciekawe jest jednak to, że pomimo ponownego zdefiniowania
tej zmiennej w klasach potomnych, we wszystkich obiektach tych klas
dostępna jest tylko jedna zmienna klasowa. Widzimy wyraźnie, że
obiekty oval
i triangle
należą do innych klas, a mimo to współdzielą
zmienną klasową @@instances
. Ta własność Ruby przez niektórych uznawana
jest jako mało intuicyjną. Trudno też wyjaśnić dokładnie z jakimi
obiektami stowarzyszone są zmienne klasowe.
Metody
Metody, obok atrybutów, stanowią podstawowe instrumentarium języków zorientowanych obiektowo. Chociaż w tym rozdziale pojawiły się już definicje wielu metod, to nie poświęcono im dotychczas szczególnej uwagi. W tym punkcie przedstawimy zatem podstawowe informacje dotyczące metod: ich definiowania, widoczności, etc.
Metody instancyjne
Definicje metod, podobnie jak definicje funkcji, rozpoczynają się słowem
kluczowym def
, po którym następuje nazwa metody oraz lista jej parametrów.
Jedyna różnica w definicji metody w stosunku do definicji funkcji polega
na tym, że definicja tej pierwszej pojawia się wewnątrz definicji klasy:
def function(param) puts param end class Example def method(param) puts param end end
Wywołanie metody, w przeciwieństwie do wywołania funkcji może być dokonywane
tylko na rzecz określonego obiektu. Obiekt ten dostępny jest w ciele metody
poprzez słowo kluczowe self
. W rzeczywistości w funkcjach pod tym słowem
również kryje się pewien obiekt, ale nie pojawia się on w wywołaniach funkcji:
def function self end class Example def method self end end function #=> main obj1 = Example.new obj2 = Example.new obj1.method #=> #<Example:0xb79ae310> obj2.method #=> #<Example:0xb79ac254>
W powyższym przykładzie w metodzie method
widnieje odwołanie do obiektu,
na rzecz którego metoda ta jest wywoływana. Widzimy zatem, że metoda
method
wywołana na rzecz obiektów obj1
oraz obj2
zwraca różne wartości,
zatem w dwóch jej wywołaniach pod słowem kluczowym self
ukrywa się inny obiekt.
Poza dostępem do obiektu, na rzecz którego wywołana jest dana metoda, w jej definicji mogą się również bezpośrednio pojawić atrybuty instancyjne danego obiektu, a także atrybuty klasowe klasy, której jest on egzemplarzem (lub jednej z klas nadrzędnych wobec niej). Własności te były już zaprezentowane przy omawianiu atrybutów.
Metody klasowe
Oczywiście wszystkie wymienione własności odnoszą się do metod instancyjnych zdefiniowanych w danej klasie. Obok nich, podobnie jak w przypadku atrybutów, mogą występować metody klasowe, które mogą być definiowane aż na 3 różne sposoby. W tym miejscu zaprezentujemy tylko dwa z nich, ponieważ wprowadzenie trzeciego sposobu (który nota bene dosyć często pojawia się w kodzie), musi być uzupełnione pewnymi dodatkowymi uwagami, które zostaną przedstawione w rozdziale następnym.
Pierwszy sposób definiowania metod klasowych polega na poprzedzeniu nazwy metody nazwą klasy zakończonej kropką:
class Figure @@instances = 0 def Figure.instances @@instances end end
Drugi sposób polega na poprzedzeniu nazwy metody słowem kluczowym self
:
class Figure @@instances = 0 def self.instances @@instances end end
W powyższych przykładach definiowana jest metoda klasowa Figure.instances
,
która zwraca wartość zmiennej klasowej @@instances
. W rzeczywistości
obie definicje są tożsame, gdyż Figure
oraz self
odnoszą się do tego
samego obiektu – obiektu klasy Class
, czyli definiowanej klasy. Przypomnijmy -
zdefiniowanie klasy powoduje utworzenie obiektu klasy Class
, który automatycznie
przypisywany jest do stałej takiej samej jak nazwa definiowanej klasy.
Metody specjalne
Ciekawą własnością języka Ruby jest to, że w nazwach metod mogą pojawiać się znaki specjalne !,? a także operatory +, -, =, etc. Ta druga cecha obecna jest np. w języku C++, tym niemniej definiowanie operatorów w Ruby jest znacznie przyjemniejsze. Natomiast znaki specjalne ! i ?, choć nie wpływają w żaden sposób na konstrukcję wyrażeń (operatory mogą być wywoływane bez kropki występującej pomiędzy obiektem, a nazwą wywoływanej metody), to jednak, ze względu na konwencje stosowane w języku, również pozwalają poprawić czytelność kodu. Konwencja ta zakłada, że metody zakończone znakiem zapytania to predykaty, czyli metody zwracające wartości logiczne. Z kolei wykrzyknik na końcu metody oznacza, że jest ona potencjalnie niebezpieczną wersją metody o takiej samej nazwie, lecz bez wykrzyknika. Tę konwencję mogliśmy zaobserwować przy omówieniu podstawowych typów język Ruby. (Więcej na temat tej konwencji można przeczytać na Blogu Radarka )
class Mind INNATE_IDEAS = [ "God exists", "2 + 2 = 4", "I think therefore I am" ] def initialize @ideas = [] end def any_ideas? !@ideas.empty? end def brain_wash! @ideas.clear end def say_something @ideas[rand(@ideas.size)].to_s end def <<(idea) @ideas << idea end def think(ideas) @ideas += ideas end def Mind.new_idea Idea.new(INNATE_IDEAS[rand(3)]) end end class Idea attr_reader :verbal_expression def initialize(content) @verbal_expression = content end def +(other) Idea.new(@verbal_expression + "." + other.verbal_expression) end def to_ary @verbal_expression.split(".").collect{|ve| Idea.new(ve)} end def to_s @verbal_expression end end cartesian_mind = Mind.new cartesian_mind.any_ideas? #=> false cartesian_mind << Mind.new_idea cartesian_mind.any_ideas? #=> true cartesian_mind.say_something #=> "I think therefore I am" cartesina_mind.brain_wash! cartesian_mind.any_ideas? #=> false stupid_idea1 = Idea.new("Pineal gland is the seat of the soul") stupid_idea2 = Idea.new("Animals are machines") cartesian_mind.think(stupid_idea1 + stupid_idea2) cartesian_mind.say_something #=> "Pineal gland is the seat of the soul"
W powyższym przykładzie można zaobserwować w jaki sposób metody operatorowe oraz
metody zakończone znakami specjalnymi poprawiają czytelność kodu. Zdefiniowane zostały
dwie klasy Mind
oraz Idea
. W pierwszej klasie metoda any_idea?
wartość true
,
jeśli w umyśle znajdują się jakieś myśli, natomiast brain_wash!
powoduje, że
wszystkie myśli są usuwane (niestety nie jest to najlepszy przykład użycia wykrzyknika…).
Ponadto dla umysłu został zdefiniowany operator <<
, który
pozwala dodawać do niego nowe myśli.
W klasie Idea
pojawiają się natomiast dwie dodatkowe metody to_s
oraz to_ary
.
Metody zaczynające się na to_
stosowane są do konwertowania obiektów jednych
klas na obiekty innych klas. W pierwszy wypadku obiekt zamieniany jest na łańcuch
znaków, a metoda ta jest standardowo używana do wyświetlania tekstowej reprezentacji
danego obiektu. Dlatego też, pomimo tego, że w umyśle przechowywane są obiekty
klasy Idea
, metoda say_something
powoduje wyświetlenie odpowiedniego łańcucha znaków.
Metoda to_ary
pozwala natomiast przekształcić obiekt danej klasy w tablicę – bez
jej zdefiniowania niemożliwe byłoby przekazanie do metody think
sumy dwóch myśli.
Dostępność metod
Jedną z ważnych własności języków zorientowanych obiektowo jest możliwość ukrywania implementacji pod ściśle określonym interfejsem, za pomocą którego można zmieniać stan obiektu. Jak pokazaliśmy wcześniej – dostęp do atrybutów danego obiektu odbywa się za pomocą metod o nazwie takiej jak nazwa atrybutu. Ukrywanie atrybutów nie pokrywa jednak w pełni problemu ukrywania implementacji – często w trakcie rozwoju danej klasy pojawiają się w niej metody pomocnicze, które nie powinny być udostępniane w jej publicznym interfejsie. Z tego też względu w językach zorientowanych obiektowo pojawiają się metody o różnym dostępie, najczęściej: publiczne, chronione i prywatne. W Ruby również można specyfikować dostępność danej metody, ale semantyka poszczególnych trybów jest nieco inna niż np. w Javie.
Przypomnijmy – Ruby jest językiem zorientowanym obiektowo, zatem tym podstawowym kontekstem,
wobec którego określa się semantykę różnych konstrukcji języka są obiekty, a nie np. klasy.
Dlatego też dostępność metod określana jest w kontekście obiektów. I tak: metody prywatne to
metody, które mogą być wywoływane tylko przez dany obiekt na rzecz samego siebie.
Innymi słowy są one tylko i wyłącznie wywoływane na rzecz obiektu, który kryje się
pod słowem kluczowym self
. Nie ma jednak znaczenia, gdzie zdefiniowana jest dana metoda!
W przeciwieństwie do Javy, w Ruby metody prywatne mogą być zatem wywoływane również
w metodach zdefiniowanych w klasach potomnych. Z drugiej strony – obiekty należące do
tej samej klasy nie mogę wywoływać swoich metod prywatnych.
Metody chronione to metody, które mogą być wywoływane przez obiekty tej samej klasy oraz jej klas potomnych. Metody publiczne mogą być zaś wywoływane w dowolny kontekście, czy to przez obiekty innych klas, czy też z głównego poziomu programu, byleby określony był obiekt, na rzecz którego wywoływana jest dana metoda.
Dostępność metod określana jest za pomocą słów kluczowych private
, protected
oraz public
.
Domyślnie jeśli w definicji klasy nie określimy dostępności metody, to jest ona publiczna.
Jeśli zaś chcemy zawęzić jej dostępność, możemy skorzystać z dwóch sposobów.
Pierwszy polega na użyciu specyfikatora dostępu (np. private
), po którym następuje
lista symboli odpowiadających nazwom metod prywatnych. Istotne jest to, że definicje
metod, które pojawiają się na liście muszą pojawić się przed specyfikatorem dostępu.
Drugi sposób polega na użyciu
specyfikatora dostępu bez listy argumentów – wtedy wszystkie metody, aż do następnego
specyfikatora (lub końca definicji klasy) otrzymają określony dostęp.
class Accessible # public method def foo end # private methods def fus end private :fus private def bar end def baz end # protected methods protected def dip end def dup end end
W definicji powyższej klasy tylko metoda foo
jest publiczna. Metody fus
, bar
i baz
są
prywatne, natomiast metody dip
i dup
są chronione.
TODO: więcej przykładów
Podsumowanie
W niniejszym rozdziale przedstawiliśmy podstawowe własności obiektowe języka Ruby. Omówiona została ogólna filozofia stojąca za takimi pojęciami jak obiekty i klasy, przedstawione zostały też podstawowe mechanizmy języków zorientowanych obiektowo takie jak dziedziczenie, czy ukrywanie implementacji.
Jednakże język Ruby oferuje znacznie więcej ponad to, co zostało przedstawione w tym rozdziale. Właśnie te zaawansowane własności powodują, że zdobywa on tak dużą popularność. Najważniejsze z nich zostały omówione w rozdziale zatytułowanym Zagadnienia zaawansowane.
Poprzedni rozdział | Następny rozdział