Do czego służą traity? Kiedy warto je stosować? Jak dobrze ich używać? Czy są dobre czy złe? Spróbujmy znaleźć odpowiedzi.
W tym wpisie nie będę opisywał szczegółów korzystania z traitów. Skupimy się za to na tym kiedy i do czego warto je stosować oraz jak unikać pomyłek.
Czym nie są traity?
Na początek warto rozwiać parę popularnych nieporozumień:
- Traity nie mają nic wspólnego z programowaniem obiektowym. Naprawdę! Projektując obiektową architekturę nie powinniśmy myśleć o traitach.
- Traity nie są typem jak klasy i interfejsy. Nie można zażądać aby zmienna była obiektem mającym jakiś trait.
- Traity nie są kontraktem jak interfejsy. O ile interfejs mówi Ci jedynie jakie metody się opjawią, o tyle trait posiada implementację. Ale uwaga – nie ma gwarancji, że klasa czy obiekt nie przesłoni interpretacji.
- Traity nie są klasami abstrakcyjnymi. Klasa abstrakcyjna definiuje pewien twór podczas gdy traity mówią jedynie o cechach, które mogą być rozproszone po wielu tworach.
Wyrzuciliśmy większość ciekawych elementów programowania obiektowego. Po co nam więc traity i jak mamy o nich myśleć?
Czym są traity?
Dokumentacja PHP mówi o traitach jako o sposobie na re-używanie kodu. I właśnie o to chodzi. Traity są sposobem na copy-paste. Niczym więcej.
Traity to ciekawe rozwiązanie na powtarzające się kawałki kodu. Tak samo jak funkcje rozwiązały code-reuse w programowaniu proceduralnym, tak traity przenoszą tą koncepcję na poziom obiektów.
Dobre użycie traitów
Nie próbujmy więc myśleć o traitach w kategoriach obiektowych. Pomyślmy o nich, jako o przydatnej funkcjonalności używanej w wielu miejscach.
Widziałem bardzo dobre wykorzystanie traitów w jednym projekcie:
Był sobie bundle (w Symfony2) zarządzający użytkownikami. W tym bundlu była też encja określająca konta użytkowników:
namespace UserBundle\Entity; class User { ... }
Mieliśmy więc utworzonych użytkowników. Pracując dalej zechcemy tworzyć inne encje posiadane przez użytkownika – artykuły, produkty, wpisy, obrazki itp. Za każdym razem muselibyśmy tworzyć w encji kod wiążący nas z użytkownikiem:
namespace PictureBundle\Entity; class Picture { /** @ORM\ManyToOne(targetEntity=\UserBundle\Entity\User */ private $user; public function setUser(\UserBundle\Entity\User $user) { ... } public function getUser() { ... } }
Będziemy mieć więc wiele różnych encji nie związanych ze sobą ale wszystkie będą związane z użytkownikiem. Tworząc każdą z nich albo pomylimy się przepisując te same funkcje albo będziemy intensywnie klikać ctrl-C, ctrl-V
A może klasa abstrakcyjna? Co ma wspólnego zdjęcie z np. numerem konta użytkownika? Oba należą do jakiegoś konta ale oba pochodzą z zupełnie różnych dziedzin.
Z pomocą przychodzi nam trait. Oryginalny UserBundle daje nam taki kod:
trait Owned { /** @ORM\ManyToOne(targetEntity=\UserBundle\Entity\User */ private $user; public function setUser(\UserBundle\Entity\User $user) { $this->user = $user; } /** @return \UserBundle\Entity\User */ public function getUser() { return $this->user; } }
Dzięki temu każda encja mająca właściciela (użytkownika) używa tylko traita:
class Picture { use \UserBundle\Trait\Owned; ... }
Ominęliśmy męczące definiowanie zmiennej wciąż tych samych getterów i setterów. Co więcej, jeśli zmienimy cokolwiek w implementacji np. setUser – zmienimy to w traicie. Kod zadziała wszędzie.
Plus jest jeszcze jeden – kod związany z użytkownikami trzymamy w UserBundle a nie kopiujemy do wszystkich innych bundli w kodzie.
Problemy z traitami
Problem 1. Nieprzejrzysty kod.
Powyższy przykład był ładny i elegancki. Ale łatwo jest nadużywać traitów. Możemy stworzyć wiele zmiennych, wiele kodu i sporo logiki wewnątrz traita. Potem używamy go w klasie, która będzie miała jedną prostą własną metodę.
Programista zajrzy do klasy i widzi, że jest prosta. Ma tylko jeden „use”. Ale pod tym „use” kryje się ogrom kodu, funkcji i logiki.
Taki problem opisuje wiele artykułów. Ale czy nie tak samo mamy z funkcjami, magicznymi metodami, dziedziczeniem itp? Przecież nie wiemy czy pisząc: $obiekt->atrybut = "wartość";
nie wywołamy magicznej metody __set()
, która po drodze pobierze pół internetu, wyśle maila do szefa i zarezerwuje nam lot do Tanzani w jedną stronę. Moim zdaniem ten argument przeciwko traitom jest nietrafiony.
Problem 2. Gdy zmienimy traita…
Gdy zmienimy traita – zmieniamy wszystkie klasy korzystające z niego. W przykładzie powyżej możemy w traicie dodać funkcję np. changeUser(). Ale co się stanie gdy taka funckja już istnieje w jednej klasie? Skąd wiemy, czy nie zepsujemy czegoś w innym dalekim miejscu kodu?
A jednak wiemy! Zasada Open-Closed podpowiada nam co zrobić w tej sytuacji.
Mamy więc nasz trait \UserBundle\Trait\Owned
, który jest już szeroko używany przez społeczność PHP. Aby dorobić nową funkcjonalność tworzymy nowy trait wykorzystujący nasz poprzedni:
trait OwnedForever { use Owned; public function setUser($user) { if (empty($user)) { throw new \Exception(); } Owned::setUser($user); } }
W ten sposób nic nie zepsuliśmy. Mamy nowego traita z rozszerzoną funkcjonalnością.
Podsumowując
Same traity nie są ani złe ani dobre. Pomimo wielu kontrowersji mogą się bardzo przydać w programowaniu. Należy jednak ostrożnie ich używać.
Artykuł pisałem na podstawie wielu artykułów w sieci rozważających plusy i minusy traitów. Warto zapoznać się z nimi tutaj, tutaj, tutaj i tutaj.