W jednym z poprzednich artykułów mówiliśmy o nowym mechaniźmie języka php, którym są traits. Wprowadzenie ich wywołało dużo kontrowersji. Ten artykuł ma na celu próbę pokazania, że traits faktycznie mogą okazać się przydatne.
Szukając materiałów na prezentacje o traits, natknąłem się w sieci na ciekawy przykład zastosowania. Chodziło o trait, który umożliwiał zwrócenie obiektu w postaci JSON’a. Przykład widoczny poniżej jest dosyć prosty i można mieć do niego zastrzeżenia, ale nie to przykuło moją uwagę.
trait JSONized { public function toJson() { $properties = get_object_vars($this); return json_encode($properties); } }
Oryginalny artykuł dostępny jest tutaj
„We need to go deeper”
To co widzieliśmy przy okazji JSONized to mapowanie obiektu na określoną strukturę. W tym przypadku był to JSON, ale przecież może być to cokolwiek innego… co powiecie na tabele w bazie danych ? A może tak implementacja Active Record bez narzuconej hierarchii dziedziczenia !?
trait ActiveRecord { public function save() { /*...*/ } protected function update() { /*…*/ } protected function insert() { /*…*/ } public function delete() { /*...*/ } } class Article extends Whatever { use ActiveRecord; ... }
Moim zdaniem ma to wielki sens i świetnie wpasowuje się w to co chcemy przedstawić, w ten wycinek rzeczywistości, który chcemy zinterpretować za pomocą kodu. Wróćmy do Active Record i powyższego przypadku, ale bez traits. Czym jest Artykuł ? Czy na prawdę jest rekordem ? Albo czym jest inny obiekt, który chcemy zmapować, powiedzmy Użytkownik – czy on też dziedziczy po rekordzie ? Otóż nie ! To jest z góry bez sensu logicznie, ma nijak do rzeczywistości. On jest rekordem, bo inaczej nie zaimplementujemy Active Record, ale to co chcemy na prawdę zrobić to sprawić, żeby Artykuł, Użytkownik i inne mapowane obiekty zachowywały się jak rekord. Takie zastosowanie traits pozwala na uniknięcie głównego grzechu Active Record, czyli narzuconej hierarchii dziedziczenia.
Od zera do traits
Prześledźmy od początku próbę napisania w miarę sprawnego i dobrze zaprojektowanego logowania do jednej z klas aplikacji. Spójrzmy na poniższy kod:
class IAmUsingLogging { public function doSomething() { $this->log('I am about to do something'); // I am doing something $this->log(”I've done it!”); } private function log($message) { // I am doin logging } }
Kod zadziała, ale czy jest to dobry projekt ? Co jeżeli inna klasa też chciała by logować swoje akcje ? Mamy duplikować kod ? Logowanie jest na tyle odrębnym zestawem operacji, że zasługuje na własną klasę, do której należałoby oddelegować wykonywanie logowania. Od razu uspokajam – jeszcze nie wprowadzmy traits, czyli absolutnie nie robimy czegoś takiego jak poniżej.
trait Logging { private function log($message) { // I am doin logging } } class IAmUsingLogging { use Logging; public function doSomething() { $this->log('I am about to do something'); // I am doing something $this->log(”I've done it!”); } } class IAmUsingLoggingToo { use Logging; public function doSomething() { $this->log('I am about to do something'); // I am doing something $this->log(”I've done it!”); } }
To jest przykład na coś co może się stać kiedy ludzie którzy nie mają za dużo pojęcia o wzrocach projektowych, o klasach usługowych, kompozycji, zaczną używać traits.
To co powinniśmy zrobić to stworzyć osobną klasę loggera i do niej oddelegować metody logujące.
interface LoggerInterface { public function log($message); } class Logger implements LoggerInterface { public function log($message) { // I am doin logging } } class IAmUsingLogging implements LoggerInterface { private $logger; public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } private function log($message) { $this->logger->log($message); } public function doSomething() { $this->log('I am about do do something'); // I am doing something $this->log(”I've done it!”); } }
Nasz wycinek aplikacji zaczyna mieć ręcę i nogi. Logger robi ciężką robotę, klasa która logowania potrzebuje oddelegowuje do niego zadania. Dla porządku, zarówno Logger jak i nasza klasa implementują ten sam interfejs. I nagle do gry wchodzi kolejna klasa która chce używać logowania.
class IAmUsingLoggingToo implements LoggerInterface { private $logger; public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } private function log($message) { $this->logger->log($message); } public function doSomething() { $this->log('I am about do do something'); // I am doing something $this->log(”I've done it!”); } }
Sytuacja nie jest krytyczna. Ciężką robotę nadal wykonuje Logger. Widzimy jednak, że musimy powtórzyć 3 elementy. Ich implementacja jest banalna, jednak zawsze są to 3 elementy na każdą klasę wymagającą logowania. Sądzę, że do takich przypadków – pole przechowujące klasę usługową, setter, metody wynikające z interfejsu klasy usługowej – traits nadają się znakomicie. Spójrzmy:
trait Logging { private $logger; public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } public function log($message) { $this->logger->log($message); } } class IAmUsingLogging implements LoggerInterface { use Logging; public function doSomething() { $this->log('I am about do do something'); //... I am doing something $this->log(”I've done it!”); } }
Traits jako Doctrine Behaviors
Traits ma za zadanie określać „zachowanie” klasy. Idąc tym krokiem rozumowania możemy skojarzyć to ze znanymi z Doctrine 1.x behaviorami. W rzeczywistości, sposób „wpinania” behaviorów do klasy poprzez słowo kluczowe actAs bardzo przypomina use znane z traits. W Doctrine 2 znikły znane z Doctrine 1 behaviory i, sądząc po notce na stronie projektu (tutaj), nic nie wskazuje na to że się pojawią. Firma Knp Labs, której chyba głównym obszarem zainteresowań jest Symfony 2 wypuściła jednak ostatnio bardzo ciekawą paczkę z behaviorami pod Doctrine 2, które są oparte właśnie na traits ! Przypominam tylko, że devhelp również sygnalizował podobieństwa pomiędzy tymi mechanizmami w poprzednim artykule . Link do artykułu na Knp Labs znajduje się tutaj, polecam również zajrzeć na ich konto na githubie z kodem źródłowym.
