poniedziałek, 20 kwietnia 2015

Where's the law?

z obcymi (nie) za pan brat

Jeżeli chcielibyśmy ująć kwintesencję Prawa Demeter (Law of Demeter) w jednym zdaniu to brzmiałoby ono: "rozmawiaj tylko z (bliskimi) przyjaciółmi".
W pełnej formie mówi ono o tym, że metoda danego obiektu może odwoływać się jedynie do metod należących do:
  • tego samego obiektu,
  • dowolnego parametru przekazanego do niej,
  • dowolnego obiektu przez nią stworzonego,
  • dowolnego składnika, klasy do której należy dana metoda.

Definicję mamy za sobą, a teraz zastanówmy się dlaczego to w ogóle ma sens. I jakie konsekwencje niesie za sobą nie przestrzeganie tego prawa.

sam ze sobą

Jeżeli wywołujemy metodę jakiegoś obiektu to powinna ona mieć możliwość korzystania zarówno z innych metod jak i z atrybutów tego obiektu, bez względu na ich widoczność. W końcu jest to jedna, spójna całość, która jako całość powinna funkcjonować.

Załóżmy jednak, że metoda takich możliwości nie ma. Z czym wtedy mamy do czynienia? Jeżeli wewnątrz metody moglibyśmy korzystać jedynie z publicznego API danego obiektu to musielibyśmy przestać myśleć o niej (metodzie) jak o części pewnej całości. W takim wypadku część stała by się metodą zależną od danego obiektu, a więc mówilibyśmy o zależności.

Warto również mieć na uwadze to, jakie wskazówki daje nam informacja, gdy metoda nie korzysta z żadnych innych metod/atrybutów danego obiektu. Jeżeli tak się dzieje i nie ma części wspólnej to może doszło do naruszenie Single Responsibility Principle?

to co dostanę

Pewne metody wymagają przekazania parametrów wejściowych, aby przeprowadzić pożądane operacje (pomyślcie o podstawowych działaniach matematycznych) i otrzymać rezultat musimy określić parametry.
Gdyby jednak nie było możliwości tworzenia metod sparametryzowanych to skończylibyśmy z iście boskimi obiektami, które musiałyby zawierać całą pulę metod, które w obecnie nam znanych kodach są z nią w jakiś sposób powiązane.
Kolejnym negatywnym następstwem byłaby niesamowita duplikacja kodu. W dodatku wielokrotna. I zapewne Ctrl-C + Ctrl-V Pattern byłby wtedy ulubionym wzorcem każdego programisty, tyle, że tym razem w pełni uzasadnionym :)

w danym kontekście

Zajmijmy się teraz punktem trzecim. Niekiedy zapisanie wyników pośrednich bądź stopniowe budowanie odpowiedzi to konieczność. Oczywiście z niektórych zmiennych rzeczywiście dałoby się zrezygnować, ale po pierwsze, nie ze wszystkich, a po drugie, w wielu przypadkach rzutowałoby to negatywnie na czytelność kodu.

sąsiedzie mój, gdzie jest sąsiad sąsiada?

W definicji jest mowa o rozmowie z „najbliższym sąsiadem” co przekłada się w kodzie na to, że powinniśmy korzystać jedynie z metod należących do obiektów wcześniej wymienionych.
I w tym momencie docieramy do akcesorów, które niechybnie metodami są, ale… dlaczego nie?

Jeżeli mamy potrzebę dobrania się do któregoś z atrybutów obiektu, z którym mogę współpracować, to jest jednoznaczne z wyciąganiem od niego informacji na temat jego detali dotyczących implementacji. A jak wiadomo powinniśmy zależności opierać na zachowaniu, a nie implementacji. Dobra, powyższy argument jednak wiele osób może potraktować jako powód stricte teoretyczny, ot, taka sztuka dla sztuki. Jakie jednak realne problemy niesie za sobą zaglądanie we wnętrzności danych obiektów?
Jesteśmy w stanie określić kontrakt, który muszą wypełnić wszystkie obiekty, które zostaną przekazane do metody, odpowiedni typ parametru nam to gwarantuje. Sprawa jednak się komplikuje gdy zaczynamy sięgać głębiej, po atrybuty tych obiektów. W jaki sposób możemy zagwarantować, że te dalsze zależności będą miały pożądane przez nas API? Poinformujemy o tym w komentarzu dotyczącym danej metody? Nie jest to raczej rozwiązanie nas satysfakcjonujące. Dlatego też zamiast konstrukcji podobnych do:
sebastian.getSkills().contains(OOP);

lepiej zamienić kod na coś takiego:
sebastian.hasSkill(OOP);

Jeszcze gorzej gdy nasze zależności są zorganizowane wokół dłuższych ciągów wywołań:
sebastian.getSkills().get(OOP).increase();

i tak dalej. Refaktoryzacja w takiej sytuacji wymaga większego nakładu pracy, ale też da się to zrobić i kod z pewnością zyska na czytelności, a do nas z powrotem „wróci” możliwość wymuszenia odpowiedniego API.

Dobrze, ale co w przypadku atrybutów obiektu oraz zmiennych lokalnych? Przecież są sytuacje gdy jako ich typ wykorzystujemy określoną klasę, więc w pewnym stopniu opieramy się nie tylko na zachowaniu, ale również na implementacji?
W takich sytuacjach warto zdać sobie sprawę, że korzystając z wiedzy na temat budowy danego komponentu umacniamy zależność pomiędzy obiektami. Dopóki opierają się one jedynie na zachowaniu to są łatwo wymienialne, gdy dotykamy już ich budowy, to ta wymiana nie jest już taka prosta.
Poza tym, korzystanie z atrybutów tych obiektów (poprzez gettery bądź bezpośredni dostęp) zwiększa ilość zależności naszego obiektu. Najgorsze jednak jest to, że są to zależności niejawne, zależności, o których dowiemy się dopiero czytając kod danej metody, czyli krótko mówiąc trudne do znalezienia.

I na koniec ostatni argument. Przestrzeganie tego prawa zwiększa czytelność kodu. Popatrzcie na przykład z tego paragrafu. Która wersja jest czytelniejsza? A to się przekłada na mniejszy wysiłek w przyszłości, gdy będziemy musieli wrócić do tego kodu i dokonać zmian. Mniej rzeczy do analizy oraz mniej skakania po klasach.

czy zależy Ci na relacjach?

Tak więc, jeżeli chcecie aby Wasza metoda była w relacjach z jak największą liczbą innych obiektów, w dodatku w relacjach, o których nawet możecie nie mieć pojęcia, to nieprzestrzeganie LoD jest jedną z najlepszych dróg do tego. Pamiętajcie jednak o konsekwencjach utrzymywaniu tylu „znajomości” i o tym, jak wpływa to na kruchość Waszego kodu.