Obiekty w Rubim, cz 1
Posted by Jacek Galanciak on Jul 30 2007
Ta część tutoriala omówi w ekspresowym tempie podstawy definiowania klas i metod.
Teorię programowania obiektowego każdy już zapewne zna, przejdziemy więc od razu do konkretów: jak to się robi w Rubim?
Klasy
Spójrzmy na poniższy kod:
class Animal
@@count = 0 # (0)
def initialize(name, sound) # (1)
@name, @sound, @state = name, sound, "sitting"
@@count += 1 # (0)
end
def growl
puts @sound
end
def hierarchy
"Object > Animal"
end
end
class Wolf < Animal # (3)
def initialize(name, sound, color = "white")
super(name, sound) # (4)
@color = color
end
def hierarchy
super + " > Wolf" # (5)
end
end
burek = Wolf.new("Burek", "wrrrrrr") # (2)
Garść wyjaśnień:
- zmienne klasy (0) są zmiennymi, które nie należą do konkretnego obiektu (egzemplarza) klasy, są dla wszystkich obiektów danej klasy wspólne; odwołujemy się do nich, poprzedzając ich nazwę podwójną małpą (
@@
) - konstruktory realizuje się metodą o nazwie
initialize
(1), natomiast wywołuje się poprzezNazwaObiektu.new(parametry)
- dziedziczenie (3) realizujemy poprzez konstrukcję
NazwaKlasy < NazwaKlasyNadrzednej
; w Ru3bim istnieje jedynie dziedziczenie jednobazowe, ale tzw. miksiny (o których napiszę w następnej cześci) umożliwiają bardziej czytelny i łatwiejszy do zapanowania odpowiednik dziedziczenia wielobazowego super
(4) umożliwia wywołanie metody klasy nadrzędnej; wywołany bez parametrów przyjmuje takie, jakie otrzymała metoda podrzędna; w naszym przypadku, dla klasy wilka, dochodzi jeden dodatkowy parametr, stąd konieczność ręcznego podania parametrów dla metodysuper
- warto pamiętać, że super jest metodą, a więc może zwracać wartość - można go więc stosować tak, jak w (5)
getAttribute(), setAttribute(Attr attr) - won!
Nigdy nie używajmy takich konstrukcji - zamiast tego stosujmy piękne, rubinowe odpowiedniki. Jak zdefiniować settery i gettery w Rubim?
class Animal
# ...
def name=(name) # set
@name = name
end
def name # get
@name
end
end
burek = Wolf.new("Burek", "wrrrrrr")
puts burek.name
burek.name = "Szarik"
puts burek.name
Można jeszcze krócej. I ładniej przy okazji. Zastąpmy te dwie metody dwiema linijkami kodu:
class Animal
# ...
attr_reader :name # get
attr_writer :name # set
end
Gdy mamy pewność, że potrzebujemy jednocześnie settera i gettera dla danego atrybutu, możemy powyższą deklarację skrócić do jednej linijki:
attr_accessor :name
Ups…
Settery i gettery mogą powodować błędy, które czasem trudno wykryć. Nie jest to spowodowane niedociągnięciami języka, a zwykłą nieuwagą programisty. Chcąc skorzystać z settera/gettera wewnątrz klasy, zawsze używajmy self
. Chcąc zmienić atrybut name
, nigdy nie piszemy name = "newname"
, tylko zawsze self.name = "newname"
. W pierwszym przypadku zmieniamy nie atrybut, a zwykłą zmienną lokalną, czego przecież nie chcemy. Sytuacja wygląda zupełnie analogicznie w przypadku getterów.
Duck typing
Podobnie jak inne dynamiczne języki obiektowe, Ruby umożliwia tzn. duck typing (polskiego tłumaczenia nie ma co przytaczać, gdyż jest wulgarne). Krótko mówiąc: jeśli coś chodzi jak kaczka i kwacze jak kaczka, może byc traktowane jak kaczka. Dopiszmy do kodu z klasą zwierzęcia i wilka parę linijek:
class Animal
# ...
end
class Wolf < Animal
# ...
end
class Daemon
def growl
puts "666!"
end
end
arr = [Wolf.new("Burek", "wrrrrrrr"), Daemon.new]
arr.each { |a| a.growl }
wrrrrrrr
666!
Duck typing daje programiście wspaniały prezent - nie musi się babrać metodami wirtualnymi i polimorfizmem ani martwić się o interfejsy - zamiast tego otrzymuje te same możliwości, do których sięga się prostą i przejrzystą składnią.
Metody klasy (class methods)
Dotychczas zajmowaliśmy się metodami obiektu, egzemplarza (instance methods). Ich wywołanie było ściśle uzależnione od konkretnego obiektu i tego właśnie obiektu dotyczyło. Czasem jednak zachodzi potrzeba wywołania metody, która nie jest w żaden sposób uzależniona od jakiegokolwiek obiektu. Przykładem jest IO.read("plik.txt")
(czytanie z pliku bez konieczności tworzenia obiektu klasy IO
) lub…
class Animal
# ...
def Animal.population
@@count
end
end
Proste puts Animal.population
wyświetli ilość egzemplarzy klasy Animal lub potomnych, które zostały stworzone w programie.
Kontrola dostępu
…czyli private
, public
i protected
. W skrócie:
public
- metodę może wywołać każdy, bez ograniczeń dostępuprivate
- tylko dany obiekt może wywołać taką metodę (wykonywana jest w kontekścieself
, czyli na sobie samym); uwaga: w innych językach jest inaczej - tam bowiem metodę prywatną może wywołać również inny obiekt tej samej klasyprotected
- metodę może wywołać tylko obiekt danej klasy lub klasy potomnej
Jak to skodzić?
Można na dwa sposoby: klasycznie lub… inaczej :-) [jak to nazwać?].
class TestClass
public # każda metoda (poza initialize, która jest prywatna)
# jest domyślnie publiczna
def pub_meth1
end
def pub_meth2
end
private
def priv_meth1
end
protected
def prot_meth1
end
def prot_meth2
end
end
class TestClass
def pub_meth1
end
def pub_meth2
end
def priv_meth1
end
def prot_meth1
end
def prot_meth2
end
public :pub_meth1, :pub_meth2
private :priv_meth1
protected :prot_meth1, :prot_meth2
end
Który sposób wybierasz?
Parę słów o parametrach metod
Podstawy programowania obiektowego w Rubim mamy za sobą. Skupmy się teraz na nieco bardziej zaawansowanej składni przy definicjach metod.
Zmienna liczba argumentów
Argumenty, których ilości nie potrafimy przewidzieć, są zwyczajną tablicą, która pozwala nam na wygodne nimi manipulowanie. Tablicę znakujemy gwiazdką i występuje ona jako ostatni argument metody.
def test1(a, b, *c)
print a, b
c.each { |arg| print arg }
end
test1(1, 2, 4, 6, 8)
Zauważmy, że wywołanie test1(1, 2)
nie zwróci żadnego błędu. Na wyjściu otrzymamy 12. Wygodnie i bezpiecznie :-).
Wywołać blok bez yielda
Czasem możemy potrzebować mieć dostęp do bloku jako obiektu. yield
nam w tym nie pomoże. Ale są inne sposoby. Argument, który jest blokiem oznaczamy ampersandem (&), a przekazujemy go na wszystkim znany sposób:
def test2(a, b, &c)
print a, b
if block_given?
puts "", c.class
c.call
end
end
test2(1, 2) do
puts "Jestem w bloku!"
end
Jedno i drugie
Coś dla ludzi o wysokich wymaganiach:
def test3(a, b, *c, &d)
print a, b
c.each { |arg| print arg }
if block_given?
puts "", d.class
d.call
end
end
test3(1, 2, 4, 6, 8) do
puts "Jestem w bloku!"
end
Czy wiesz, że…
private
,protected
orazpublic
w obu formach to metody (w pierwszej formie informują, że następne definiowane metody będą miały określony dostęp, w drugiej - dynamicznie zmieniają dostęp metody na zadany)
poniższy kod wykona się poprawnie:
class TestClass
puts "Test!"
end
Można tutaj Rubiemu zarzucać małą restrykcyjność, która owocuje burdelem w kodzie. Nic bardziej mylnego, czego dowodzą metody kontroli dostępu (public
…) a także wspaniałe konstrukcje, które znamy z Rails (np. has_many :comments
lub validates_presence_of :login
)
- metody klasy można definiować na aż trzy sposoby:
class Animal
# ...
# to już znamy
# tak definiować możemy zarówno wewnątrz, jak i na zewnątrz klasy:
def Animal.population
@@count
end
# można też tak:
def self.population
@@count
end
# oraz tak: (przydatne przy grupowaniu metod klas)
class <<self
def population
@@count
end
end
end