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.