Szukamy definicji
Co się zatem dzieje, gdy wywołujemy metodę na jakimś obiekcie?
W pierwszej kolejności sprawdzana jest klasa tego obiektu. Jeśli w niej znajduje się definicja szukanej metody – świetnie! Poszukiwania zakończone i wiadomo co wołać.
W przypadku braku definicji metody w klasie obiektu, przeszukiwane są jej klasy nadrzędne (zwane także przodkami). Łańcuch przodków każdego z obiektów możemy podejrzeć przy użyciu metody ancestors()
:
> a.class.ancestors
=> [MyClass, Object, Kernel, BasicObject]
Innym określeniem klasy nadrzędnej do obecnej jest termin superklasa. Dla MyClass
superklasą jest Object
:
> MyClass.superclass
=> Object
Łańcuch przodków jest przeszukiwany na obecność metody do znalezienia jej najbliższej definicji. Jeśli metody znaleźć się nie uda, zwracany jest wyjątek NoMethodError
.
Co z wywoływaniem metod na klasie – “metodami klasowymi”?
Tak naprawdę nie ma czegoś takiego, jak metody klasowe – Ruby obsługuje tylko metody instancyjne!
W tym miejscu warto przypomnieć sobie o jednej z fundamentalnych zasad: w Rubym wszystko jest obiektem! Dotyczy to także klas – one też nie są niczym innym niż instancjami klasy Class
.
class MyClass; end
> MyClass.class
=> Class
Pod pojęciem “metod klasowych” muszą się więc kryć znane nam już metody instancyjne, które muszą być zdefiniowane w klasie danego obiektu.
Jednym ze sposobów na dodanie metody klasowej jest więc zdefiniowanie jej w klasie Class
:
class Class
def my_class_method
'this is an instance method of Class'
end
end
> MyClass.my_class_method
=> "this is an instance method of Class"
Metoda klasowa jak się patrzy!
Oczywiście w takim przypadku jest ona dostępna dla wszystkich klas, bo każda z nich jest obiektem klasy Class
:
class MyDifferentClass; end
> MyDifferentClass.my_class_method
=> "this is an instance method of Class"
Jak to się zatem dzieje, że definiując metodę klasową w sposób “klasyczny” nie jest ona po prostu wrzucana do Class
?
class MyClass
def self.class_method
'this is a class method of MyClass'
end
end
> Class.instance_methods.include?(:class_method)
=> false
W Rubym każdy obiekt ma swoją klasę _singletonową_ (zwaną też klasą anonimową lub wirtualną).
To właśnie w tej klasie singletonowej ląduje definicja “metody klasowej” – oczywiście już jako jej metoda instancyjna ?
>MyClass.singleton_class
=> #<Class:MyClass>
>MyClass.singleton_class.instance_methods.inlcude?(:class_method)
=> true
Metody zdefiniowane w klasie singletonowej można też podejrzeć w ten sposób:
> MyClass.singleton_methods
=> [:class_method]
Uzbrojeni w wiedzę dotyczącą definiowania i wywoływania metod możemy przejść do omawiania różnic między dziedziczeniem a modularnością.
Moduły a sprawa Klasowa
Modularność kontra dziedziczenie
Rozszerzać możliwości klasy możemy na dwa sposoby:
- poprzez modularność,
- poprzez dziedziczenie.
W idealnej sytuacji, napisane przez nas obiekty mają wysoką spójność (czyli jedną odpowiedzialność – Single Responsibly Objects) i mało zależności.
Modularność polega właśnie na rozbiciu problemu na osobne moduły – każdy z nich obarczony jedną odpowiedzialnością – które następnie można jasno dodawać do klasy według potrzeb.
Dziedziczenie często prowadzi do obiektów posiadających wiele różnych “odpowiedzialności”, które zostały hurtem otrzymane od obiektów-rodziców. Mniejsza jasność, większa złożoność, trudniejszy w ogarnięciu kod, kręcenie głowami, płacz, zgrzytanie zębów.
Modularność oczywiście również ma swoje wady: większą ilość kodu i więcej roboty przy dołączaniu wielu modułów. Jeśli okaże się, że podobny efekt uzyskalibyśmy dziedzicząc, a zysk z modularności jest mniejszy niż potrzebna jej nadwyżka pracy – używajmy dziedziczenia.
Innym przypadkiem, w którym użycie dziedziczenia jest sensowne, to sytuacja, kiedy mamy wiele obiektów należących do jednej “rodziny” i dziedziczących po wspólnym przodku. Przodek ten (najczęściej nazwiemy go BaseCośTam
) zawiera wtedy metody odpowiedzialne za podstawową funkcjonalność, a potomkowie z nich korzystają i dodają do nich coś w razie potrzeby (a zdarza się, że nie dodają nic, i cała klasa potomka to tylko samo dziedziczenie!).
Jak modułować kod?
Za dołączanie modułów do klas w Rubym odpowiadają trzy słowa kluczowe:
include
– zawieranieprepend
– poprzedzanieextend
– rozszerzanie
Zarówno include
jak i prepend
dodają metody instancyjne, extend
– metody klasowe. Ale po kolei.
Include
Słowo kluczowe include
pozwala na zawarcie w klasie określonego modułu i sprawia, że metody tego modułu będą traktowane jak metody instancyjne klasy – czyli będą dostępne dla jej instancji.
Moduł zawarty w klasie przy użyciu include
wskoczy na drugie po niej miejsce w łańcuchu przodków tej klasy.
Modułów możemy w ten sposób dodać ile tylko nam się podoba, warto jednak pamiętać, że ostatnio dodany będzie brany jako pod uwagę jako pierwszy.
Krótki przykład:
module Morning
def greeting
'Good morning!'
end
end
module Evening
def greeting
'Good evening!'
end
end
class MyClass
include Morning
include Evening
end
> MyClass.ancestors
=> [MyClass, Evening, Morning, Object, Kernel, BasicObject]
> MyClass.new.greeting
=> "Good evening!"
Prepend
Rzadziej używany odpowiednik include
, który dla odmiany wrzuca moduły na początek łańcucha przodków (czyli jeszcze przed samą klasą):
class MyClass
prepend Morning
prepend Evening
end
> MyClass.ancestors
=> [Evening, Morning, MyClass, Object, Kernel, BasicObject]
> MyClass.new.greeting
=> "Good evening!"
Częstszym przykładem użycia prepend
jest rozbudowanie metod otrzymanych od przodków przy użyciu super
:
module Descriptor
def get_to_work
puts "Time to work..."
puts "-----"
super
puts "-----"
puts "Work is done!"
end
end
class Worker
prepend Descriptor
def get_to_work
puts "Calculatig 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8..."
puts "Result is: #{(1..8).inject(:*)}"
return 1
end
end
> Worker.new.get_to_work
Time to work...
-----
Calculatig 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8...
Result is: 40320
-----
Work is done!
Extend
Tak jak wspomnieliśmy, Ruby wspiera jedynie metody instancyjne. Wszystkie metody klasowe są metodami instancyjnymi klasy singletonowej, którą posiada każdy obiekt.
Okazuje się, że taka klasa singletonowa ma swój własny łańcuch przodków. Właśnie do tego łańcucha dodany zostaje moduł, którym extend
ujemy daną klasę.
Zobaczmy to na prostym przykładzie:
module Night
def greeting
'Good night.'
end
end
class MyClass
extend Night
end
Metody z modułu Night
nie zostają dodane bezpośrednio do klasy singletonowej klasy MyClass
:
> MyClass.singleton_class.instance_methods(false)
=> []
ale dzięki obecności Night
w łańcuchu przodków klasy singletonowej:
> MyClass.singleton_class.ancestors
=> [#<Class:MyClass>, Night, #<Class:Object>, ...]
metody zdefiniowane w Night
są widoczne jako metody singletonowe i możemy je wywołać:
> MyClass.singleton_methods
=> [:greeting]
> MyClass.greeting
=> "Good night."
Metody klasowe przez include
Railsy umożliwiają dodanie także metod klasowych przy użyciu tylko include
‘a. Służy do tego metoda included
.
included
przyjmuje argument określający obiekt, do którego dany moduł jest załączany. Możemy więc w jej ciele rozszerzyć ten moduł o metody klasowe:
def self.included(base)
base.extend(ClassMethods)
end
I na przykładzie:
module ChitChat
def greeting
'Hello!'
end
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def salutation
'Cheers :)'
end
end
end
class Person
include ChitChat
end
> Person.new.greeting
=> "Hello!"
> Person.salutation
=> "Cheers :)"
# a także:
> Person.greeting
NoMethodError: undefined method `greeting' for Person:Class
> Person.new.salutation
NoMethodError: undefined method `salutation' for #<Person:0x00007fc09f06fba0>
tl;dr, czyli podsumowanie
- wszystko jest obiektem!
- obiekty to zbiór zmiennych instancyjnych i link do klasy,
- metody obiektu znajdują się w jego klasie, gdzie nazywamy je metodami instancyjnymi,
- klasa to tylko obiekt z listą metod instancyjnych i linkiem do jej klasy,
- wywołanie metody szuka jej definicji w klasie obiektu, na którym jest wołana, a następnie w jej “przodkach” (
ancestors()
), - zawieranie modułu (
include
) w klasie umieszcza go w łańcuchu przodków tuż za klasą, - dodanie do klasy modułu przy użyciu
prepend
sprawi, że moduł będzie w łańcuchu przodków przed samą klasą, - metody klasowe nie są zdefiniowane w samej klasie, tylko w nadrzędnej jej klasie anonimowej
singleton
– tak działa rozszerzanie klasy modułami przezextend
.
Za współpracę przy artykule bardzo dziękuję Alicji Cyganiewicz! ?