9. Cechy
Trait to struktury analogiczne do klas. Nie można ich jednak instancjonować. Są one przeznaczone do włączania do klas. Włączenie Trait do klasy daje taki sam efekt, jakby skopiowano kod Trait do klasy. W Trait umieszcza się kod, który może być ponownie wykorzystany w wielu klasach.
9.1. Struktura drzewa skryptów

9.2. Włączenie cechy do klasy
Skrypt [traits-01.php] pokazuje podstawowe wykorzystanie cechy w klasie:
<?php
class Class04 {
// atrybut
private $name;
// konstruktor
public function __construct(string $name) {
$this->name = $name;
}
// metody pobierające i ustawiające
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->name = $name;
}
}
trait Trait04 {
// atrybut
private $name;
// metody pobierające i ustawiające
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->name = $name;
}
}
class Class05 {
// dołączenie cechy
use Trait04;
// konstruktor
public function __construct(string $name) {
$this->name = $name;
}
}
// test --------------
$class04 = new Class04("Tim");
$class05 = new Class05("Burton");
print $class04->getName() . "\n";
print $class05->getName() . "\n";
// wyświetlanie obu klas
print_r($class04);
print_r($class05);
Komentarze do kodu
- wiersze 3–21: definicja klasy [Class04] z atrybutem, metodami get/set oraz konstruktorem;
- wiersze 23–36: przejmujemy kod z pliku [Class04] bez jego konstruktora i przenosimy go w całości do cechy [Trait04]. Nie przejmujemy konstruktora, ponieważ cechy nie można instancjonować;
- wiersz 23: to słowo kluczowe [trait] sprawia, że [Trait04] jest cechą, a nie klasą;
- wiersze 38–45: definiujemy klasę [Class05], która przejmuje kod cechy [Trait04] (wiersz 40) i dodaje do niej konstruktor (wiersze 43–45), identyczny z konstruktorem klasy [Class04], aby umożliwić instancjonowanie tej klasy;
- wiersz 40: to słowo kluczowe [use] umożliwia włączenie cechy do klasy;
- wiersze 50–56: testy wykazują, że klasy [Class04] i [Class05] działają w ten sam sposób;
Wyniki
Wyniki z wierszy 3–10 pokazują, że klasy [Class04] i [Class05] mają tę samą zawartość;
Wniosek
Zastosowanie instrukcji [use Trait] w klasie jest równoważne z umieszczeniem kodu z [Trait] w tej klasie.
9.3. Wykorzystanie tego samego wzorca w różnych klasach
Jedną z głównych zalet tej struktury wydaje się możliwość ponownego wykorzystania tego samego kodu (atrybutów i metod) w różnych klasach. Zobaczymy jednak, że ten sam cel można osiągnąć, korzystając z prostych klas.
Współdzielenie klasy trait między klasami ilustruje poniższy skrypt [trait-02.php]:
<?php
trait Trait01 {
// atrybut
private $id = 0;
// metoda
public function doSomething() {
print "Trait01::doSomething… ($this->id)\n";
}
}
class Class02 {
// włączenie Trait01
use Trait01 {
// metoda [Trait01::doSomething] jest dostępna
// w klasie pod nazwą [doSomethingInTrait]
Trait01::doSomething as doSomethingInTrait;
}
// metoda właściwa dla tej klasy
public function doSomething(): void {
// atrybut id
$this->id += 10;
// wywołanie metody Trait01
$this->doSomethingInTrait();
// wyświetlanie lokalne
print "Class02->doSomething\n";
}
}
class Class03 {
// włączenie Trait01
use Trait01;
// metoda lokalna klasy
public function doSomethingElse(): void {
// atrybut id
$this->id += 10;
// użycie metody Trait01
$this->doSomething();
// wyświetlanie lokalne
print "Class03->doSomethingElse\n";
}
}
// test01 ----------------
function test01(): void {
$class02 = new Class02();
$class03 = new Class03();
$class02->doSomething();
$class03->doSomethingElse();
}
// test01
print "test01-----------------\n";
test01();
Komentarze
- wiersze 3–10: element definiujący atrybut (wiersz 5) oraz metodę (wiersze 8–10).
- Cecha [Trait01] jest wstrzykiwana do dwóch klas: [Class02] (wiersze 14–32) oraz [Class03] (wiersze 34–46).
- wiersze 16–20: wstawienie klasy [Trait01] do klasy [Class02];
- wiersz 19 ma na celu rozwiązanie konfliktu: klasy [Trait01] i [Class02] posiadają obie metodę o nazwie [doSomething]. Należy przewidzieć dwa przypadki:
- metoda [Class02::doSomething] jest wywoływana spoza klasy. W tym przypadku metoda [Class02::doSomething] ma pierwszeństwo przed metodą [Trait01::doSomething] i to właśnie ona jest wywoływana;
- metoda [Class02::doSomething] jest wywoływana z wnętrza klasy. W tym przypadku dochodzi do konfliktu: interpreter PHP nie wie, którą metodę wywołać;
Wiersz 19 pozwala na zmianę nazwy metody [Trait01::doSomething] na [doSomethingInTrait]. W ten sposób wewnątrz klasy [Class02] będą używane następujące zapisy:
- [doSomethingInTrait] w celu wywołania metody [Trait01::doSomething];
- [doSomething] w celu wywołania metody [Class02::doSomething];
- wiersze 25, 27: klasa [Class02] wykorzystuje atrybut i metodę klasy [Trait01] tak, jakby były one jej własnymi;
- wiersze 34–48: klasa [Class03] jest identyczna z klasą [Class02]. Włączenie klasy [Trait01] jest tutaj prostsze, ponieważ nie występuje konflikt między metodami klas [Trait01] i [Class03];
Wyniki
Należy zwrócić uwagę, że cecha [Trait01] nie jest wspólna dla klas [Class02] i [Class03]. W ten sposóbatrybut [Trait01::i], poprzez włączenie [Trait01] do klas [Class02] i [Class03], staje się dwoma różnymi atrybutami: [Class02::i] i [Class03::i]. Pokazują to wiersze 2 i 4 wyników. Gdyby atrybut [Trait01::i] był wspólny dla klas [Class02] i [Class03], w wierszu 4 pojawiłaby się liczba 20 zamiast 10.
Skrypt [trait-03.php] pokazuje, że ten sam wynik można uzyskać, używając klasy zamiast cechy:
<?php
// klasa zastępująca cechę
class Class01 {
// atrybut
private $id = 0;
// ustawianie
public function setId(int $id) {
$this->id = $id;
}
// metoda pobierająca
public function getId(): int {
return $this->id;
}
// metoda
public function doSomething(): void {
print "Class01::doSomething… ($this->id)\n";
}
}
class Class02 {
// włączenie Class01
private $class01;
// metoda ustawiająca
public function setClass01(Class01 $class01) {
$this->class01 = $class01;
}
// metoda specyficzna dla klasy
public function doSomething(): void {
// zmiana atrybutu klasy Class01
$id = $this->class01->getId();
$id += 10;
$this->class01->setId($id);
// wywołanie metody klasy Class01
$this->class01->doSomething();
// wyświetlanie lokalne
print "Class02->doSomething\n";
}
}
class Class03 {
// dołączenie klasy Class01
private $class01;
// metoda setter
public function setClass01(Class01 $class01) {
$this->class01 = $class01;
}
// metoda lokalna klasy
public function doSomethingElse(): void {
// zmiana atrybutu klasy Class01
$id = $this->class01->getId();
$id += 10;
$this->class01->setId($id);
// użycie metody klasy Class01
$this->class01->doSomething();
// wyświetlanie lokalne
print "Class03->doSomethingElse\n";
}
}
// test01 ----------------
function test01(): void {
// dwa obiekty
$class02 = new Class02();
$class03 = new Class03();
// uzyskają dostęp do dwóch różnych instancji [Class01]
$class02->setClass01(new Class01());
$class03->setClass01(new Class01());
// weryfikacja
$class02->doSomething();
$class03->doSomethingElse();
}
// test02 ----------------
function test02(): void {
// wspólna instancja [Class01]
$class01 = new Class01();
// dwa obiekty
$class02 = new Class02();
$class03 = new Class03();
// uzyskają dostęp do tej samej instancji [Class01]
$class02->setClass01($class01);
$class03->setClass01($class01);
// weryfikacja
$class02->doSomething();
$class03->doSomethingElse();
}
// test01
print "test01-----------------\n";
test01();
// test02
print "test02-----------------\n";
test02();
Komentarze
- wiersze 4–23: klasa [Class01] zastępuje pozycję [Trait01]. Kod klasy [Trait01] został włączony do kodu klas [Class02] i [Class03]. W tym przypadku tak nie będzie. Odwołanie do kodu klasy [Class01] zostanie wstawione do klas [Class02] i [Class03]. W związku z tym atrybuty klasy [Class01] będą znajdować się poza kodem klas [Class02] i [Class03]. Ponieważ w tym przypadku atrybut [id] jest prywatny (wiersz 6), należy przewidzieć metodę pobierającą (wiersze 14–16) oraz metodę ustawiającą (wiersze 9–11). Jest to pierwsza różnica w stosunku do cechy: konieczne jest utworzenie kodu umożliwiającego dostęp do prywatnych atrybutów klasy. Można by zmienić widoczność atrybutu [id] na [public], ale nigdy nie zaleca się tego robić. Korzystanie z metody ustawiającej (setter) w celu ustalenia wartości atrybutu pozwala na sprawdzenie jej poprawności;
- wiersz 27: dodanie w kodzie [Class02] odwołania do klasy [Class01]. Ponieważ ten atrybut jest prywatny, musimy utworzyć metodę ustawiającą (wiersze 30–32), aby go zainicjować;
- wiersze 37–41: przy każdym użyciu kodu klasy [Class01] musimy korzystać z atrybutu [$this→class01];
- wiersze 48–69: klasa [Class03] jest klonem klasy [Classe02], z tą różnicą, że jej metoda ma inną nazwę;
- wiersze 72–82: pierwszy test. Polega on na wstrzyknięciu do klas [Class02] i [Class03] dwóch różnych instancji klasy [Class01] (wiersze 77 i 78);
- wiersze 85–97: drugi test wprowadza do klas [Class02] i [Class03] tę samą instancję klasy [Class01] (wiersze 97, 92, 93);
- wiersze 100–101: wykonanie testu [test01];
- wiersze 103–104: wykonanie testu [test02];
Wyniki
Komentarz do wyników
- wiersze 2–5: uzyskujemy te same wyniki, co w przypadku elementu [Trait01]. Można zatem stwierdzić, że użycie cechy nie jest tutaj niezbędne, ale pozwala na skrócenie kodu, ponieważ nie ma potrzeby stosowania metod w celu uzyskania dostępu do atrybutów cechy: są one integralną częścią kodu, w którym cecha została wbudowana;
- wiersze 7–10: ponieważ w klasach [Class02] i [Class03] wstrzyknięto to samo odwołanie do [Class01], atrybut [Class01::id] został współdzielony między obiema klasami. Dlatego w wierszu 9 wyników wyświetla się liczba 20 zamiast 10 z wykorzystaniem cechy. Można z tego wywnioskować, że jeśli atrybuty cechy mają być współdzielone między klasami, to cecha nie nadaje się do użycia i należy wówczas zastosować klasę;
9.4. Grupowanie metod w trait
W poprzednim przykładzie trait zawierał atrybuty i metody. Tutaj rozważamy przypadek, w którym zawiera on wyłącznie metody. W takim przypadku trait przypomina skupisko metod, które można następnie wykorzystać w różnych klasach. Ponieważ trait nie posiada tutaj żadnych atrybutów, rozważymy przypadek, w którym zgromadzone w nim metody działają wyłącznie na parametrach, które są im przekazywane. W rzeczywistości nie jest to obowiązkowe: trait może operować na atrybucie [$this→attribut1], nie posiadając tego atrybutu. Wówczas to klasy korzystające z tego traitu muszą zapewnić atrybut [$this→attribut1].
W przypadku, gdy cecha zawiera wyłącznie metody działające wyłącznie na przekazanych im parametrach, pokażemy, że cechę tę można zastąpić klasą posiadającą te same metody co cecha, zadeklarowane jako statyczne.
Wykorzystanie cechy ilustruje skrypt [trait-04.php], który przejmuje cechę z poprzedniego przykładu, usuwając z niej wszystkie atrybuty:
<?php
trait Trait01 {
// metoda do udostępnienia
public function doSomething() {
print "Trait01::doSomething ….\n";
}
}
class Class02 {
// włączenie Trait01
use Trait01 {
// metoda [Trait01::doSomething] jest dostępna
// w klasie pod nazwą [doSomethingInTrait]
Trait01::doSomething as doSomethingInTrait;
}
public function doSomething(): void {
// wywołanie metody Trait01
$this->doSomethingInTrait();
// wyświetlanie lokalne
print "Class02->doSomething\n";
}
}
class Class03 {
// włączenie Trait01
use Trait01;
// metoda lokalna klasy
public function doSomethingElse(): void {
// wywołanie metody Trait01
$this->doSomething();
// wyświetlanie lokalne
print "Class03->doSomethingElse\n";
}
}
// test ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();
Komentarze
- wiersze 3–10: cecha [Trait01] nie ma już atrybutów;
- wiersze 14–18: włączenie [Trait01] do [Class02]. W wierszu 22 wykorzystywana jest metoda [Trait01];
- wiersz 31: włączenie [Trait01] do [Class03]. Metoda z [Trait01] jest wykorzystywana w wierszu 36;
Wyniki
W tym przypadku funkcja [Trait01] może zostać z łatwością zastąpiona przez klasę. Pokazuje to poniższy skrypt [trait-05.php]:
<?php
abstract class Class01 {
// metoda statyczna do współdzielenia
public static function doSomething() {
print "Class01::doSomething ….\n";
}
}
class Class02 {
public function doSomething(): void {
// wywołanie metody klasy Class01
Class01::doSomething();
// wyświetlanie lokalne
print "Class02->doSomething\n";
}
}
class Class03 {
// metoda lokalna klasy
public function doSomethingElse(): void {
// wywołanie metody klasy Class01
Class01::doSomething();
// wyświetlanie lokalne
print "Class03->doSomethingElse\n";
}
}
// test ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();
Komentarze
- wiersze 3–10: cecha [Trait01] zostaje zastąpiona przez klasę abstrakcyjną [Class01], w której wszystkie metody są zadeklarowane jako statyczne. Klasa ta została zadeklarowana jako abstrakcyjna wyłącznie w celu uniemożliwienia jej instancjonowania. Chcielibyśmy również zapisać [final], aby uniemożliwić jej dziedziczenie, ale PHP 7 nie akceptuje przedrostka [final abstract] dla klasy. Można użyć jednego lub drugiego, ale nie obu;
- wiersz 16: zamiast zapisać [$this→doSomethingInTrait], zapisujemy teraz [Class01::doSomething], tzn. wywołujemy metodę statyczną [doSomething] klasy [Class01];
- wiersz 28: powtarzamy tę samą procedurę w klasie [Class03];
Wyniki
Otrzymujemy dokładnie ten sam wynik, co w przypadku cechy [Trait01], co pokazuje, że można uniknąć korzystania z tej cechy. Stwierdzono, że metody cechy mogą operować na atrybucie [$this→attribut1], nawet jeśli sama cecha nie posiada tego atrybutu. W takim przypadku to klasy korzystające z tego cechy muszą zapewnić atrybut [$this→attribut1]. Jest to przypadek nietypowy: równie dobrze można „przenieść” atrybut [$this→attribut1], który muszą posiadać klasy korzystające z cechy, do samej cechy. W ten sposób atrybut ten z konieczności stanie się częścią atrybutów klasy korzystającej z cechy.
9.5. Wielokrotne dziedziczenie za pomocą cechy
W literaturze dotyczącej PHP często można przeczytać, że cecha umożliwia wielokrotne dziedziczenie: możliwość dziedziczenia przez jedną klasę po wielu klasach. Język C++ posiada tę możliwość, ale nie języki Java ani C#, które znają jedynie dziedziczenie pojedyncze. Pokażemy, że choć rzeczywiście użycie cechy w klasie pochodnej pozwala zaimplementować coś przypominającego wielokrotne dziedziczenie, to ten przypadek użycia można zrealizować również za pomocą zwykłych klas.
Skrypt [trait-06.php] wykorzystuje klasę pochodną i cechę:
<?php
trait Trait01 {
// atrybut
private $i;
// metoda do współdzielenia
public function doSomethingInTrait01() {
// modyfikacja Trait01::$i
$this->i++;
// wyświetlanie
print "Trait01::doSomethingInTrait01… i=$this->i\n";
}
}
class Class02 {
// atrybut
protected $j = 0;
// metoda
public function doSomethingInClass02(): void {
// modyfikacja Class02::j
$this->j += 10;
// wyświetlanie
print "Class02->doSomethingInClass02… j=$this->j\n";
}
}
// klasa pochodna
class Class03 extends Class02 {
// dziedziczy po Class02:j i Trait01::i
// włączenie Trait01
use Trait01;
// metoda
public function doSomethingInClass03(): void {
// wykorzystanie metody z Trait01
$this->doSomethingInTrait01();
// modyfikacja Trait01::i
$this->i += 100;
// modyfikacja Class03::j (==Class02::j)
$this->j += 1000;
// wyświetlanie
print "Class03->doSomethingInClass03… i=$this->i, j=$this->j\n";
}
}
// test ----------------
(new Class02())->doSomethingInClass02();
(new Class03())->doSomethingInClass03();
Komentarze
- wiersze 3–15: powracamy do cechy [Trait01] z atrybutem i metodą obsługującą ten atrybut;
- wiersze 17–29: klasa [Class02], która nie ma nic wspólnego z cechą [Trait01]. Nie korzysta z niej;
- wiersz 19: zadeklarowano jedyny atrybut klasy [Class02] z widocznością [protected], aby był on dostępny w klasach pochodnych;
- wiersz 32: klasa [Class03] rozszerza klasę [Class02]. Ponadto zawiera cechę [Trait01] (wiersz 35). Wreszcie dziedziczy ona atrybuty i metody klasy [Class02] oraz zawiera atrybuty i metody klasy [Trait01]. Mamy więc do czynienia z czymś analogicznym do dziedziczenia wielokrotnego;
Wyniki
Podobnie jak w poprzednim przykładzie, pokażemy, że:
- cechę można zastąpić klasą;
- zamiast włączać cechę do klasy pochodnej, włączamy odwołanie do instancji tej klasy;
Skrypt [trait-07.php] wygląda następująco:
<?php
class Class01 {
// atrybut
protected $i;
// metody getter i setter
public function getI(): int {
return $this->i;
}
public function setI(int $i): void {
$this->i = $i;
}
// metoda
public function doSomethingInClass01(): void {
// modyfikacja Class01::$i
$this->i++;
// wyświetlanie
print "Class01::doSomething in Class01… i=$this->i\n";
}
}
class Class02 {
// atrybut
protected $j = 0;
// metoda specyficzna dla klasy
public function doSomethingInClass02(): void {
// modyfikacja Class02::j
$this->j += 10;
// wyświetlanie
print "Class02->doSomethingInClass02… j=$this->j\n";
}
}
class Class03 extends Class02 {
// włączenie Class01
private $class01;
// ustawianie
public function setClass01(Class01 $class01) {
$this->class01 = $class01;
}
// metoda lokalna klasy
public function doSomethingInClass03(): void {
// wykorzystanie metody klasy Class01
$this->class01->doSomethingInClass01();
// zmiana Class01::i
$i = $this->class01->getI();
$i += 100;
$this->class01->setI($i);
// modyfikacja Class03::j
$this->j += 1000;
// wyświetlanie
print "Class03->doSomethingInClass03… i=$i, j=$this->j\n";
}
}
// test ----------------
$class01 = new Class01();
$class02 = new Class02();
$class03 = new Class03();
$class03->setClass01($class01);
$class02->doSomethingInClass02();
$class03->doSomethingInClass03();
Komentarze
- wiersze 3–24: klasa [Class01] zastępuje cechę [Trait01]. Ponieważ klasa [Class01] zostanie włączona do klas poprzez odwołanie, przewidziano metody get / set dla atrybutu [$i];
- wiersze 26–38: klasa [Classe02] pozostaje bez zmian;
- wiersz 40: klasa [Class03] rozszerza klasę [Classe02];
- wiersz 42: klasa [Classe01] jest włączona do klasy [Classe03] poprzez odwołanie;
- wiersze 45–47: przewidziano klasę setter w celu zainicjowania odwołania do klasy [Classe01];
- wiersze 50–61: metoda [doSomethingInClass03] wykonuje tę samą czynność co poprzednio, jednak przy użyciu bardziej złożonego kodu;
Wyniki
Z tego przykładu można wywnioskować, że również w tym przypadku cecha nie jest niezbędna, ale należy przyznać, że pozwala ona na napisanie krótszego kodu w klasie pochodnej.
9.6. Użycie trait zamiast klasy abstrakcyjnej
Często spotykamy się z następującym przypadkiem użycia: tworzymy dość ogólny interfejs I, który może dać początek wielu implementacjom. Implementacje te mają wspólny kod, ale różnią się innymi metodami. Ten przypadek użycia można zaimplementować na dwa sposoby:
- tworzymy klasę abstrakcyjną C, która zawiera kod wspólny dla klas pochodnych. Klasa C implementuje interfejs I, ale niektóre metody, które muszą zostać zadeklarowane w klasach pochodnych, są w klasie C zadeklarowane jako abstrakcyjne, a zatem sama klasa C jest abstrakcyjna. Następnie tworzymy klasy C1 i C2, pochodne od klasy C, z których każda na swój sposób implementuje metody niezdefiniowane (abstrakcyjne) swojej klasy nadrzędnej C;
- tworzymy cechę T niemal identyczną z klasą abstrakcyjną C z poprzedniego rozwiązania. Cecha ta nie implementuje interfejsu I, ponieważ z punktu widzenia składni nie jest to możliwe. Następnie tworzymy klasy C1 i C2, które implementują interfejs I i wykorzystują cechę T. Klasom tym pozostaje jedynie zaimplementować metody interfejsu I, które nie zostały zaimplementowane przez importowaną przez nie cechę T;
Oto przykład ilustrujący duże podobieństwo tych dwóch rozwiązań.
Aplikacja 1 implementuje opisane wcześniej rozwiązanie 1 [trait-08.php]:
<?php
interface Interface1 {
public function doSomething(): void;
public function doSomethingElse(): void;
}
abstract class AbstractClass implements Interface1 {
// atrybuty
protected $attr1 = 11;
protected $attr2 = 12;
// metody pobierające i ustawiające
public function getAttr1() {
return $this->attr1;
}
public function getAttr2() {
return $this->attr2;
}
public function setAttr1($attr1) {
$this->attr1 = $attr1;
return $this;
}
public function setAttr2($attr2) {
$this->attr2 = $attr2;
return $this;
}
// zaimplementowana metoda
public function doSomething(): void {
print "AbstractClass::doSomething [$this->attr1,$this->attr2]\n";
}
// metoda niezrealizowana
abstract public function doSomethingElse(): void;
}
// klasa pochodna 1
class Class1 extends AbstractClass {
// atrybut
private $attr3 = 13;
// metody pobierające i ustawiające
public function getAttr3() {
return $this->attr3;
}
public function setAttr3($attr3) {
$this->attr3 = $attr3;
return $this;
}
// implementacja doSomethingElse
public function doSomethingElse(): void {
print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
}
}
// klasa pochodna 2
class Class2 extends AbstractClass {
// atrybut
private $attr4 = 14;
public function getAttr4() {
return $this->attr4;
}
public function setAttr4($attr4) {
$this->attr4 = $attr4;
return $this;
}
// implementacja doSomethingElse
public function doSomethingElse(): void {
print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
}
}
// funkcja zewnętrzna
function useInterfaceWith(Interface1 $interface):void{
$interface->doSomething();
$interface->doSomethingElse();
}
// testy
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());
Komentarze
- wiersze 3–8: interfejs [Interface1] posiada dwie metody;
- wiersze 10–41: klasa abstrakcyjna [AbstractClass] implementuje interfejs [Interface1] (wiersz 10). Posiada dwa atrybuty wraz z ich metodami pobierającymi i ustawiającymi (wiersze 12–32), implementuje metodę [doSomething] z interfejsu [Interface1] (wiersze 35–37) ale nie potrafi zaimplementować metody [doSomethingElse]. Metoda ta jest zatem zadeklarowana jako abstrakcyjna (wiersz 40). Klasa abstrakcyjna [AbstractClass] nie może być instancjonowana i aby mogła pełnić jakąkolwiek funkcję, musi być koniecznie pochodną;
- wiersze 44–63: klasa Class1 dziedziczy po klasie abstrakcyjnej [AbstractClass], a zatem implementuje interfejs [Interface1] (wiersz 14). Nadaje ona treść metodzie [doSomethingElse], której jej klasa nadrzędna nie zdefiniowała (wiersze 59–61). Dodaje również atrybut do atrybutów swojej klasy nadrzędnej (wiersze 46–56);
- wiersze 66–82: klasa Class2 rozszerza klasę abstrakcyjną [AbstractClass], a tym samym implementuje interfejs [Interface1] (wiersz 66). Zawiera treść metody [doSomethingElse], której jej klasa nadrzędna nie zdefiniowała (wiersze 80–82). Dodaje również atrybut do atrybutów swojej klasy nadrzędnej (wiersze 68–77);
- wiersze 87–90: funkcja [useInterfaceWith] przyjmuje jako parametr typ [Interface1] i wywołuje obie metody tego interfejsu;
- wiersze 93–94: funkcja [useInterfaceWith] jest wywoływana po raz pierwszy z typem [Class1], a po raz drugi z typem [Class2]. Jest to poprawne, ponieważ oba te typy implementują interfejs [Interface1];
Wyniki
AbstractClass::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
AbstractClass::doSomething [11,12]
Class2::doSomethingElse [11,12,14]
Teraz wdrażamy rozwiązanie 2 za pomocą skryptu [trait-09.php]. Polega to na zastąpieniu klasy abstrakcyjnej cechą:
<?php
interface Interface1 {
public function doSomething(): void;
public function doSomethingElse(): void;
}
trait Trait1 {
// atrybuty
private $attr1 = 11;
private $attr2 = 12;
// metody pobierające i ustawiające
public function getAttr1() {
return $this->attr1;
}
public function getAttr2() {
return $this->attr2;
}
public function setAttr1($attr1) {
$this->attr1 = $attr1;
return $this;
}
public function setAttr2($attr2) {
$this->attr2 = $attr2;
return $this;
}
// zaimplementowana metoda
public function doSomething(): void {
print "Trait::doSomething [$this->attr1,$this->attr2]\n";
}
}
// klasa pochodna 1
class Class1 implements Interface1 {
// użycie cechy
use Trait1;
// atrybut
private $attr3 = 13;
// metody pobierające i ustawiające
public function getAttr3() {
return $this->attr3;
}
public function setAttr3($attr3) {
$this->attr3 = $attr3;
return $this;
}
// implementacja doSomethingElse
public function doSomethingElse(): void {
print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
}
}
// klasa pochodna 2
class Class2 implements Interface1 {
// wykorzystanie cechy
use Trait1;
// atrybut
private $attr4 = 14;
public function getAttr4() {
return $this->attr4;
}
public function setAttr4($attr4) {
$this->attr4 = $attr4;
return $this;
}
// implementacja doSomethingElse
public function doSomethingElse(): void {
print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
}
}
// funkcja zewnętrzna wykorzystująca interfejs
function useInterfaceWith(Interface1 $interface): void {
$interface->doSomething();
$interface->doSomethingElse();
}
// testy
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());
Komentarze
- wiersze 3–8: interfejs [Interface1] nie uległ zmianie;
- wiersze 10–41: cecha [Trait1] zastępuje klasę abstrakcyjną [AbstractClass] z rozwiązania 1. Kod jest taki sam, z wyjątkiem następujących szczegółów:
- wiersz 10: cecha [Trait1] nie implementuje interfejsu [Interface1]. Jest to niemożliwe pod względem składniowym;
- wiersze 12–13: atrybut widoczności [protected] wśród atrybutów klasy abstrakcyjnej [AbstractClass] zmienia się tutaj na [private]. Oba te atrybuty mają na celu zapewnienie klasom pochodnym bezpośredniego dostępu do atrybutów klasy nadrzędnej (protected) lub cechy (private) bez konieczności korzystania z metod getter i setter;
- cecha [Trait1] nie deklaruje metody abstrakcyjnej [doSomethingElse];
- wiersze 41–62: klasa [Class1] z rozwiązania 2 jest identyczna z klasą [Class1] z rozwiązania 1, z wyjątkiem następujących szczegółów:
- wiersz 41: klasa [Class1] implementuje interfejs [Interface1], podczas gdy w rozwiązaniu 1 rozszerzała klasę abstrakcyjną [AbstractClass];
- wiersz 43: wykorzystuje cechę [Trait1] do implementacji części interfejsu;
- wiersze 65–85: można tu sformułować te same uwagi, co w przypadku klasy [Class1];
- wiersze 87–95: pozostała część kodu pozostaje bez zmian;
Wyniki
Trait::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
Trait::doSomething [11,12]
Class2::doSomethingElse [11,12,14]
Otrzymujemy rzeczywiście te same wyniki.
9.7. Conclusion
Z powyższych przykładów wynika, że nie jest jasne, w jakich sytuacjach zastosowanie linii przyniosłoby wyraźną korzyść. W naszych przykładach zawsze można się bez niej obejść, zastępując ją klasą. Wydaje się jednak, że jej użycie jest praktyczne w celu wyodrębnienia kodu między różnymi klasami pochodnymi, tak jakby ten kod należał do klasy nadrzędnej. Właśnie to zrobimy w kolejnym przykładzie.