Rails i Train Wrecks
Michał Orman - 29 marca 2010
Frameworki ORM takie jak Hibernate czy ActiveRecord pozwalają nam w dość naturalny sposób przechodzić pomiędzy zależnościami modeli (encji). Wystarczy po kropce dodać nazwę atrybutu i gotowe. Niestety takie podejście kończy się tym, że wywołania kolejnych zależności ciągną się w nieskończoność:
user.profile.address.city.zip_code
Takie konstrukty nazywamy z angielska train wreck. Nie jest to dobre podejście z punktu widzenia programowania obiektowego, gdyż ujawnia zbyt dużą ilość informacji na temat wewnętrznej implementacji obiektu. Po co nam informacja w jaki sposób klasa przechowuje kody pocztowe?
Aby ulepszyć naszą klasę i poprawić jej hermetyzację musimy skorzystać z mechanizmu delegacji. Moglibyśmy w modelu User
utworzyć metodę delegującą wywołanie metody zip_code
do modelu Profile
:
class User < ActiveRecord::Base
has_one :profile
def zip_code
self.profile.zip_code
end
end
W modelu Profile
zdefiniowalibyśmy delegację do Address
a dalej do City
. W każdym z tych modeli (poza City
) musimy utworzyć metodę delegująca, aby ostatecznie dobrać się do kodu pocztowego. Dzięki mechanizmowi delegacji zamiast wcześniejszego łańcuszka możemy wykonać:
user.zip_code
W tym momencie wewnętrzna implementacja klasy User
jest przed nami ukryta.
Jak się jednak okazuje framework Rails daje nam inny, bardziej deklaratywny sposób definiowania delegacji.
Dyrektywa delegate
Dyrektywa delegate
pozwala nam zadeklarować delegację, bez potrzeby tworzenia faktycznej metody:
class User < ActiveRecord::Base
has_one :profile
delegate :zip_code, :to => :profile
end
Dzięki temu nie tylko zaoszczędziliśmy nieco linii kodu i parę klepnięć w klawiaturę, ale i definicja naszej klasy staje się czytelniejsza, ponieważ nie zaśmiecamy jej metodami nie do końca biznesowymi. Poza tym taka deklaracja jawnie mówi nam, że chcemy delegować komunikat. Deklaratywność ta przydaje nam się, jeżeli chcemy dodać delegację kolejnego komunikatu. Po prostu dodajemy go do listy:
class User < ActiveRecord::Base
has_one :profile
delegate :zip_code, :zip_code=, :to => :profile
end
Zamiast tworzyć dwie metody deklarujemy delegacje w jednym wyrażeniu. Co ciekawe, możemy nawet pominąć konieczność powtarzania delegacji w kolejnych klasach i od razu zadeklarować delegację z modelu User
do City
:
class User < ActiveRecord::Base
has_one :profile
delegate :zip_code, :zip_code=, :to => 'profile.address.city'
end
Niestety to podejście pozostawia nam train wrecka i wywleka wewnętrzną implementację modelu Profile
na wierzch, stąd nie polecam tego podejścia.
To jeszcze nie koniec. Załóżmy, że nasz model User
posiada atrybut name
i taki sam atrybut posiada model City
. Co jeżeli chcielibyśmy delegować pobieranie nazwy miasta w postaci komunikaty city_name
? Rails udostępnia nam taką możliwość:
class User < ActiveRecord::Base
has_one :profile
delegate :zip_code, :zip_code=, :to => :profile
delegate :name, :to => :profile, :prefix => :city
end
Jest tutaj pewna subtelna pułapka. Mianowicie o ile zadeklarowaliśmy, że komunikat city_name
ma być delegowany do modelu Profile
(a dalej aż do City
), to komunikat wysłany do tego modelu będzie bez prefiksu, czyli name
. Oznacza to, że nasz model pośredniczący Profile
musi delegować komunikat name
bez prefiksu city_
:
class Profile < ActiveRecord::Base
has_one :address
belongs_to :user
delegate :zip_code, :zip_code=, :to => :address
delegate :name, :to => :address
end
Podobna sytuacja jest w modelu Address
. Co ważne ani Profile
ani Address
nie mogą definiować metody name
, gdyż ta metoda zostanie wywołana zamiast oddelegowania do kolejnego modelu.
Podsumowanie
Poprawna enkapsulacja, czyli hermetyczne odseparowanie implementacji klasy od świata zewnętrznego pozwala nam tworzyć kod, w którym klasy są luźniej powiązane i zmiany w nich nie niszczą innych części systemu. Delegacja pełni bardzo ważną rolę w zapewnianiu poprawnej hermetyzacji. Framework Rails pozwala nam w bardziej deklaratywny sposób zarządzać delegacjami. Nie musimy tworzyć żadnych metod, a w czytelny sposób deklarujemy, iż chcemy delegować wywołanie danego komunikatu do kolejnego obiektu.
http://michalorman.pl/blog/2010/03/rails-i-train-wrecks/
Michał Orman
Full stack software developer, konsultant IT.
Niekwestionowany full stack developer, architekt rozwiązań IT, project manager, entuzjasta Agile z biznesowym zacięciem, który lubi robić rzeczy do końca. Prawdziwy człowiek orkiestra, którego największymi zaletami są nieustanne dążenie do perfekcji, produktywność, dbanie o najwyższą jakość wyprodukowanego oprogramowania. Obecnie programuje w Ruby on Rails i JavaScript, ale do listy znanych mu technologii bez wątpienia można dopisać Java/J2EE. Tworzył aplikacje mobilne na platformę Android, systemy wbudowane w C/C++. Zapalony wyznawca zasad SOLID lubiący proste rozwiązania oraz czysty kod. Do swojego portfolio technologicznego również może dodać Unix/Linux, VIM oraz Git.
michalorman.com
Comments