Obiekty w Rubim, cz 1

(10 komentarzy)

W kategoriach: Ruby , Ruby tutorial , Techblog / 30 lipca 2007 [01:19:44]

Tagi technorati:

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ę poprzez NazwaObiektu.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 metody super
  • 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ępu
  • private - tylko dany obiekt może wywołać taką metodę (wykonywana jest w kontekście self, 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 klasy
  • protected - 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 oraz public 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

Komentarze: Skocz na dół, na górę

  1. > duck typing (polskiego tłumaczenia nie ma co przytaczać, gdyż jest wulgarne)

    Hm, nie znałem.. Możesz przytoczyć? ;)

  2. Hyhy, ja też nie znam. Chyba nie ma. Ale w wolnym tłumaczeniu byłoby coś z kaczkami. Tfu, tfu.

  3. „metody klasy można definiować na aż trzy sposoby:” Ogólna postać definicji metod dla klas to: def Klasa.metoda… (w tym przypadku Animal to to samo co self). Dlatego metody klasy można też definiować „od zewnątrz” (def String.foo;end) bez jawnego otwierania klasy. Można też definiować metody per instance: (s=„foo”;def s.blah;end). Metoda blah jest definiowana dla klasy singleton wpinanej do listy (i w drugą stronę pod zmienną attached) przed właściwą klasą String (można więc powiedzieć że nawet klasy są wirtualne). Btw. używanie zmiennych jest raczej odradzane na korzyść zmiennych instancji klasy.

  4. Świetne! :-)

  5. Podkreśliłbym, iż w innych językach zwykło być tak, że metoda prywatna może być wywołana przez inny obiekt tej samej klasy. W Rubym — nie — może być wywołana tylko przez obiekt, na rzecz którego jest wywoływana. ;-)

    A i dopisałbym, że aby wywołać setter z wewnątrz jakiejś metody (tej samej klasy), należy napisać „self.setter = cośtam”, zamiast „setter = cośtam”. W drugim wypadku Ruby uzna, że chceliśmy zadeklarować zmienną lokalną o nazwie „setter”.

  6. mcv, lopex: Wielkie dzięki za uwagi. Uzupełniłem posta o Wasze sugestie :-).

    lopex: O singletonach (a także modułach, miksinach i innych takich) napiszę w przyszłym odcinku, który nadchodzi wielkimi człapami. Cierpliwości ;).

  7. W klasie Animal:
    def hierarchy „Object < Animal”
    end

    Nie powinno być „Animal < Object”?

  8. Hmm. Object jest klasą nadrzędną dla wszystkich, więc powinien w wyliczeniu być najbardziej po lewej stronie. Czyżbym odwrócił kierunek „strzałek” (< i >)? ;)

  9. No właśnie, strzałki czy znaki większości? ;) Wg. Slagella Obiekt jest „większy” od innych klas… ;)

  10. Ależ ja głupi, przecież nawet w składni Rubiego dziedziczenie strzałkuje się tak, jak mówisz. Widocznie walnąłem kierunek, na jaki miałem wtedy ochotę. Ale byłem konsekwentny, nie można mi tego odmówić :-D.

    Dzięki za czujność :).

Dodaj komentarz na temat

Zanim skomentujesz...

W komentarzach działają znaczniki Textile.
Zastrzegam sobie prawo do edycji Twojego komentarza tylko i wyłącznie w celach estetycznych (naprawienie źle wstawionego kodu, itp). Nie zmieniam ich treści, ortografii, interpunkcji. Jeśli odczuwasz potrzebę edycji swojego komentarza, skontaktuj się ze mną, a zdziałamy co trzeba.