Powiedz, nie pytaj czyli Prawo Demeter
|Programując obiektowo, możesz z czasem zauważyć, że bardzo często odpytujesz obiekt i na podstawie tej odpowiedzi podejmujesz jakieś działania. Okazuje się, że nie jest to do końca zgodne z teorią „czystego kodu”.
Alec Sharp w swojej książce (Smalltalk by Example) pisze (pozwoliłem sobie przetłumaczyć):
Kod proceduralny dostaje informacje, a następnie podejmuje decyzje. Kod obiektowy mówi obiektom co mają robić.
Parafrazując: powinno się dążyć do tego, aby mówić obiektom co my chcemy, aby one zrobiły zamiast: najpierw je odpytywać o stan, potem sprawdzać warunek i na tej podstawie kazać im coś zrobić. Dość prosta zasada. Najlepszym sposobem na wyjaśnienie, będzie zaprezentowanie kilku praktycznych przykładów:
Przykład 1:
Nie za dobrze:
if ($user->isAdmin()) { $message = $user->admin_text; } else { $message = $user->user_text; }
Lepiej:
$message = $user->getMessage();
Przykład 2:
Nie za dobrze:
$limit = $downloadLimiter->getMaxLimit(); if ($limit > $currentVolume) { throw new Exception("Limit exceeded!"); }
Lepiej:
$downloadLimiter->checkMaxLimit();
Przykład 3:
Nie za dobrze:
class Post { public function send($user, $content) { if ($user instanceof FacebookUser) { $user->sendMessage($content); } else if ($user instanceof EmailUser) { $user->sendEmail($content); } } }
Lepiej:
class Post { public function send($user, $content) { $user->send($content); } } class FacebookUser { public function send($content) { $this->sendMessage($content); } } class EmailUser { public function send($content) { $this->sendEmail($content); } }
Prawo Demeter
Przy opisanej wyżej technice warto wspomnieć o czymś co nazywamy Prawem Demeter (źródło wikipedia):
Prawo Demeter mówi, ż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
Częstą sytuacją, która może sugerować, że Twój kod łamie powyższe punkty, jest otrzymywanie błędu typu: Trying to get property of non-object. Mógłbym nawet pokusić się o nazwaniem tego „zapachem” (jest pomysł na inny wpis, może nawet serię wpisów: „zapachy kodu php” ?).
Najlepiej będzie przedstawić kolejny przykład. Poniższy kod prezentuje 3 klasy: User, Avatar oraz Photo. Jest to prosta struktura: User ma pod sobą Avatar’a który z kolei ma pod sobą Photo. Do tego dochodzi kilka geterów. Następnie (w kodzie) wskrzeszamy odpowiednie obiekty, wydobywamy składniki i wyświetlamy tekst na ekranie. Całość wygląda tak:
class User { public function __construct($name, Avatar $avatar = NULL) { $this->name = $name; $this->avatar = $avatar; } public function getAvatar() { return $this->avatar; } } class Avatar { public function __construct($title, Photo $photo = NULL) { $this->title = $title; $this->photo = $photo; } public function getTitle() { return $this->title; } public function getPhoto() { return $this->photo; } } class Photo { public function __construct($path) { $this->path = $path; } public function getPath() { return $this->path; } } $user = new User('Arkadiusz', new Avatar('itcraftsman', new Photo('files/logo.jpg'))); $title = ''; $path = ''; if ($user->getAvatar()) $title = $user->getAvatar()->getTitle(); if ($user->getAvatar() && $user->getAvatar()->getPhoto()) $path = $user->getAvatar()->getPhoto()->getPath(); echo 'Zdjęcie ' . $title . ' jest w ' . $path;
W prosty sposób zastosujemy Prawo Demeter na powyższym przykładzie. Jak widać tytuł wydobywany jest w sposób, który łamie pierwszy podpunkt prawa ($title = $user->getAvatar()->getTitle()). Dopiszmy teraz nową metodę getTitle() do klasy User:
public function getTitle() { if ($this->avatar) { return $this->avatar->getTitle(); } }
Następnie pozbędziemy się zagłębionego łańcucha ($user->getAvatar()->getPhoto()->getPath()) odpytującego o ścieżkę do pliku. Zaczniemy od dopisania metody getPhotoPath() do klasy Avatar:
public function getPhotoPath() { if ($this->photo) { return $this->photo->getPath(); } }
W ten sposób, nie łamiąc tytułowego prawa, klasa Avatar może wydobyć ścieżkę z obiektu klasy Photo. Pozostaje nam jeszcze skonstruowanie analogicznej metody dla klasy User, abyśmy mogli wydobyć z niej bezpośrednio całą ścieżkę do pliku. Dlatego dodajemy kolejną metodę getPhotoPath() do klasy User:
public function getPhotoPath() { if ($this->avatar) { return $this->avatar->getPhotoPath(); } }
Na końcu możemy pozbyć się warunków sprawdzających i wydobyć potrzebne dane bezpośrednio z obiektu $user:
$title = $user->getTitle(); $path = $user->getPhotoPath();
W ten sposób udało nam się pozbyć wszystkich niedozwolonych odwołań w klasach i nasz kod stał się czystszy. Poniżej kompletny kod po zmianach:
class User { public function __construct($name, Avatar $avatar = NULL) { $this->name = $name; $this->avatar = $avatar; } public function getAvatar() { return $this->avatar; } public function getTitle() { if ($this->avatar) { return $this->avatar->getTitle(); } } public function getPhotoPath() { if ($this->avatar) { return $this->avatar->getPhotoPath(); } } } class Avatar { public function __construct($title, Photo $photo = NULL) { $this->title = $title; $this->photo = $photo; } public function getTitle() { return $this->title; } public function getPhoto() { return $this->photo; } public function getPhotoPath() { if ($this->photo) { return $this->photo->getPath(); } } } class Photo { public function __construct($path) { $this->path = $path; } public function getPath() { return $this->path; } } $user = new User('Arkadiusz', new Avatar('itcraftsman', new Photo('files/logo.jpg'))); $title = $user->getTitle(); $path = $user->getPhotoPath(); echo 'Zdjęcie ' . $title . ' jest w ' . $path;
Podsumowanie:
Na zakończenie warto wspomnieć, że korzystanie z Prawa Demeter ma też swoje wady. Jak widać w powyższym przykładzie, aby osiągnąć ten sam efekt (na wyjściu) musieliśmy dopisać trzy nowe metody. Wydaje się więc, że kod stanie się bardziej zagmatwany i pełen niepotrzebnych metod.
Z drugiej jednak strony, stosowanie wspomnianego prawa prowadzi do zmniejszenia zależności i pozwala na pisanie bardziej elastycznego kodu. Dzieje się tak dlatego, ponieważ kod wywołujący daną metodę nie potrzebuje wiedzy na temat struktury obiektu. Umożliwia to łatwą zmianę struktury obiektu, bez potrzeby przepisywania kodu korzystającego z jego metod.
W razie pytań lub wątpliwości czekam na Wasze komentarze pod wpisem 🙂 Możecie mnie znaleźć równie przez Twittera: @ArkadiuszKondas
Zdjęcie z wpisu: Flickr.