Wprowadzenie do języka Ruby, cz. 3

Dziedziczenie

Fraktalne dłonie

W niedawno opublikowanym artykule pisałem o klasach i obiektach w Rubym. Jednak nie wyjaśniłem tam mechanizmu tzw. dziedziczenia, które jest jednym z fundamentalnych koncepcji programowania obiektowego.

Dziedziczenie

Co to jest i do czego służy dziedziczenie (ang. inheritance)? Przypomnijmy sobie do czego służą klasy. Są to w skrócie nowe typy danych, które sami tworzymy. Możemy w nich określać, z jakich danych będą się składały obiekty klas, a także jakie operacje na tych danych będzie można wykonywać.

Programowanie bez dziedziczenia

Wyobraźmy sobie, że mamy już stworzony nowy typ danych określony klasą Osoba:

Przykład tworzenia klasy
class Osoba
  attr_accessor :wiek, :imie, :plec
 
  def pokaz
    puts "#{@imie} ma #{@wiek} lat i jest #{@plec ? 'kobieta' : 'mezczyzna' }"
  end
end
class Osoba attr_accessor :wiek, :imie, :plec def pokaz puts "#{@imie} ma #{@wiek} lat i jest #{@plec ? 'kobieta' : 'mezczyzna' }" end end

Zauważymy, że jest tam sześć akcesorów (trzy służące do zapisu, a trzy do odczytu), czyli metod, których funkcja polega na ustawianiu bądź odczytywaniu zawartości zmiennych instancyjnych o nazwach takich jak one. Naprawdę klasa ta wygląda więc tak:

Przykład tworzenia klasy z ręcznie zdefiniowanymi akcesorami
class Osoba
  def wiek; @wiek end
  def wiek=(x); @wiek = x end
  def imie; @imie end
  def imie=(x); @imie = x end
  def plec; @plec end
  def plec=(x); @plec = x end
  
  def pokaz
    puts "#{@imie} ma #{@wiek} lat i jest #{@plec ? 'kobieta' : 'mezczyzna' }"
  end
end
class Osoba def wiek; @wiek end def wiek=(x); @wiek = x end def imie; @imie end def imie=(x); @imie = x end def plec; @plec end def plec=(x); @plec = x end def pokaz puts "#{@imie} ma #{@wiek} lat i jest #{@plec ? 'kobieta' : 'mezczyzna' }" end end

Wyobraźmy sobie teraz, że ktoś nas odwiedza i prosi o rozszerzenie programu o możliwość operowania na informacjach o studentach. Zmiana ma polegać na dodaniu obsługi następujących danych: imię, wiek, płeć i kierunek studiów. Możemy wtedy stworzyć kolejną klasę, która wyglądała będzie na przykład tak:

Przykład tworzenia klasy powielającej metody i pola innej klasy
class Student
  attr_accessor :wiek, :imie, :plec, :kierunek
  
  def pokaz
    puts "#{@imie} ma #{@wiek} lat i jest #{@plec ? 'kobieta' : 'mezczyzna' }"
    puts "studiuje na kierunku: #{@kierunek}"
  end
end
class Student attr_accessor :wiek, :imie, :plec, :kierunek def pokaz puts "#{@imie} ma #{@wiek} lat i jest #{@plec ? 'kobieta' : 'mezczyzna' }" puts "studiuje na kierunku: #{@kierunek}" end end

Takie podejście ma jednak wadę: powtarzamy się. Mamy już w programie klasę Osoba, a stwarzamy bardzo podobną klasę Student. Dlaczego ta druga klasa jest podobna? Ze względu na relację zawierania cech, ponieważ student jest osobą, a reprezentujący go typ danych pochodzi od typu reprezentującego osobę.

Klasa pochodna

Aby unikać powtarzania się, większość języków obiektowych obsługuje mechanizm zwany dziedziczeniem. Warunkiem do skorzystania z niego jest stworzenie tzw. klasy pochodnej (ang. derived class) względem już istniejącej, zwanej w tej relacji klasą bazową (ang. base class).

Przykład tworzenia klasy pochodnej
class Osoba
  attr_accessor :wiek, :imie, :plec
 
  def pokaz
    puts "#{@imie} ma #{@wiek} lat i jest #{@plec ? 'kobieta' : 'mezczyzna' }"
  end
end

class Student < Osoba
  attr_accessor :kierunek
  
  def pokaz
    super
    puts "studiuje na kierunku: #{@kierunek}"
  end
end

