Rails i Train Wrecks
2010-03-29 · ruby · rubyonrails
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ść:
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
:
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ć:
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:
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:
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
:
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ść:
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_
:
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/
Autor: Michał Orman
Full-stack web developer, software and solution architect, project manager, agile enthusiast and professional with enterprise background that loves getting things done. Productivity, constant improvement, highest possible quality are his attitudes.
Currently he is working with Ruby on Rails stack and JavaScript but his background includes enterprise development in Java/J2EE, mobile development for Android platform and embedded development in C/C++ for telecom.
He is an avid follower of SOLID principles, that loves simple design. Nothing makes him more happy than simple interfaces, small methods and clean code. His toolkit includes UNIX/Linux systems, VIM and of course Git.