Cechy a interfejsy
W poprzednim wpisie przedstawiono podstawowe informacje o cechach (ang. trait). Przyjrzyjmy się im bliżej.
Porównanie: cechy a interfejsy
Cecha nie może implementować interfejsu, tylko klasa. Cecha może zawierać potrzebny kod do zaimplementowania interfejsu:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
interface AllowsOpen{ public function open(); } trait Container{ public function open(){ echo 'Something is inside!'; } } class Can implements AllowsOpen{ use Container; } |
W specyfikacji możemy przeczytać, że cecha ma pierwszeństwo, jeśli chodzi o nadpisywanie danej metody dziedziczonej przez klasę. Jednak sama klasa może nadpisać metodę cechy, co jest i w tym przypadku:
1 2 3 4 5 6 7 |
class Book implements AllowsOpen{ use Container; public function open(){ echo 'You see only blank pages'; } } |
Metoda open() z cechy Container została tutaj nadpisana.
Sprawdźmy, jakie to ma działanie z polami klasy. Interfejsy są przeznaczone do obsługi metod. Jedyne pola, jakie mogą w sobie mieścić, to stałe:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
interface Valueable{ const myVariable = 12; } class ValueA implements Valueable{ public function show(){ echo Valueable::myVariable; } } $objValueA = new ValueA(); $objValueA->show(); echo $objValueA::myVariable; |
W przeciwieństwie do interfejsów cecha nie może posiadać stałych. Za to cecha może posiadać pozostałe rodzaje pól:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
trait ValuesContainer{ //const myVariable = 12; // A trait can't has constants private $value1 = 1; protected $value2 = 2; public $value3 = 3; } class ValueB{ use ValuesContainer; //private $value1 = 7; // FAIL !!! //protected $value2 = 6; // FAIL !!! //public $value3 = 5; // FAIL !!! } |
Klasa wykorzystujące cechę, nie może ponownie zadeklarować i nadać im nowych wartości. Klasa dziedzicząca po ValueB już tą możliwość ma. Jak już wspomniano, inaczej jest z metodami cechy. Rozważmy najpierw ten przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Basic{ public function sayHello($name){ echo 'Hello world! I am '.$name; } } trait TraitMethod{ public function sayHello($name){ echo 'Hello! My name is '.$name; } } class TestA extends Basic{ use TraitMethod; } |
Klasa TestA dziedziczy po klasie Basic i dołącza cechę TraitMethod. W tym przypadku cecha nadpisuje metodę z klasy bazowej. Przyjrzyjmy się drugiej klasie TestB:
1 2 3 4 5 6 7 |
class TestB extends Basic{ use TraitMethod; public function sayHello($name){ echo 'Witam! Nazywam się '.$name; } } |
Jak widzimy, klasa TestB nadpisze tutaj metodę cechy. Na przykładzie klasy TestC pokazujemy jak uzyskać dostęp do każdej wersji metody sayHello():
1 2 3 4 5 6 7 8 9 10 |
class TestC extends Basic{ use TraitMethod{ TraitMethod::sayHello as sayHelloTrait; } public function sayHello($name){ parent::sayHello($name); $this->sayHelloTrait($name); } } |
Aby uzyskać dostęp do nadpisanej metody cechy, został nadany jej alias.
Narzucanie implementacji danej metody
Interfejs narzuca implementację. W interfejsie metody do zaimplementowania mogą być tylko publiczne. Cechy zaś, podobnie jak klasy, mogą narzucić implementację danej metody poprzez zdefiniowanie jej jako abstrakcyjnej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
trait TraitName{ protected $name; abstract protected function setName(); public function __construct(){ $this->setName(); } public function getName(){ return $this->name; } } class MyClass{ use TraitName; protected function setName(){ $this->name = __CLASS__; } } |
W przykładzie tym mamy cechę TraitName, która posiada publiczny konstruktor, a w nim wywołanie metody setName(), która ma ustawić wartość zmiennej $name. Metoda setName() została określona jako abstrakcyjna, więc klasa dołączająca cechę TratName, musi ją zaimplementować.
Weźmy pod uwagę kolejny przypadek. Jeśli interesowałoby nas przeciążenie konstruktora danej klasy. Oczywiście, jeśli klasa wykorzystująca cechę, posiada zdefiniowany konstruktor, konstruktor cechy nie zostanie wywołany. Możemy w tym przypadku poradzić sobie tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class MyClass{ use TraitName{ TraitName::__construct as private __traitConstruct; } protected function setName(){ $this->name = __CLASS__; } public function __construct(){ $this->__traitConstruct(); } } |
Czy stosować konstruktor w cechach, czy lepiej nie? A jeśli tak, to na co uważać? O tym możemy przeczytać tutaj. Ogólnie stosowanie tego nie jest to zalecane.
__TRAIT__, __CLASS__ i parent
Wraz z dodaniem cech doszła też nowa, magiczna stała __TRAIT__. Zaprezentujmy przykład, jak wykorzystać __TRAIT__, __CLASS__ i operator parent:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
trait TraitName{ public function check(){ $parent = get_parent_class($this); if($parent && array_key_exists(__TRAIT__, class_uses($parent))){ echo 'Rodzic mówi: '.parent::sayHello().'.'; }else{ echo 'Rodzic nie posiada cechy '.__TRAIT__; } echo ' Obecna klasa to: '.__CLASS__; echo '. To jest działanie cechy '.__TRAIT__; } abstract public function sayHello(); } |
Musimy się upewnić, że możemy skorzystać z konstrukcji parent::getName(). W przykładzie używamy paru pomocnych funkcji jak get_parent_class(), za pomocą której otrzymamy nazwę rodzica obecnej klasy. Druga funkcja class_uses() zwraca nam tablicę, która zawiera listę cech używanych przez daną klasę, ale już nie nie uwzględnia cech wykorzystywanych przez rodzica. W naszym przykładzie, za pomocą array_key_exists() sprawdzamy, czy lista zawiera naszą cechę, której nazwa jest przechowywana w stałej __TRAIT__.
Istnieje również inna funkcja, get_declared_traits(), która zwraca listę wszystkich zadeklarowanych cech.
Zastosowanie
Gdzie stosujemy? Wszędzie tam, gdzie potrzebujemy mieć pewną funkcjonalność, która nie tylko, że ma być wyciągnięta osobno, ale także ma być wykorzystywana w wielu miejscach.
- Cecha może zawierać implementację potrzebną klasie do spełnienia założeń interfejsu, na przykład dla interfejsu Iterator,
- Implementacja wzorca projektowego, np. Singleton,
- Może pełnić rolę helpera dla klas.