s = Student.new
s.imie = "Jan Kowalski"
s.plec = false
s.wiek = 25
s.kierunek = "Informatyka"

s.pokaz

# => Jan Kowalski ma 25 lat i jest mezczyzna
# => studiuje na kierunku: Informatyka 
class Osoba attr_accessor :wiek, :imie, :plec def pokaz puts &#34;#{@imie} ma #{@wiek} lat i jest #{@plec ? &#39;kobieta&#39; : &#39;mezczyzna&#39; }&#34; end end class Student &lt; Osoba attr_accessor :kierunek def pokaz super puts &#34;studiuje na kierunku: #{@kierunek}&#34; end end s = Student.new s.imie = &#34;Jan Kowalski&#34; s.plec = false s.wiek = 25 s.kierunek = &#34;Informatyka&#34; s.pokaz # =&gt; Jan Kowalski ma 25 lat i jest mezczyzna # =&gt; studiuje na kierunku: Informatyka

Mechanizm działania

Dziedziczenie polega na tym, że klasa pochodna może odwoływać się do metod zdefiniowanych w klasie bazowej. Jest to po prostu rozszerzanie już istniejącego typu danych o nowe elementy. Wywołania metod są przezroczyste i nie wymagana jest specyficzna składnia.

W praktyce interpreter języka pamięta w każdej klasie odpowiedni wskaźnik kierujący do klasy bazowej (zwanej też superklasą). Jeśli wywołamy metodę, która nie została zdefiniowana w klasie bieżącej, to interpreter skorzysta z tego odwołania i zajrzy do klasy nadrzędnej, aby sprawdzić, czy aby tam nie ma tej metody. Czynność ta jest powtarzana, aż do momentu, w którym ścieżka dziedziczenia się zakończy.

W języku Ruby nie istnieje dziedziczenie wielobazowe, a więc klasy mogą mieć tylko jednego bezpośredniego „przodka”. We wcześniejszym przykładzie klasa Student jest klasą pochodną, a klasa Osoba jest jej klasą bazową.

Sprawdzanie ścieżki dziedziczenia

Istnieją sposoby na to, aby metoda klasy „dowiedziała się” po czym jej obiekt dziedziczy. Co więcej, instancje klas (obiekty), są już w nią wyposażone:

Przykład sprawdzania ścieżki dziedziczenia
s.class            # zwraca obiekt reprezentujący klasę obiektu
s.class.superclass # zwraca obiekt reprezentujący klasę bazową obiektu
s.class # zwraca obiekt reprezentujący klasę obiektu s.class.superclass # zwraca obiekt reprezentujący klasę bazową obiektu

Jak to? Przecież nie tworzyliśmy metody o nazwie class ani klasowej metody superclass, więc skąd się one wzięły? Żeby ułatwić życie programiście w języku Ruby wszystkie tworzone klasy dziedziczą po klasie Object. Wyjątkiem są oczywiście te klasy, które dziedziczą po innych klasach, jak w naszej przykładowej klasie Student. Jednak klasa Osoba, będąca przodkiem klasy Student, również jest potomkiem klasy Object, a więc ścieżka dziedziczenia zaprowadzi nas w końcu do zdefiniowanej metody class.

Właśnie w klasie Object znajdziemy metodę class, która zwraca obiekt klasy, dla której została ona wywołana. Brzmi to dziwacznie, ale przecież w Rubym nawet klasy są specyficznymi obiektami.

Z kolei metoda superclass zdefiniowana jest w klasie Class, która z kolei jest klasą wszystkich obiektów będących klasami. Zauważmy, że w podobny sposób dostępna jest między innymi metoda klasowa new, używana do tworzenia nowych obiektów. Aby przekonać się, że klasy są też obiektami (instancjami klasy Class) możemy użyć konstrukcji:

  Student.class
Student.class

Nadpisywanie metod

W klasie Student z wcześniejszego przykładu znajdziemy metodę pokaz, której nazwa jest taka sama, jak nazwa metody z klasy bazowej Osoba. Interpreter wywoła właśnie ją i nie będzie już poszukiwał jej w superklasie. Nazywamy to nadpisywaniem metody (ang. method overriding).

Jednak nasza metoda ma uzupełniać, a nie całkowicie unieważniać działanie oryginalnej i dlatego korzystamy w niej ze słowa kluczowego super. Umieszczenie go sprawia, że zostanie wywołana nadpisywana sama metoda zdefiniowana w superklasie.


comments powered by Disqus