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
:
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:
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:
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).
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 "#{@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
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żeli 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:
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.