The service provided by Consileon was professional and comprehensive with a very good understanding of our needs and constrains.

Wolfgang Hafenmayer, Managing partner, LGT Venture Philanthropy

Technical quality of staff offered, capability of performing various project roles, as well as motivation and dedication to the project (... [...]

dr Walter Benzing, Head of development B2O, net mobile AG

Technical quality of staff offered, capability of performing roles of developers, lead developers, trainers, team leaders, architects as wel [...]

Karl Lohmann, Itellium Systems & Services GmbH

Firma Consileon Polska jest niezawodnym i godnym zaufania partnerem w biznesie, realizującym usługi z należytą starannością (...)

Waldemar Ściesiek, Dyrektor zarządzający IT, Polski Bank

The team was always highly motivated and professional in every aspect to perform on critical needs of our startup environment.

Denis Benic, Founder of Ink Labs

Rails i Train Wrecks

Category: Other Tags: ,

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 progra­muje 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


Tags: ,

Comments

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Trwa ładowanie