Traits w PHP – czym są i do czego służą?

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ń:

  1. Traity nie mają nic wspólnego z programowaniem obiektowym. Naprawdę! Projektując obiektową architekturę nie powinniśmy myśleć o traitach.
  2. Traity nie są typem jak klasy i interfejsy. Nie można zażądać aby zmienna była obiektem mającym jakiś trait.
  3. 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.
  4. 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.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *