Metaprogramowanie
Poprzedni rozdział | Następny rozdział
Metaprogramowanie to temat, który w kontekście języka Ruby zyskał szczególnego znaczenia. Niektóre z omówionych wcześniej technik (np. introspekcja, rozszerzanie klas wbudowanych, aliasowanie metod) mogłyby zostać w pewnym stopniu objęte tym terminem. W rzeczywistości metaprogramowanie nie sprowadza się do jednej konstrukcji językowej – obejmuje szereg instrumentów dostarczonych przez język.
Jak napisaliśmy w poprzednim rozdziale – metaprogramowanie to programowanie na poziomie
klas, czyli obiektów klasy Class
, które stoi w opozycji do zwykłego
programowania, którego przedmiotem są instancje tych klas.
Postawienie wyraźnej granicy pomiędzy tymi sposobami programowania nie jest
wcale takie proste, gdyż wywołanie metody is_a?
, która sprawdza pewne
własności klasy danego obiektu, może być już potraktowane jako przejaw
metaprogramowania.
Tym niemniej w kontekście języka Ruby termin metaprogramowanie ma zazwyczaj nieco inny sens – chodzi o możliwość dodawania pewnych własności do klas wykorzystując do tego pewien algorytm. Można powiedzieć, że te nowe własności są dopiero obliczane w trakcie wykonania programu. Oczywiści taka sytuacja nie dziwi nas w kontekście programowania obiektów – wiadomo, że stan danego obiektu, zależy od wcześniejszych obliczeń, które zostały wykonane z jego udziałem. Teraz jednak obiektem tym jest klasa. Zatem zachowanie klasy obiektów określane jest przez pewne obliczenie, które dokonuje się dopiero w trakcie wykonania programu.
Metaprogramowanie zatem często będzie sprowadzało się do dodawania, modyfikowania a nawet usuwania metod w klasach, które obejmuje.
Wykonywanie kodu w kontekście klasy
Aby uczynić nasze rozważania bardziej konkretnymi, pokażmy prosty przykład
zastosowania tej techniki. Wykorzystana jest w nim metoda class_eval
,
która przyjmuje kod w postaci bloku lub łańcucha znaków i
powoduje dołączenie go do klasy, w kontekście której jest wywoływana:
class Alphabet ('a'..'z').each do |letter| class_eval <<-END def #{letter} "#{letter}" end END end end alpha = Alphabet.new alpha.a #=> "a" alpha.b #=> "b"
Uwaga
Stosowana przeze mnie bibliotek do kolorowania składni nie jest doskonała, dlatego pomimo tego, że wszystko co znajduje się pomiędzy słowami END jest zwykłym łańcuchem znaków, rozpoznaje w nim słowa kluczowe.
W powyższym przykładzie w klasie Alphabet
dla każdej małej litery
łacińskiego alfabetu definiowana jest metoda o takiej samej nazwie,
zwracająca tę literę. Całość opiera się na tym, że metoda class_eval
wywoływana jest wielokrotnie dla łańcucha znaków uzupełnianego
kolejnymi literami alfabetu. Powyższy kod w tradycyjny sposób
można by zapisać następująco:
class Alphabet def a "a" end def b "b" end def c "c" end # itd. end
Widzimy zatem, że kod, który zapisany w tradycyjny sposób byłby bardzo długi, z wykorzystaniem metaprogramowania, uległ znacznemu skróceniu.
Oczywiście rzadko kiedy przydatne jest definiowanie klasy, która
posiadałaby tak nieprzydatne metody. Co nie znaczy, że sama technika
jest nieprzydatna – wręcz przeciwnie. Jest ona przedstawiona
np. w książce “Rails – przepisy” do stworzenia własnych wersji
metod pomocniczych dla formularzy wykorzystywanych na stronach
internetowych. Jeśli chcemy zachować spójny wygląd różnych elementów
formularza (np. dodać etykietę, otoczyć paragrafem lub umieścić w tabeli),
nie wystarczy nam czysty CSS
– konieczne jest wygenerowanie
odpowiedniego HTML-a. Każdy element formularza modyfikowana jest jednak
w taki sam sposób, a biorąc pod uwagę ich dosyć dużą liczbę,
zastosowanie tej techniki przynosi realne korzyści.
Meta-definiowanie metod instancyjnych
Przywołane rozwiązanie jest o tyle niedoskonałe, że dołączany kod jest ukryty w łańcuchu znaków. Bezpieczniej byłoby jednak zamiast niego zastosować bloki kodu. (Nie chodzi tutaj tylko o poprawność syntaktyczną, która może być poprawiona przez odpowiedni edytor, ale np. o to, że część łańcucha może pochodzić od użytkownika, który nie zawsze ma chlubne zamiary.)
Aby w powyższym przykładzie uniknąć tego problemu można wykorzystać
metodę define_method
, która jak sama nazwa wskazuje, powoduje
zdefiniowanie metody w kontekście klasy, na rzecz której została wywołana.
To co różni ją od słowa kluczowego def
, to
określanie nazwy metody poprzez symbol lub łańcuch znaków.
Przyjrzyjmy się najpierw
dwóm definicjom tej samej metody z wykorzystaniem słowa kluczowego
def
oraz metody define_method
:
class Hello def say(message) puts "Hello " + message end define_method :say do |message| puts "Hello " + message end end Hello.new.say "metaworld" #=> "Hello metaworld"
W powyższym przykładzie obie definicje metody say
są sobie równoważne.
Definicja wykorzystująca wywołanie define_method
jest oczywiście
bardziej rozwlekła, a argumentu metody pojawiają się jako parametry
bloku, co czyni jej zapis bardziej skomplikowanym. Oczywiście stosowanie
tego wywołania w tej postaci mija się z celem. Możemy je natomiast
wykorzystać do poprawienia wcześniejszego przykładu z alfabetem:
class Alphabet ('a'..'z').each do |letter| class_eval do define_method letter do letter end end end end alpha = Alphabet.new alpha.a #=> "a" alpha.b #=> "b"
Bingo! Możemy zatem definiować nowe metody w czasie wykonania programu. Co więcej – ich nazwa może być również “obliczana”. Zatem pierwsze kroki na drodze do metaprogramowania zostały zrobione. Czas na kolejne.
Meta-definiowanie metod klasowych
Przedstawiony sposób rozszerzania własności klas pozwala w zasadzie na dodawanie dowolnych metod instancyjnych. Tym co stanowi o prawdziwej sile metaprogramowania jest jednak możliwości dodawania i modyfikowania metod klasowych. Oczywiście jeśli uważnie przeczytaliśmy rozdział poświęcony programowaniu obiektowemu, a także poprzedni punkt – nie jest dla nas zaskoczeniem, że w Ruby – skoro można dodawać metody instancyjne – można również dodawać metody klasowe. Wszak klasy są obiektami, a metody klasowe są po prostu metodami singletonowymi tych obiektów. Tym niemniej właśnie to niezwykle spójne podejście do obiektowości powoduje, że metaprogramowanie niewiele różni się od zwykłego programowania, a w konsekwencji stanowi o sile tego języka.
Klasa singletonowa zwykłej klasy
Jak zatem możemy dodać metody klasowe wykorzystując metaprogramowanie?
W stosunku do wcześniejszej metody istnieje tylko jedna różnica:
wywołanie class_eval
powinny odbywać się nie na rzecz modyfikowanej
klasy, lecz jej klasy singletonowej. Reszta pozostaje identyczna:
Jak zatem można dostać się do klasy singletonowej danej klasy?
Istnieje idiom Ruby, który służy właśnie do tego:
class << self; self; end
Wywołany w kontekście klasy zwraca jej klasę singletonową.
Na pierwszy rzut oka wywołanie to jest bardzo dziwne. Problem bierze się
stąd, że występujące w nim słowo kluczowe self
reprezentuje dwa
różne obiekty! Pierwsze wystąpienie odnosi się do obiektu klasy, do czyjej
klasy singletonowej chcemy się dostać. Drugie wystąpienie zaś odnosi
się właśnie do tej klasy singletonowej. Wyrażenie class << self
“otwiera”
kontekst klasy singletonowej, zatem wewnątrz tego kontekstu słowo self
odnosi się właśnie do niej. W wyniku ewaluacji całego wyrażenia
zostaje właśnie zwrócona obiekt, który ukrywa się pod drugim słowem
self
, którym będzie właśnie poszukiwana klasa singletonowa.
class Person def self.meta class << self self end end end Person.meta #=> #<Class:Person>
W powyższym przykładzie w klasie Person
zdefiniowana jest metoda klasowa
meta
. Jej ciało stanowi wyrażenie pozwalające na dostęp do klasy
singletonowej (jest to bardziej rozwlekła postać wyrażenia class << self; self; end
–
średniki zostały zastąpione znakami końca linii).
Wywołanie tej metody daje w wyniku klasę singletonową klasy Person
.
Można ją rozpoznać po tym, że jej reprezentacja składa się ze
słowa Class
, po którym następuje dwukropek oraz domyślna reprezentacja
obiektu, do którego jest ona dołączona (w przypadku klas jest to po
prostu ich nazwa).
Metody klasowe
Skoro posiadamy już te wszystkie informacje, możemy przystąpić do
dodawania do naszej klasy metod klasowych. Jak wskazaliśmy wcześniej –
w stosunku do metod instancyjnych różnica polega tylko na tym, że
metoda class_eval
wywoływana jest w kontekście klasy singletonowej,
a nie danej klasy. Zatem nasza definicja klasy Alphabet
będzie
wyglądała następująco:
class Alphabet ('a'..'z').each do |letter| (class << self; self; end).class_eval do define_method letter do letter end end end end Alphabet.a #=> "a" Alphabet.b #=> "b"
Powyższy kod zapisany w sposób tradycyjny wygląda następująco:
class Alphabet def self.a "a" end def self.b "b" end # itd. end
Poznaliśmy zatem podstawowe sposoby “metadodawania” metod instancyjnych i klasowych. Najwyższy czas aby pokazać jakieś ciekawe, nietrywialne przykłady wykorzystania tej techniki.
Przykład 1: cattr_reader, cattr_writer, cattr_accessor
W rozdziale dotyczącym obiektowości przedstawione zostały m.in.
dwa zagadnienia: możliwość łatwego definiowania akcesorów dla zmiennych
instancyjnych (za pomocą konstrukcji attr_accessor
), a także
dziwna własność zmiennych klasowych, polegająca na tym, że po ich
zdefiniowaniu w jakiejś klasie, ponowne ich zdefiniowanie w
klasach potomnych nie jest możliwe. Być może bardziej odpowiadałyby
nam nieco inne “atrybuty klasowe” – takie, które byłby związane
faktycznie z wybraną klasą, a nie całym drzewem dziedziczenie,
które z niej wyrasta. Co więcej, chcielibyśmy mieć możliwość
definiowania akcesorów dla tych atrybutów w taki sam sposób jak
definiowane są akcesory dla atrybutów instancyjnych, np.
przez wywołania cattr_reader, cattr_writer
i cattr_accessor
.
Aby spełnić pierwsze wymaganie można posłużyć się atrybutami instancyjnymi obiektów klas. Każda klasa, jako obiekt, posiada swoje własne zmienne instancyjne, dlatego też ich wykorzystanie nie powoduje “zaśmiecenia” drzewa dziedziczenia.
Aby spełnić drugie wymaganie konieczne jest rozszerzenie klasy Class
.
Dodanie do niej odpowiednich metod, spowoduje, że będą one mogły
być wykorzystywane dokładnie tak samo, jak wykorzystywane są wyrażenia
attr_reader
i pozostałe.
class Class def meta class << self; self; end end def cattr_reader(*names) meta.class_eval do attr_reader *names end end def cattr_writer(*names) meta.class_eval do attr_writer *names end end def cattr_accessor(*names) cattr_reader(*names) cattr_writer(*names) end end class A cattr_accessor :instances self.instances = 0 def initialize self.class.instances += 1 end end class B < A cattr_accessor :instances self.instances = 0 end a = A.new A.instances #=> 1 B.instances #=> 0 b = B.new B.instances #=> 1 A.instances #=> 1
W powyższym przykładzie w klasie Class
definiowane są metody cattr_reader
,
cattr_writer
oraz cattr_accessor
, zgodnie z wcześniejszą propozycją.
Dzięki tym metodom, możemy dla klas definiować akcesory do ich atrybutów
instancyjnych, które mogą funkcjonować jak “poprawione” zmienne klasowe.
W naszym rozwiązaniu zdefiniowaliśmy najpierw metodę meta
, która
pozwala na wygodny dostęp do klasy singletonowej danej klasy.
Następnie definiowane są metody cattr_reader
, cattr_writer
i
cattr_accessor
korzystające z… wbudowanych konstrukcji attr_reader
,
attr_writer
! Rozwiązanie to działa, ponieważ wywołania te są
realizowane w kontekście klas singletonowych poszczególnych klas,
zatem odpowiednie metody dostępowe, stają się metodami klasowymi
tych klas.
Przykład ten jest na tyle pouczający, że warto go jeszcze trochę poeksploatować.
Rozszerzanie języka
Pierwszy powód jest bardzo praktyczny – pozwala ukazać, że rozszerzanie
języka Ruby nie jest trudne. Metody cattr_reader
i pozostałe
są na tyle przydatne, że pojawiły się np. w rozszerzeniach klas wbudowanych
dostarczonych w frameworku Ruby on Rails (ich definicje są jednak trochę inne,
ponieważ operują na zmiennych klasowych, a nie klasowo-instancyjnych).
Nie jest wykluczone,
że podobne konstrukcje będą wbudowane w sam język w przyszłych jego wersjach.
Tym niemniej przykład ten pokazuje, że dostosowanie języka do własnych
potrzeb jest możliwe i nie nastręcza poważniejszych problemów, o ile
opanujemy podstawowe zagadnienia metaprogramowania.
Przesada
Drugi powód związany jest z zaleceniem, które pojawiło się na początku tego rozdziału – nie powinniśmy przesadzać z tymi wszystkimi magicznymi własnościami języka. Za przesadę uważam następujące rozwiązanie przedstawionego problemu:
class Class def meta class << self; self; end end %w{reader writer accessor}.each do |postfix| class_eval do define_method "cattr_" + postfix do |*names| meta.class_eval do send("attr_" + postfix, *names) end end end end end
Oczywiście jeśli chcemy wygrać konkurs na /-/4xor4 roku, to możemy tworzyć konstrukcje tego rodzaju. Niestety bardzo ujemnie wpływają one na czytelność kodu. Przebywanie na meta-meta-meta-poziomach niech pozostanie zarezerwowane dla filozofów. Programiści powinni jednak bardziej twardo stąpać po ziemi (korzystając z terminologii Tarskiego: wyrażać swoje myśli w języku przedmiotowym (nie mylić z obiektowym ;P )).
Modyfikowanie atrybutów instancyjnych
Trzeci powód ma charakter dydaktyczny – otóż przykład ten możemy wyrazić w nieco bardziej rozwlekły sposób, prezentując jednocześnie pewne metody ważna w metaprogramowaniu. Przyjrzyjmy się poniższemu kodowi:
class Class def meta class << self; self; end end def cattr_reader(*names) names.each do |name| meta.class_eval do define_method name do instance_variable_get("@"+name.to_s) end end end end def cattr_writer(*names) names.each do |name| meta.class_eval do define_method name.to_s + "=" do |val| instance_variable_set("@"+name.to_s,val) end end end end def cattr_accessor(*names) cattr_reader(*names) cattr_writer(*names) end end
Oczywiście powyższy kod realizuje to samo zadanie co wcześniej.
Na czym zatem polega różnica? Otóż nie idziemy w nim na skróty
korzystając z metod attr_reader
,... ale definiujemy
każdą z metod z osobna. Dzięki temu możemy dokładnie zobaczyć
co dzieje się “pod spodem” i wykorzystać to w przypadku, gdy
wcześniejsze rozwiązanie nie będzie wystarczające.
Dokładna implementacja metod attr_reader
i attr_writer
jest
bardzo podobna – z grubsza z tą różnicą, że w pierwszej z nich
wykorzysta jest metoda instance_variable_get
, a w drugiej
instance_variable_set
. Cóż to za metody? Jak sama nazwa wskazuje…
pierwsza z nich pozwala odczytać, a druga ustawić wartość _zmiennej
instancyjnej o nazwie przekazanej jako parametr_. Hmm…
Czy nie łamie to enkapsulacji, zapyta fan metodyki obiektowej?
Tak, łamie. Co gorsza – metody te są publiczne!
Skąd zatem wzięły się tego rodzaju metody i po co nam one?
Zwróćmy po pierwsze uwagę, że nie zostały one omówione w rozdziale
dotyczącym programowania obiektowego, a na pierwszy rzut oka
tam właśnie powinny się znaleźć. Napisałem wręcz, że nie mamy
innego dostępu z zewnątrz do zmiennych instancyjnych, niż poprzez
dobrze zdefiniowane metody. Można napisać, że skłamałem! No możne
nie do końca, bo instance_variable_get/set
też są metodami,
ale mało komu przyszłoby do głowy reimplementować je we własnej
klasie, tak żeby zabezpieczyć się przed “majstrowaniem” przy stanie
obiektu.
Metody te przewidziane są właśnie do obsługi takich specjalnych
przypadków – z powodzeniem mogą być stosowane w technikach
metaprogramowania, ale na pewno nie powinno się ich wykorzystywać
w zwykłym programowaniu! Od tego mamy interfejs klasy, aby z niego
i tylko z niego korzystać. Z drugiej strony, jeśli weźmiemy pod
uwagą, że w Ruby istnieje wywołanie eval
(którego omówienie
ominąłem aż do tej chwili), pozwalające na wykonanie dowolnego
kodu przekazanego jako łańcuch znaków, wszelkie konstrukcje
tego rodzaju są jedynie niewinną igraszką. Aby dostać się
do wartości zmiennej moglibyśmy np. zdefiniować metodę, która
zwracałaby wartość tej zmiennej, wywołać ją, a na koniec
usunąć i żadne mechanizmy nie zabezpieczyłyby nas przed takimi
“czarami”. Dzięki metodom takim jak instanve_variable_get
nie musimy
uciekać się nieustannie do takich sztuczek – ich wykorzystanie
powoduje, że to co mogło być ukryte w łańcuchu znaków, teraz jest
“powiedziane wprost”, innymi słowy – jest bardziej czytelne.
Dzięki takim rozwiązaniom język Ruby wydaje się zachowywać balans pomiędzy ograniczeniami jaki narzuca, a możliwościami jakie oferuje. Jeśli chcemy zrobić coś zwykłego – możemy to zrobić w prosty sposób (vide zdefiniowanie akcesorów dla atrybutów instancyjnych). Jeśli chcemy zrobić coś niezwykłego i potencjalnie niebezpiecznego - możemy to zrobić, ale wymaga to pewnego wysiłku (vide dostęp do metaklasy, manipulowanie zmiennymi instancyjnymi), chociażby nawet wysiłek ten miał sprowadzać się do wprowadzanie większej liczby znaków na klawiaturze.
Skoro zatem uznaliśmy sensowność tego rodzaju mechanizmów, możemy
przystąpić do analizy rozwiązania przedstawionego powyżej. W przypadku
metody cattr_reader
, dla każdego jej argumentu definiowana
jest metoda o takiej samej jak on nazwie, która pobiera wartość
zmiennej instancyjnej też o tej samej nazwie. Metoda instance_variable_get
wymaga aby nazwa atrybutu poprzedzona była znakiem @, dlatego
jest on dodawany do nazwy definiowanej metody.
Podobnie jest w przypadku metody cattr_writer
– definiowane są
metody o nazwach takich jak atrybuty, uzupełnione znakiem =.
Dlatego też metody te mogą stawać po lewej stronie instrukcji przypisania.
Definiowane metody akceptują jeden argument, który przekazywany jest do
metody instance_variable_set
, ustawiającej wartość odpowiedniego
atrybutu.
Ostatnia metoda sprowadza się do wywołania dówch wcześniejszych – nie ma tutaj żadnej magii.
Przykład 2: ActiveRecord
Możliwość rozszerzania języka, w sposób który tutaj został przedstawiony, na pewno będzie atrakcyjna dla części programistów. Tym niemniej z pewnością istnieje grupa nieprzekonanych, którzy myślą sobie - co mi to właściwie daje? Mam nadzieję, że kolejny przykład pozwoli ich przekonać. Pochodzi on również z frameworku Ruby on Rails. Dla osób, które korzystają z niego nie opanowawszy wszystkich arkanów języka, może on się wydać na tyle znany, że aż oczywisty. Jednakże jego zrozumienie bez znajomości technik metaprogramowania jest de facto niemożliwe.
Chodzi mi o pierwsze lepsze makro z klasy ActiveRecord::Base
(ActiveRecord to system mapowania obiektowo-relacyjnego, stanowiący
jeden z elementów frameworku Ruby on Rails), np.
has_many
czy belongs_to
. Pierwsze pozwala na wyrażenie relacji jaka zachodzi
pomiędzy dwoma klasami – wskazuje, że jedna instancja pewnej
klasy może być powiązana z wieloma instancjami drugiej klasy.
Drugie jest odwrotnością pierwszego.
Weźmy klasyczny przykład z koszykiem:
class Item < ActiveRecord::Base belongs_to :cart end class Cart < ActiveRecord::Base has_many :items end my_cart = Cart.new item1 = Item.new item2 = Item.new my_cart.items << item1 my_cart.items << item2
W powyższym przykładzie definiowane są dwie klasy dziedziczące
z ActiveRecord::Base
: Item
oraz Cart
. Pierwsza z nich
reprezentuje jakiś obiekt, który może pojawić się w koszyku (np.
oklepaną książkę), natomiast druga właśnie ów koszyk.
Na pierwszy rzut oka – nic specjalnego. Tym niemniej kiedy przyjrzymy
się sposobowi użycia obiektu klasy Cart
, możemy
zastanawiać się skąd wzięła się metoda items
? Na pewno nie została
zdefiniowana w klasie ActiveRecord::Base
. Dodam jeszcze, że
powyższa definicja jest całkowicie wystarczająca, aby przedstawione
rozwiązanie zaczęło działać (tzn. pokazana jest cała definicja klasy
Cart
).
Oczywiście wszystko tkwi w wywołaniu has_many :items
– to ono musiało
spowodować dodanie metody items
do klasy Cart
! A czy mogło
zostać dodane bez uciekania się do metaprogramowania?
Oczywiście nie.
Wywołanie makra has_many
na rzecz danej klasy, powoduje dodanie do niej
metody o nazwie takiej jak jego pierwszy argument. Dodatkowo makro
to akceptuje również opcjonalne parametry (np. nazwę tabeli w bazie danych,
nazwę klucza, itp.) Te informacje składowane są w atrybutach klasowo-instancyjnych
danej klasy i wykorzystywane w utworzonym wywołaniu do skonstruowania adekwatnego
wywołania SQL-a. Ponieważ wykorzystywane są techniki metaprogramowania -
utworzone metody, z punktu widzenia użytkownika tych klas, nie różnią się
niczym od pozostałych metod – mogą być wykorzystywane bezpośrednio w kodzie,
są widoczne w klasach pochodnych, itp.
ActiveRecord a Hibernate
Jeśli porównamy rozwiązanie zaproponowane w ActiveRecord z rozwiązaniem stosowanym w popularnej bibliotece ORM języka Java – Hibernate, zobaczymy jak wielka jest przewaga tego pierwszego nad drugim. Mapowanie w AR (skrót od ActiveRecord) jest definiowane bezpośrednio w klasie, a nie w zewnętrznych plikach XML, co znacząco ułatwia utrzymywanie spójności pomiędzy modelem obiektowym a modelem relacyjnym. Co więcej – w AR nie musimy sami definiować metod służących do tworzenia asocjacji pomiędzy obiektami, jak ma to miejsce w Java+Hibernate. Dzięki temu definiowanie asocjacji bardzo przypomina definiowanie atrybutów – wystarczy zadeklarować je w jednym miejscu i cała sprawa jest załatwiona.
Jednym z rozwiązań przedstawionego problemu dla Javy jest biblioteka XDoclet, która również pozwala przechowywać mapowanie obiektowo-relacyjne razem z kodem źródłowym. Ponieważ jednak odpowiednie pliki XML i tak muszą być stworzone, każdorazowa modyfikacja tego mapowania pociąga za sobą konieczność wygenerowania tych plików, co negatywnie wpływa na użyteczność tego rozwiązania.
Rozwiązania te zdecydowanie kontrastują z prostotą AR: jedna modyfikacja kodu jest natychmiastowo odzwierciedlana we wszystkich istotnych miejscach.
Przykład 3: pluginy w Ruby on Rails
W poprzednim przykładzie starałem się pokazać jak dzięki technikom metaprogramowania można tworzyć DSL-e, a w konsekwencji wpłynąć na czytelność i spójność tworzonego kodu (mapowanie obiektowo-relacyjne jest jedną z tych dziedzin, gdzie stworzenie specjalnego języka mapowania jest wręcz niezbędne).
Tym niemniej w dobie programowania komponentowego, gdzie największy nacisk kładzie się na możliwość ponownego wykorzystania kodu, należy pokazać w jaki sposób metaprogramowanie może przyczynić się do zaspokojenia wymagań tej technologii.
Jednym z testów na to, jaki komponentowy potencjał posiada dany język lub platforma, jest możliwość tworzenia rozszerzeń, zwanych często pluginami, które często utożsamia się z reużytkowalnymi komponentami.
Tym czego przede wszystkim oczekuje się od pluginów, to możliwość wykorzystani ich bez jakichkolwiek modyfikacji oryginalnej struktury programu, czy samych pluginów, w czym zawiera się również mechanizm ich automatycznego wykrywania, konfigurowania i uruchamiania. Ponadto ważne jest również, aby tworzenie rozszerzeń nie różniło się zbytnio od tworzenia pozostałych elementów oprogramowania, a także brak ograniczeń co do dostarczanych przez nie funkcjonalności.
Nie trudno domyśleć się, że spełnienie tych wszystkich, przeciwstawnych wymagań jest zwykle dosyć trudne. W szczególności wymóg automatycznego wykrywania oraz uruchamiania pluginów niesie poważne ograniczenia dla języków które wymagają kompilowania kodu źródłowego przed uruchomieniem. Zazwyczaj kod taki musi być dodatkowo wyposażony w plik manifestu, w którym określone są jego komponentowe własności. W dalszej części pokażemy jak problem ten został rozwiązany w popularnym w środowisku Javy frameworku Eclipse, który posiada jeden z najbardziej rozbudowanych ekosystemów pluginów. Teraz zaś postaramy się przedstawić na przykładzie platformy Ruby on Rails jak wygląda możliwość tworzenia komponentów w języku Ruby.
Ładowanie pluginów
Zanim wskażemy te cechy języka, które są szczególnie przydatne
przy tworzeniu pluginów, musimy powiedzieć kilka słów o tym
jak działa system plugiów frameworku Rails. W największym skrócie
można powiedzieć, że pluginu trafia do katalogu
vendor/plugins/nazwa_pluginu
i musi zawierać plik init.rb
, którego kod
wywoływany jest automatycznie przy starcie całej aplikacji.
Dalej zaś katalog lib
, który znajduje się w głównym katalogu
pluginu dołączany jest do listy katalogów, zawierających
kod źródłowy, które są przeszukiwane w momencie wywołania
dyrektywy require
.
Choć pluginy mogą rozszerzać framework Ruby on Rails w nieco inny sposób, to opisany tutaj mechanizm w zupełności wystarcza do zrozumienia dalszych rozważań, dlatego też na nim poprzestaniemy.
Modyfikowanie dowolnej metody z określonej klasy
Pierwszą cechą języka Ruby, na którą należy zwrócić uwagę w kontekście pluginów, jest możliwość modyfikowania zachowania dowolnych klas. Wcześniej pokazywaliśmy ją na przykładzie klas wbudowanych – tym niemniej nie jest dla nikogo zaskoczeniem, że dotyczy ona dowolnej dostępnej klasy. Mechanizm ten sprowadza się do tego, że klasa posiada wszystkie te zachowania, które zostały zdefiniowane jako ostatnie. W szczególności: jeśli jakaś metoda jest definiowana dwa razy, to zawsze wykorzystywana jest ta definicja, która pojawiła się później (która później została wczytana przez interpreter):
class Changeable def my_name "Changeable" end def my_name "I'm changing!" end end changeable = Changeable.new changeable.my_name #=> "I'm changing!" class Changeable def my_name "Who knows my name?" end end changeable.my_name #=> "Who knows my name?"
Dzięki tej własności języka możemy w zasadzie zmodyfikować każdy fragment
programu, nie modyfikując a jedynie dodając nowy kod źródłowy.
Zatem jeśli znamy API programu, dla którego tworzymy plugin, możemy
posunąć się nawet do tego, że zastępujemy jedną lub wiele metod w jakiejś
klasie. Tym o co musimy zadbać to właściwa kolejność wczytywania kodu
(musimy zapobiec sytuacji, w której nasza modyfikacja wczytana
jest przed modyfikowanym kodem). Problem ten może być rozwiązany poprzez
jawne dołączenie (za pomocą dyrektywy require
) klasy, którą chcemy
zmodyfikować albo poprzez określenie kolejności wczytywanych pluginów
w pliku konfiguracyjnym aplikacji.
Podstawowym ograniczeniem tego rozwiązania jest to, że po dołączeniu
pluginu (np. poprzez umieszczenie modyfikacji w pliku init.rb
),
nie mamy możliwości jej wyłączenia. Zmiana została dokonana,
a dostęp do wersji oryginalnej zmodyfikowanej metody jest
niemożliwy (o ile nie zastosowaliśmy mechanizmu aliasowania).
Znacznie lepszym rozwiązaniem byłaby możliwość uruchamiania dostarczonej funkcjonalności na żądanie – np. tylko w wybranych klasach, dziedziczących z modyfikowanej klasy. Takie rozwiązanie pozwalałoby bowiem na stworzenie bardzo elastycznego mechanizmu pluginów. Oczywiście dzięki metaprogramowaniu jest ono w zasięgu ręki.
Dodawanie opcjonalnych własności
Pożądany schemat modyfikacji dostarczanej przez plugin powinien wyglądać następująco: nowa własność powinna pojawić się tylko w niektórych klasach dziedziczących z klas (lub dołączających moduły) znajdujące się w rozszerzanej aplikacji (w tym wypadku frameworku Ruby on Rails). Oczywiście – potencjalnie – własność ta powinna być dostępna we wszystkich klasach dziedziczących z wybranej klasy (dołączających moduł).
Najlepszą realizacją tego schematu jest dodanie w klasie bazowej (tzn. będącej elementem frameworku) pewnej metody singletonowej (innymi słowy – makra), której wywołanie powodowałoby aktywowanie danego rozszerzenia w wybranej klasie. Mechanizm ten byłby aktywowany w sposób podobny do wcześniej przedstawionego mechanizmu określania asocjacji pomiędzy klasami. Wykorzystywane rozszerzenie mogłoby nawet dostarczać cały DSL do realizacji dostarczanych funkcjonalności.
Jako przykład weźmy wykorzystaną wcześniej klasę ActiveRecord
.
Wyobraźmy sobie, że posiadamy kilka aplikacji opartych o
framework Ruby on Rails, w których potrzebujemy identycznej
funkcjonalności – obiekty niektórych klas modelu powinny posiadać
metody związane z czasem ich powstania i modyfikacji, np. pozwalające
określić czy dany obiekt jest nowy czy stary (w dziedzinie danego
modelu). Dodatkowe założenie polegałoby na tym, że w obrębie
wybranej klasy, przejście ze stanu nowy do stary jest
identyczne dla wszystkich obiektów (np. film jest stary jeśli
ma więcej niż 5 lat, a gazeta gdy ma więcej niż miesiąc, itp.)
new?
– zwracającej wartość prawda, jeśli obiekt byłby nowy,old?
– zwracającej wartość prawda, jeśli obiekt byłby stary,status
– zwracające stan (nowy/stary) obiektu,find_new
– zwracającej wszystkie nowe obiekty,find_old
– zwracającej wszystkie stare obiekty, etc.
w każdej z klas z osobna. Następny krok, mógłby polegać na wyodrębnieniu wspomnianych metod do postaci modułu i dołączaniu go do każdej z klas. Tym niemniej moduły te powtarzałyby się w odrębnych aplikacjach. Ponadto samo dołączenie modułu byłoby niewystarczające – konieczne byłoby jeszcze wywołanie metod, które dokonałyby stosownej konfiguracji klasy. Kolejny krok polegałby na dołączeniu modułu w klasie bazowej (w tym wypadku ActiveRecord) i takim zmodyfikowaniu modułu, aby definiował on jedną metodę, która uruchamiałaby jego funkcjonalność w danej klasie, a zarazem pozwalała na jej konfigurację. Ostateczne rozwiązanie polegałoby na wyodrębnieniu wspomnianego modułu w plugin i dołączeniu go do poszczególnych aplikacji.
Poniżej przedstawiamy ostateczny kształt naszego rozwiązania.
Nasz plugin nazwiemy acts_as_timed
. Railsy dostarczają
skrypt generujacy pluginy:
$ script/generate plugin acts_as_timed
W wyniku jego wywołania, w katalogu vendor/plugins/acts_as_timed
pojawia się m.in. plik init.rb
, który modyfikujemy w sposób
następujący:
require 'acts_as_timed' class ActiveRecord::Base include Apohllo::Acts::Timed end
Na początku dołączany jest (jeszcze niezdefiniowany) plik acts_as_timed.rb
z katalogu vendor/plugins/acts_as_timed/lib
. Dalej zaś do
klasy ActiveRecord::Base
dołączany jest (jeszcze niezdefiniowany)
moduł Timed
znajdujący się w przestrzeni nazw Apohllo::Acts::
.
Poniżej przedstawiona jest zawartość pliku acts_as_timed.rb
:
module Apohllo module Acts module Timed def self.included(mod) mod.extend(ClassMethods) end module ClassMethods def acts_as_timed(options={}) extend Apohllo::Acts::Timed::ClassMethods include Apohllo::Acts::Timed::InstanceMethods (class<<self; self;end).class_eval{attr_reader :old} @old = options[:old] || 1.year end end module SingletonMethods def find_new find(:all, :conditions => ["created_at > ?", Time.now - @old]) end def find_old find(:all, :conditions => ["created_at < ?", Time.now - @old]) end end module InstanceMethods def new? self.created_at > Time.now - self.class.old end def old? !new? end def status new? ? :new : :old end end end # Timed end # Acts end # Apohllo
W powyższym kodzie kilka rzeczy wymaga wyjaśnienie – zacznijmy od najprostszych.
Moduł Timed
znajduje się w przestrzeni nazw Apohllo::Acts::
-
dzięki temu unikamy problemu konfliktu nazw, który jest szczególnie
niebezpieczny w kontekście wielokrotnego wykorzystania kodu.
Zdefiniowanie tego modułu w globalnej przestrzeni nazw mogłoby
spowodować nieprzewidziane skutki, jeśli użytkownik w swojej aplikacji
również definiowałby moduł Timed
.
Druga kwestia dotyczy podziału reszty kodu na moduły:
wyraźnie oddzielone są metody klasowe (ClassMethods
– dostępne we wszystkich
klasach dziedziczących z rozszerzanej klasy, w tym wypadku ActiveRecord::Base
),
metody instancyjne (InstanceMethods
– pojawią się jako metody instancyjne,
w klasach, w których zostanie aktywowane to rozszerzenie) oraz
metody singletonowe (SingletonMethods
– pojawią się jako metody klasowe,
w klasach, w których zostanie aktywowane to rozszerzenie).
Metody – haki (hook)
Szczegóły działania mechanizmu są następujące.
Metoda self.included
jest wywoływana w momencie, gdy dany moduł jest
dołączany do jakiejś klasy. W wyniku jej wykonania, klasa ActiveRecord::Base
zostanie wzbogacona o metody klasowe znajdujące się w module
ClassMethods
. Dzieje się tak dzięki temu, że na parametrze mod
(który reprezentuje rozszerzany moduł lub klasę) wywołana jest metoda
extend
. Jej działanie jest podobne do działania słowa kluczowego
include
, z tą różnicą, że metody dołączanego modułu pojawiają się
jako metody klasowe a nie instancyjne. Pokazane jest to na poniższym
przykładzie:
module A def a "a" end end class B include A end class C extend A end b = B.new b.a #=> "a" B.a # NameError... c = C.new c.a # NameError... C.a #=> "a"
Teoretycznie zatem zamiast stosować hak (hook) included
,
można by w pliku init.rb
napisać:
class ActiveRecord::Base extend Apohllo::Acts::Timed::ClassMethods end
Efekt byłby taki sam. Zastosowano jednak wywołanie zwrotne, z
dwóch powodów: po pierwsze – dołączanie modułów za
pomocą dyrektywy include
jest bardziej naturalne i
uniezależnia nas od tego, jakich metod dostarcza ów moduł;
po drugie zaś – w metodzie included
można wykonać
pewien kod np. sprawdzający, czy w klasie do której
dołączany jest moduł, nie występują już metody o takich
samych nazwach jak metody modułu. Ma to szczególne znaczenie
w przypadku, gdy wykorzystywanych jest wiele pluginów i
istnieje niebezpieczeństwo zdublowania nazw. W tym wypadku
można po np. zgłosić błąd i przerwać wykonanie programu.
Przy okazji warto zwrócić uwagę, że istnieje wiele
podobnych metod – haków, np. inherited
jest wywoływana
w chwili gdy jakaś klasa dziedziczy z danej klasy,
zaś method_defined
, kiedy w danej klasie definiowana
jest nowa metody. Metody te mogę być również wykorzystywane
w zaawansowanych technikach metaprogramowania.
Metody instancyjne i klasowe
Wracając jednak do przedstawionego przykładu – w module
ClassMethods
zdefiniowana jest tylko jedna metod acts_as_timed
,
która służy właśnie do aktywowania tego rozszerzenia w wybranych
klasach. Jej wywołanie powoduje dołączenie (za pomocą dyrektyw
include
oraz extend
) odpowiednio metod instancyjnych i klasowych
do danej klasy. Ponadto definiowany jest atrybut klasowo-instancyjny
old
a także metoda pozwalająca na jego odczytanie. Na jego podstawie
obiektom danej klasy będzie przypisywany odpowiedni status.
Definicje metod instancyjnych i klasowych nie są zbyt interesujące –
zawierają zwykły kod, który dokonuje porównania odpowiednich
wartości czy też przekazania wywołania do systemu mapowania
obiektowo-relacyjnego (metoda find
). Atrybut created_at
zawiera datę utworzenia obiektu (jest ona utomatycznie
ustawiana przez framework), a porównanie jego wartości z aktualnym
czasem pozwala nam stwierdzić, czy dany obiekt jest nowy, czy stary.
Jest jednak jedna
subtelność, na którą należy zwrócić uwagę – w metodzie new?
pobierana jest wartość atrybutu klasowo-instancyjnego old
klasy danego obiektu. Ponieważ definiowane rozszerzenie
może być dołączane do różnych klas nie mamy innego sposobu
na dostanie się do tego atrybutu, niż przez jego klasę.
Wszystko działa świetnie pod warunkiem, że ograniczymy się
do obiektów tylko tej klasy. Jednakże jeśli będziemy tworzyć
obiekty klas z niej dziedziczących, to zdefiniowane metody
nie będą działać. Dlaczego?
Odpowiedź jest prosta, jeśli weźmiemy pod uwagę to gdzie przechowywana
jest wartość atrybutu old
– w obiekcie klasy. Atrybut ten
nie jest jednak dostępny w klasach z niej dziedziczących – są
to po prostu inne instancje klasy Class
! Jak zatem
rozwiązać ten problem? Otóż trzeba skorzystać z haka
inherited
– kiedy nasza klasa jest rozszerzana, w nowotworzonej
klasie musimy ustawić odpowiednią wartość parametru old
.
Co ciekawe – w frameworku Rails problem ten można łatwo rozwiązać
stosując makro write_inheritable_attribute
.
Widzimy zatem, że ten z pozoru nieistotny szczegół, ma jednak dosyć spore znaczenie. W istocie – metaprogramowanie wymaga nieraz wielkiej subtelności i wyczulenia na szczegóły.
Poniżej przedstawiamy możliwość wykorzystania tak zdefiniowanego pluginu:
class Newspaper < ActiveRecord::Base acts_as_timed :old => 1.month end class Car < ActiveRecord::Base acts_as_timed :old => 10.years def to_s new? ? "Cudowny samochód" : "Wrak" end end piatkowa_wyborcza = Newspaper.new(:title => "Wyborcza") wymarzony_samochod = Car.new(:name => "Chrysler 300D", :price => 40_000.euro) piatkowa_wyborcza.new? #=> true wymarzony_samochod.new? #=> true #po 40 dniach piatkowa_wyborcza.new? #=> false wymarzony_samochod.new? #=> true #po 10 latach wymarzony_samochod.new? #=> false wymarzony_samochod.to_s # "Wrak"
Na zakończenie warto wyjaśnić jeszcze jedną wątpliwość, która
mogła zrodzić się w kontekście przedstawionego przykładu – dlaczego
nie zostały w nim bezpośrednio wykorzystane techniki metaprogramowania
omówione wcześniej (w szczególności wywołanie define_method
)?
Odpowiedzieć na to pytanie można na dwa sposoby: po pierwsze –
zostały wykorzystane niektóre z prezentowanych metod, np.
wykorzystanie klasy singletonowej do zdefiniowania dostępu do atrybutów danej
klasy. Po drugie zaś – wykorzystanie wszystkich technik nie było
konieczne. Nasz plugin dostarczał metod, których nazwy były z góry określone.
Dzięki temu można było je umieścić w osobnych modułach (SingletonMethods
,
InstanceMethods
) i dołączyć za pomocą wywołań extend
oraz include
.
Takie rozwiązanie na pewno jest czytelniejsze, co nie znaczy, że nie
mogliśmy skorzystać bezpośrednio z wywołania define_method
.
Zawsze jednak powinno dążyć się do tego, żeby kod tworzony w ramach
metaprogramowania jak najbardziej przypominał zwykły kod – dzięki
temu zrozumienie tego co dzieje się “pod spodem” jest znacznie
łatwiejsze. W naszym mniemaniu dołączanie modułów zawierających
“zwykłe” definicje metod, jest czytelniejsze. Oczywiście
w pewnych przypadkach jest niewystarczające i wtedy nie można
uciec od stosowania metod define_method
, czy class_eval
.
Jeśli ktoś zaś zastanawia się gdzie powinien znaleźć się meta-kod, to oczywiście najlepszym miejscem dla niego jest owa metoda klasowa, która powoduje włącznie odpowiedniego rozszerzenia. Wszystko co występuje poza nią powinno jak najbardziej przypominać definicje zwykłych metod. Wtedy “magia” metaprogramowania jest skoncentrowana w małym obszarze kodu, dzięki czemu programiści, którzy nie do końca rozumieją tę technikę, mogą przynajmniej częściowo zrozumieć to jak działa dostarczony plugin.
Pluginy: Ruby/Rails a Java/Eclipse
W powyższym przykładzie staraliśmy się przedstawić możliwości jakie oferuje język Ruby w zakresie tworzenia i wykorzystania pluginów. Widzimy, że dynamiczne własności tego języka pozwalają w bardzo wygodny sposób definiować a także wykorzystywać pluginy. Dzięki otwartości klas, można modyfikować niemal dowolny fragment programu, zatem pluginy mogą oferować niezwykle zróżnicowane funkcjonalności. Co więcej – od twórcy platformy, na której będą działać rozszerzenia, nie wymaga się, aby przewidział wszystkie sposoby w jakie będą chcieli zmodyfikować ją jej użytkownicy.
Niebezpieczeństwa
Pytaniem pozostaje to, czy są jakieś ograniczenia dla wykorzystania pluginów, jakie niebezpieczeństwa czyhają na ich użytkowników?
Pierwsze niebezpieczeństwo bierze się właśnie z nieograniczonych możliwości modyfikacji – jeśli wykorzystamy dwa pluginy, które będą modyfikowały ten sam fragment systemu, to może dojść do sytuacji, gdy efekt ich działania stanie się zupełnie niezgodny z oczekiwaniami, w najgorszym przypadku prowadząc do nieprzewidywalnych błędów, które trudno się debuguje. Warto zwrócić uwagę, że meta-kod jest trudniejszy do zrozumienia, zatem jego debugowanie jest tym trudniejsze.
Drugie niebezpieczeństwo bierze się z niestabilności platformy, dla której tworzone są rozszerzenia. Jeśli nagle okaże się, że jakieś istotne jej szczegóły uległy zmianie, część działających rozszerzeń może okazać się zupełnie bezwartościowa. Zatem pluginy jest sens tworzyć tylko wtedy, gdy wiemy, że podstawowy projekt jest wystarczająco dojrzały i że czas poświęcony na ich tworzenie faktycznie może się zwrócić.
Framework Eclipse
Jak te cechy języka Ruby wypadają na tle Javy i najbardziej popularnej platformy wspierającej pluginy dla tego języka, czyli frameworku Eclipse?
Aby odpowiedź na to pytanie nie była zbyt powierzchowna, powinniśmy w wielkim skrócie przedstawić ów system pluginów. Jest on zbudowany na bazie technologii OSGi (która w Eclipse kryje się pod nazwą Equinox), definiującej standardy dynamicznych modułów dla języka Java. W wielkim skrócie można powiedzieć, że zawiera ona definicje mechanizmów pozwalających na dynamiczne znajdowania, uruchamianie i zatrzymywanie, a także określanie zależności pomiędzy (najlepiej) reużtkowalnymi fragmentami kodu, zwanymi modułami (module), paczkami (bundle), bądź pluginami.
Najważniejszym elementem definiowanym w ramach OSGi jest tzw. manifest, czyli plik, w którym określane są komponentowe własności danego “kawałka” kodu. Zawiera on m.in. listę importowanych i eksportowanych pakietów, unikalny identyfikator komponentu, listę wymaganych komponentów, etc. Wszystkie te informacje pozwalają w precyzyjny sposób określić środowisko w jakim działa dany komponent. Jak można się domyślić, wszystkie te własności mają szczególne znaczenie w rozwiązaniach klasy Enterprise, gdzie często wymaga się, aby poszczególne fragmenty oprogramowania można było traktować jako wymienne klocki, które funkcjonują w ściśle określonym ekosystemie. Tym niemniej własności zdefiniowane w manifeście niewiele mówią nam o tym, jakich funkcjonalności może dostarczać określony plugin.
Znacznie ważniejszy, z naszego punktu wiedzenia, jest manifest plugin.xml
,
który jest już specyficzny dla frameworku Eclipse. To co jest w nim
najważniejsze z naszego punktu widzenia, to tzw. rozszerzenia
(extensions) oraz punkty rozszerzeń (extension points).
Pierwsze z nich określają, że dany plugin dostarcza określonych
rozszerzeń dla platformy. Mogą to być elementy graficznego
interfejsu użytkownika (np. pozycje w menu głównym i kontekstowym,
widoki, edytory, itp.) a także inne elementy pozwalające rozszerzać
platformę Eclipse.
Rozszerzenia zawsze definiowane są w ramach punktów rozszerzeń – te drugie określają jakie atrybuty powinno posiadać dane rozszerzenie, aby mogło być wykorzystane w platformie. W przypadku pozycji w menu, będzie to jej ścieżka, tytuł, a także kod (klasa), który zostanie wywołany w momencie jej wybrania. Punkty rozszerzeń i rozszerzenia są komplementarne – istnienie jednych nie ma sensu bez istnienia drugich. Punkt rozszerzeń jest jakby miejscem, w którym możemy coś zmodyfikować. Rozszerzenie zaś jest konkretną modyfikacją. Zazwyczaj stworzenie rozszerzenie wiąże się z dodaniem pewnych elementów w GUI, które pozwalają na jego wykorzystanie (okno dialogowe, wizard, pozycja w menu) oraz zaimplementowanie pewnego interfejsu lub rozszerzenie klasy w celu dostarczenia określonej funkcjonalności.
Porównanie
Już na pierwszy rzut oka widać, że rozwiązanie takie jest mało elastyczne. Dostarczyciel pluginu może dostarczyć tylko takie rozszerzenie, jakie zostało przewidziane przez twórcę systemu. Jeśli zaś modyfikacje miałyby obejmować jakieś elementy, które zostały przeznaczone do modyfikacji, jedyny sensowny sposób ich ulepszenia, polega na napisaniu ich od nowa, co najwyżej z możliwością wykorzystania mechanizmu dziedziczenia. Oczywiście w systemie, w którym zależności pomiędzy poszczególnymi elementami są bardzo złożone (np. istnieje łańcuch zależnych od siebie pluginów) próba modyfikacji pierwszego pluginu w taki sposób, aby została ona rozpropagowana do pluginów zależnych, może skończyć się fiaskiem, a w konsekwencji uniemożliwić ponowne wykorzystanie kodu.
Z drugiej strony system tego rodzaju unika problemów które wskazaliśmy w stosunku do frameworku Ruby on Rails – nie ma możliwości stworzenia rozszerzeń pozostających w konflikcie (o ile tylko posiadają odmienne identyfikatory, ale zapewnienie tego wymagania jest tak samo proste/trudne jak zapewnienie globalnie unikalnej struktury pakietów, ponieważ do nazywania rozszerzeń wykorzystuje się podobny mechanizm jak do nazywania pakietów). Maleje również ryzyko sytuacji, w której twórca platformy dokonuje zmian, które uniemożliwiają wykorzystanie już stworzonych pluginów. Można się spodziewać, że skoro został zdefiniowany określony punkt rozszerzeń, to ten fragment systemu pozostanie stabilny, przynajmniej taka powinna być intencja jego twórcy.
Podsumowując te rozważania można skonstatować co następuje: język Ruby, a także zbudowany na jego bazie framework Rails pozwala na bardzo dużą elastyczność w ponownym wykorzystaniu kodu. Z drugiej strony reużytkowalne elementy powinny być stosunkowo niewielkie, aby ograniczyć ryzyko związane z ich dezaktualizacją w wyniku zmian platformy, na której są wykorzystywane.
Z drugiej strony – Java i platforma Eclipse pozwalają tworzyć bardziej stabilne i większe elementy, czego nieprzyjemną konsekwencją jest fakt, że często będą one w całości nieprzydatne. Od twórców samej platformy wymaga się zaś, aby bardzo starannie ją zaprojektowali, a także przewidzieli wszystkie istotne kierunki, w których może być ona rozszerzana. To zadanie jest jednak często bardzo trudne w realizacji.
Ostatnia uwaga jaka mi się nasuwa dotyczy związku tych języków i technik z dwoma przeciwstawnymi modelami tworzenia oprogramowania. Oczywiście Ruby będzie po stronie technologii zwinnych, skoncentrowanych na podążaniu z zmianami w wymaganiach. Java natomiast znacznie lepiej sprawdza się w modelu klasycznym, gdzie etap implementacji poprzedzony jest starannym projektowaniem aplikacji.
Poprzedni rozdział | Następny rozdział