Jak zrobić mechanizm pluginów w Ruby
wtorek, grudzień 12th, 2006 by Paweł RutkowskiPodczas pracy przy ostatnim projekcie napisałem kilka programów w Ruby. Jeden z nich był frameworkiem udostępniającym pewne API. Głównym zadaniem tego frameworku było usprawnienie operacji na bazie SQL które co jakiś czas musieliśmy wykonywać ręcznie - co było bardzo pracochłonne i wymagało wielkiej uwagi.
Założenia dla frameworku były następujące:
- udostępniać minimalną funkcjonalność - wspólną dla innych operacji
- obsługiwać pluginy które realizowały by poszczególne operacje
- pluginy powinny “myśleć” za użytkownika, tzn weryfikować dane ew. podpowiadać dostępne rozwiązania (ale to będzie temat na oddzielny wpis)
Musiałem zatem wymyślić jakiś mechanizm pluginów. Ponieważ Ruby jest językiem bardzo elastycznym nie było z tym większego problemu.
Pierwszą rzeczą było stworzenie klasy Action - z której będą dziedziczyć wszystkie pluginy. Następnie zaimplementowałem dwie metody: self.description i self.run. Pierwsza z nich była odpowiedzialna za wyświetlanie opisu pluginu, druga zaś uruchamiała właściwy plugin. Ponieważ klasa Action sama w sobie nie była wykorzystywana powyższe metody zwracały tylko informacje że należy je zaimplementować w klasie dziedziczącej.
Drugą rzeczą było udostępnienie jednego z obiektów dla każdego z pluginów. Tutaj sprawa nie była skomplikowana - wystarczyło dodać parametr do konstruktora, przypisać do zmiennej zaś przy tworzeniu obiektu przekazać odpowiednią zmienną.
Oto jak wygląda w uproszczeniu ta klasa
class Action
def initialize(api_object)
@api = api_object
end
def self.description
puts "desc method have to be overriden"
end
# method execute to run plugin
def run
puts "run method have to be overriden"
end
end
Narazie prosto prawda ? No to teraz trzeba było by jakoś ładować pluginy do aplikacji. Stwierdziłem że najlepszą metodą będzie utworzenie katalogu plugins i wczytywanie wszystkich plików które się tam znajdują (z definicją nowej klasy która de facto jest pluginem). Do tego posłuzyłem się następującym kawałkiem kodu:
def load_actions
Dir["plugins/*.rb"].sort.each { |plugin|
require plugin
}
end
I zaraz potem trafiłem na problem. Jak wyciągnąć klasy które dziedziczą z innej klasy ? Pomimo że ruby ma bardzo dobrze rozwinięte “reflections” (jest to możliwość dynamicznego metaprogramowania - np.: sprawdzanie dostępnych metod w obiektach, informacji o zmiennych, ich typach itp) to nie udało mi się znaleźć funkcji oferującej taką funkcjonalność. Trochę czasu mi zajęło przejrzenie metod dostępnych w klasach Object, ObjectSpace, Class i wyszukanie tych z których można skorzystać. Oto jak wyglądał kod (a pod spodem jego omówienie).
class Class
def self.find_by_super(sclass)
ret = []
ObjectSpace.each_object(Class) { |klass|
ret << klass if klass.superclass == sclass
}
return ret
end
end
Jak widać powyżej do klasy Class została dodana metoda find_by_super która jako parametr przyjmuję klasę nadrzędną w stosunku do klas które szukamy. Następnie przechodzimy przez wszystkie obiekty typu Class (czyli wszystkie klasy) i sprawdzamy które z nich dziedziczą z klasy określonej w parametrze funkcji.
Właśnie podczas pisania tego postu zauważyłem że można zrobić z powyższego kodu “jednolinijkowca”.
def self.find_by_super(sclass)
ObjectSpace.each_object(Class).find_all { |klass| klass.superclass == sclass }
end
Następnie należało już tylko dokonać iteracji po pluginach i wyświetlić ich listę w celu dokonania wyboru. Trick polegał na załadowaniu listy pluginów do tablicy, ich wyświetlenie a następnie zinterpretowanie wejścia użytkownika jako indeksu do konkretnego miejsca w tablicy, wskazującego na plugin który go interesuje.
[...]
# szukamy klass < Action
@actions = Class.find_by_super(Action)
[...]
# licznik na 0
n=0
actions.each { |action|
# dla kazdej akcji wywolujemy jej metode
# description i pokazujemy na ekranie
puts "#{n} - #{action.description}"
# zwiekszamy licznik ![]()
n=+1
}
[...]
printf("Please choose action: ")
# odczytujemy co podal user
@user_choose = $stdin.gets.to_i
# tworzymy obiekt na podstawie wybranej klasy i
# do konstruktora przekazujemy @api
action = @actions[user_choose].new(@api)
[...]
action.run
Proste ? Pewnie po przeczytaniu tego artykułu tak
Natomiast dla mnie było to swego rodzaju wyzwanie, które trochę czasu zajęło. Było to jednak ciekawe zagadnienie a dzięki Ruby i mozliwością tego języka bardzo przyjemne do zaimplementowania. Oczywiście cały framework oferował znacznie więcej i pojawiło się wiele innych problemów, ale nie chciałem ich tu zamieszczać żeby nie zaćmić ogólnej ideii.