Obiekty w Rubim, cz 1

Posted by Jacek Galanciak on

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