Skip to content

9. Traits

Traits zijn structuren die vergelijkbaar zijn met klassen. Ze kunnen echter niet worden geïnstantieerd. Ze zijn bedoeld om in klassen te worden opgenomen. Het opnemen van een Trait in een klasse heeft hetzelfde effect als wanneer de code van de Trait in de klasse zou zijn gekopieerd. In een Trait plaatst men code die in meerdere klassen kan worden hergebruikt.

9.1. De scriptstructuur

Image

9.2. Opname van een trait in een klasse

Het script [traits-01.php] toont een eenvoudig gebruik van een trait in een klasse:


<?php

class Class04 {
  // attribuut
  private $name;

  // constructor
  public function __construct(string $name) {
    $this->name = $name;
  }

  // getters en setters
  public function getName(): string {
    return $this->name;
  }

  public function setName(string $name): void {
    $this->name = $name;
  }

}

trait Trait04 {
  // attribuut
  private $name;

  // getters en setters
  public function getName(): string {
    return $this->name;
  }

  public function setName(string $name): void {
    $this->name = $name;
  }

}

class Class05 {
  // opname van de Trait
  use Trait04;

  // constructor
  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";
// weergave van beide klassen
print_r($class04);
print_r($class05);

Opmerkingen bij de code

  • regels 3-21: definitie van de klasse [Class04] met een attribuut, de bijbehorende get- en set-methoden en een constructor;
  • regels 23-36: we nemen de code van [Class04] zonder de constructor over en plaatsen deze ongewijzigd in de trait [Trait04]. We nemen de constructor niet over, aangezien een trait niet geïnstantieerd kan worden;
  • regel 23: het is het sleutelwoord [trait] dat ervoor zorgt dat [Trait04] een trait is in plaats van een klasse;
  • regels 38-45: we definiëren een klasse [Class05] die de code van de trait [Trait04] (regel 40) overneemt en er een constructor aan toevoegt (regels 43-45), identiek aan die van de klasse [Class04], om de klasse instantiëerbaar te maken;
  • regel 40: het sleutelwoord [use] maakt het mogelijk een trait in een klasse op te nemen;
  • regels 50-56: uit tests blijkt dat de klassen [Class04] en [Class05] op dezelfde manier werken;

Resultaten

Tim
Burton
Class04 Object
(
    [name:Class04:private] => Tim
)
Class05 Object
(
    [name:Class05:private] => Burton
)

Uit de resultaten in de regels 3-10 blijkt dat de klassen [Class04] en [Class05] dezelfde inhoud hebben;

Conclusie

Het gebruik van de instructie [use Trait] in een klasse staat gelijk aan het opnemen van de code van [Trait] in de klasse.

9.3. Eenzelfde klasse in verschillende klassen gebruiken

Een van de voordelen van de trait lijkt het hergebruik van dezelfde code (attributen + methoden) tussen verschillende klassen te zijn. We zullen echter zien dat we hetzelfde doel kunnen bereiken door gebruik te maken van eenvoudige klassen.

Het delen van een trait tussen klassen wordt geïllustreerd door het volgende script [trait-02.php]:


<?php

trait Trait01 {
  // attribuut
  private $id = 0;

  // methode
  public function doSomething() {
    print "Trait01::doSomething… ($this->id)\n";
  }

}

class Class02 {
  // inclusie Trait01
  use Trait01 {
    // de methode [Trait01::doSomething] is toegankelijk
    // in de klasse onder de naam [doSomethingInTrait]
    Trait01::doSomething as doSomethingInTrait;
  }

  // klasse-specifieke methode
  public function doSomething(): void {
    // id-attribuut
    $this->id += 10;
    // gebruik van de methode Trait01
    $this->doSomethingInTrait();
    // lokale weergave
    print "Class02->doSomething\n";
  }

}

class Class03 {
  // opname van Trait01
  use Trait01;

  // lokale methode van de klasse
  public function doSomethingElse(): void {
    // ID-attribuut
    $this->id += 10;
    // gebruik van de methode van Trait01
    $this->doSomething();
    // lokale weergave
    print "Class03->doSomethingElse\n";
  }

}

// test01 ----------------
function test01(): void {
  $class02 = new Class02();
  $class03 = new Class03();
  $class02->doSomething();
  $class03->doSomethingElse();
}

// test01
print "test01-----------------\n";
test01();

Opmerkingen

  • regels 3-10: een trait die een attribuut (regel 5) en een methode (regels 8-10) definieert.
  • de trait [Trait01] wordt geïnjecteerd in twee klassen: [Class02] (regels 14-32) en [Class03] (regels 34-46).
  • regels 16-20: injectie van [Trait01] in [Class02];
  • regel 19 is bedoeld om een conflict op te lossen: [Trait01] en [Class02] hebben beide een methode met de naam [doSomething]. Er zijn twee gevallen waarmee rekening moet worden gehouden:
    • de methode [Class02::doSomething] wordt van buiten de klasse aangeroepen. In dit geval heeft de methode [Class02::doSomething] voorrang op de methode [Trait01::doSomething] en wordt deze aangeroepen;
    • de methode [Class02::doSomething] wordt vanuit de klasse zelf aangeroepen. In dit geval is er een conflict: de interpreter PHP weet niet welke methode hij moet aanroepen;

Met regel 19 kan de methode [Trait01::doSomething] worden hernoemd naar [doSomethingInTrait]. Binnen [Class02] worden dus de volgende notaties gebruikt:

  • [doSomethingInTrait] om de methode [Trait01::doSomething] aan te roepen;
  • [doSomething] om de methode [Class02::doSomething] aan te roepen;
  • regels 25, 27: de klasse [Class02] gebruikt het attribuut en de methode van [Trait01] alsof deze eigen zijn aan de klasse;
  • regels 34-48: de klasse [Class03] is identiek aan de klasse [Class02]. Het opnemen van [Trait01] is hier eenvoudiger omdat er geen conflict is tussen de methoden van [Trait01] en [Class03];

Resultaten

1
2
3
4
5
test01-----------------
Trait01::doSomething… (10)
Class02->doSomething
Trait01::doSomething… (10)
Class03->doSomethingElse

Merk op dat de eigenschap [Trait01] niet wordt gedeeld tussen de klassen [Class02] en [Class03]. Daardoor wordt deattribuut [Trait01::i] door de opname van [Trait01] in de klassen [Class02] en [Class03] twee verschillende attributen: [Class02::i] en [Class03::i]. Dit blijkt uit de regels 2 en 4 van de resultaten. Als het attribuut [Trait01::i] was verdeeld over de klassen [Class02] en [Class03], dan zou er op regel 4 een 20 hebben gestaan in plaats van een 10.

Het script [trait-03.php] laat zien dat je hetzelfde resultaat kunt bereiken door een klasse te gebruiken in plaats van de eigenschap:


<?php

// klasse die de eigenschap vervangt
class Class01 {
  // attribuut
  private $id = 0;

  // setter
  public function setId(int $id) {
    $this->id = $id;
  }

  // getter
  public function getId(): int {
    return $this->id;
  }

  // methode
  public function doSomething(): void {
    print "Class01::doSomething… ($this->id)\n";
  }

}

class Class02 {
  // inclusie Class01
  private $class01;

  // setter
  public function setClass01(Class01 $class01) {
    $this->class01 = $class01;
  }

  // klasse-specifieke methode
  public function doSomething(): void {
    // attribuut van Class01 wijzigen
    $id = $this->class01->getId();
    $id += 10;
    $this->class01->setId($id);
    // gebruik van methode van Class01
    $this->class01->doSomething();
    // lokale weergave
    print "Class02->doSomething\n";
  }

}

class Class03 {
  // Class01 opnemen
  private $class01;

  // setter
  public function setClass01(Class01 $class01) {
    $this->class01 = $class01;
  }

  // lokale methode van de klasse
  public function doSomethingElse(): void {
    // attribuut van Class01 wijzigen
    $id = $this->class01->getId();
    $id += 10;
    $this->class01->setId($id);
    // gebruik van methode van Class01
    $this->class01->doSomething();
    // lokale weergave
    print "Class03->doSomethingElse\n";
  }

}

// test01 ----------------
function test01(): void {
  // twee objecten
  $class02 = new Class02();
  $class03 = new Class03();
  // krijgen toegang tot twee verschillende instanties van [Class01]
  $class02->setClass01(new Class01());
  $class03->setClass01(new Class01());
  // controle
  $class02->doSomething();
  $class03->doSomethingElse();
}

// test02 ----------------
function test02(): void {
  // gedeelde instantie van [Class01]
  $class01 = new Class01();
  // twee objecten
  $class02 = new Class02();
  $class03 = new Class03();
  // zullen toegang krijgen tot dezelfde instantie van [Class01]
  $class02->setClass01($class01);
  $class03->setClass01($class01);
  // controle
  $class02->doSomething();
  $class03->doSomethingElse();
}

// test01
print "test01-----------------\n";
test01();
// test02
print "test02-----------------\n";
test02();

Opmerkingen

  • regels 4-23: de klasse [Class01] vervangt de klasse [Trait01]. De code van [Trait01] was opgenomen in de code van de klassen [Class02] en [Class03]. Hier zal dat niet het geval zijn. Er zal een verwijzing naar de klassecode [Class01] worden ingevoegd in de klassen [Class02] en [Class03]. Dit betekent dat de attributen van de klasse [Class01] buiten de code van de klassen [Class02] en [Class03] vallen. Aangezien het attribuut [id] hier privé is (regel 6), moeten er een getter (regels 14-16) en een setter (regels 9-11) worden gedefinieerd. Dit is het eerste verschil met de trait: er moet code worden geschreven om toegang te krijgen tot de privé-attributen van de klasse. We hadden de zichtbaarheid van het attribuut [id] kunnen wijzigen naar [public], maar het wordt nooit aangeraden om dat te doen. Door een setter te gebruiken om de waarde van een attribuut in te stellen, kan de geldigheid ervan worden gecontroleerd;
  • regel 27: opname in de code van [Class02] van een verwijzing naar de klasse [Class01]. Omdat dit attribuut privé is, moeten we een setter maken (regels 30-32) om het te initialiseren;
  • regels 37-41: voor elk gebruik van de code van [Class01] moeten we het attribuut [$this→class01] gebruiken;
  • regels 48-69: de klasse [Class03] is een kloon van de klasse [Classe02], behalve dat de methode een andere naam heeft;
  • regels 72-82: de eerste test. Deze bestaat uit het injecteren in de klassen [Class02] en [Class03] van twee verschillende instanties van de klasse [Class01] (regels 77 en 78);
  • regels 85-97: de tweede test voegt in de klassen [Class02] en [Class03] dezelfde instantie van de klasse [Class01] toe (regels 97, 92, 93);
  • regels 100-101: uitvoering van de test [test01];
  • regels 103-104: uitvoering van de test [test02];

Resultaten

test01-----------------
Class01::doSomething… (10)
Class02->doSomething
Class01::doSomething… (10)
Class03->doSomethingElse
test02-----------------
Class01::doSomething… (10)
Class02->doSomething
Class01::doSomething… (20)
Class03->doSomethingElse

Opmerkingen bij de resultaten

  • regels 2-5: we krijgen dezelfde resultaten als bij de trait [Trait01]. Hieruit kunnen we concluderen dat het gebruik van de trait hier niet onmisbaar is, maar dat het wel leidt tot een vermindering van de codelengte, omdat er geen methoden nodig zijn om toegang te krijgen tot de attributen van de trait: deze maken integraal deel uit van de code waarin de trait is opgenomen;
  • regels 7-10: omdat we dezelfde referentie van [Class01] in de klassen [Class02] en [Class03] hebben geïnjecteerd, is het attribuut [Class01::id] gedeeld tussen beide klassen. Daarom wordt in regel 9 van de resultaten 20 weergegeven in plaats van 10 met de trait. Hieruit kunnen we concluderen dat als attributen van de trait tussen klassen moeten worden gedeeld, de trait niet bruikbaar is en er in plaats daarvan een klasse moet worden gebruikt;

9.4. Methoden groeperen in een trait

In het vorige voorbeeld bevatte de trait zowel attributen als methoden. Hier bekijken we het geval waarin de trait alleen methoden bevat. In dat geval lijkt de trait op een samenvoeging van methoden die vervolgens in verschillende klassen kunnen worden gebruikt. Aangezien de trait hier geen attributen heeft, gaan we uit van het geval waarin de methoden die erin zijn samengebracht uitsluitend werken met parameters die eraan worden doorgegeven. Dit is overigens niet verplicht: een trait kan werken met een attribuut [$this→attribut1] zonder dat het dit attribuut zelf heeft. Het is dan aan de klassen die deze trait gebruiken om het attribuut [$this→attribut1] te leveren.

In het geval dat de trait alleen methoden heeft die uitsluitend werken met parameters die eraan worden doorgegeven, zullen we aantonen dat de trait dan kan worden vervangen door een klasse met dezelfde methoden als de trait, die als statisch zijn gedeclareerd.

Het gebruik van de trait wordt geïllustreerd door het script [trait-04.php], dat de trait uit het vorige voorbeeld overneemt en alle attributen verwijdert:


<?php

trait Trait01 {

  // te delen methode
  public function doSomething() {
    print "Trait01::doSomething ….\n";
  }

}

class Class02 {
  // opname Trait01
  use Trait01 {
    // de methode [Trait01::doSomething] is toegankelijk
    // in de klasse onder de naam [doSomethingInTrait]
    Trait01::doSomething as doSomethingInTrait;
  }

  public function doSomething(): void {
    // aanroep van de methode Trait01
    $this->doSomethingInTrait();
    // lokale weergave
    print "Class02->doSomething\n";
  }

}

class Class03 {
  // inclusie van Trait01
  use Trait01;

  // lokale methode van de klasse
  public function doSomethingElse(): void {
    // aanroep van methode van Trait01
    $this->doSomething();
    // lokale weergave
    print "Class03->doSomethingElse\n";
  }

}

// test ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();

Opmerkingen

  • regels 3-10: de trait [Trait01] heeft geen attributen meer;
  • regels 14-18: opname van [Trait01] in [Class02]. De methode van [Trait01] wordt gebruikt op regel 22;
  • regel 31: opname van [Trait01] in [Class03]. De methode van [Trait01] wordt gebruikt op regel 36;

Resultaten

1
2
3
4
Trait01::doSomething ….
Class02->doSomething
Trait01::doSomething ….
Class03->doSomethingElse

In dit gebruiksscenario kan de functie [Trait01] eenvoudig worden vervangen door een klasse. Dit wordt geïllustreerd door het volgende script [trait-05.php]:


<?php

abstract class Class01 {

  // statische methode om te delen
  public static function doSomething() {
    print "Class01::doSomething ….\n";
  }

}

class Class02 {

  public function doSomething(): void {
    // methode van Class01 aanroepen
    Class01::doSomething();
    // lokale weergave
    print "Class02->doSomething\n";
  }

}

class Class03 {

  // lokale methode van de klasse
  public function doSomethingElse(): void {
    // methodeaanroep van Class01
    Class01::doSomething();
    // lokale weergave
    print "Class03->doSomethingElse\n";
  }

}

// test ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();

Opmerkingen

  • regels 3-10: de trait [Trait01] wordt vervangen door een abstracte klasse [Class01] waarvan alle methoden als statisch zijn gedeclareerd. De klasse is uitsluitend als abstract gedeclareerd om te voorkomen dat er een instantie van wordt gemaakt. We hadden ook [final] willen schrijven om afleiding te voorkomen, maar PHP 7 accepteert het voorvoegsel [final abstract] niet voor een klasse. Het is het een of het ander, maar niet beide;
  • regel 16: in plaats van [$this→doSomethingInTrait] te schrijven, schrijven we nu [Class01::doSomething], d.w.z. we roepen de statische methode [doSomething] van de klasse [Class01] aan;
  • regel 28: we herhalen dezelfde procedure in [Class03];

Resultaten

1
2
3
4
Class01::doSomething ….
Class02->doSomething
Class01::doSomething ….
Class03->doSomethingElse

We krijgen inderdaad hetzelfde resultaat als met de trait [Trait01], wat aantoont dat het gebruik ervan kan worden vermeden. We hebben geschreven dat de methoden van een trait kunnen werken op een attribuut [$this→attribut1] zonder dat de trait dit attribuut heeft. Het is dan aan de klassen die deze trait gebruiken om het attribuut [$this→attribut1] te leveren. Dit is een uitzonderlijk geval: je kunt het attribuut [$this→attribut1], dat de klassen die de trait gebruiken moeten hebben, net zo goed ‘doorgeven’ in de trait zelf. Zo maakt het onvermijdelijk deel uit van de attributen van de klasse die de trait gebruikt.

9.5. Meervoudige overerving met een trait

In de literatuur wordt vaak vermeld dat de trait meervoudige overerving mogelijk zou maken: de mogelijkheid voor een klasse om van meerdere klassen te erven. De taal C++ beschikt over deze mogelijkheid, maar de talen Java en C# niet; deze kennen alleen enkelvoudige overerving. We zullen laten zien dat, hoewel het gebruik van een trait in een afgeleide klasse inderdaad de mogelijkheid biedt om iets te implementeren dat lijkt op meervoudige overerving, dit gebruiksscenario ook met gewone klassen kan worden geïmplementeerd.

Het script [trait-06.php] maakt gebruik van een afgeleide klasse en een trait:


<?php

trait Trait01 {
  // attribuut
  private $i;

  // te delen methode
  public function doSomethingInTrait01() {
    // wijziging Trait01::$i
    $this->i++;
    // weergave
    print "Trait01::doSomethingInTrait01… i=$this->i\n";
  }

}

class Class02 {
  // attribuut
  protected $j = 0;

  // methode
  public function doSomethingInClass02(): void {
    // wijziging Class02::j
    $this->j += 10;
    // weergave
    print "Class02->doSomethingInClass02… j=$this->j\n";
  }

}

// afgeleide klasse
class Class03 extends Class02 {
  // erft van Class02:j en Trait01::i
  // opname van Trait01
  use Trait01;

  // methode
  public function doSomethingInClass03(): void {
    // gebruik van methode uit Trait01
    $this->doSomethingInTrait01();
    // wijziging van Trait01::i
    $this->i += 100;
    // wijziging Class03::j (==Class02::j)
    $this->j += 1000;
    // weergave
    print "Class03->doSomethingInClass03… i=$this->i, j=$this->j\n";
  }

}

// test ----------------
(new Class02())->doSomethingInClass02();
(new Class03())->doSomethingInClass03();

Opmerkingen

  • regels 3-15: we keren terug naar een trait [Trait01] met een attribuut en een methode die dit attribuut bewerkt;
  • regels 17-29: een klasse [Class02] die niets te maken heeft met de eigenschap [Trait01]. Deze maakt er geen gebruik van;
  • regel 19: het enige attribuut van [Class02] is gedeclareerd met een zichtbaarheid van [protected], zodat dit toegankelijk is in de afgeleide klassen;
  • regel 32: de klasse [Class03] is een uitbreiding van de klasse [Class02]. Bovendien neemt ze de trait [Trait01] op (regel 35). Ten slotte erft deze klasse de attributen en methoden van [Class02] en neemt ze de attributen en methoden van [Trait01] over. We hebben hier dus inderdaad te maken met iets dat lijkt op meervoudige overerving;

Resultaten

1
2
3
Class02->doSomethingInClass02… j=10
Trait01::doSomethingInTrait01… i=1
Class03->doSomethingInClass03… i=101, j=1000

Net zoals in een eerder voorbeeld zullen we aantonen dat:

  • de trait kan worden vervangen door een klasse;
  • in plaats van de trait in de afgeleide klasse op te nemen, nemen we de verwijzing naar een instantie van de klasse op;

Het script [trait-07.php] ziet er als volgt uit:


<?php

class Class01 {
  // attribuut
  protected $i;

  // getter en setter
  public function getI(): int {
    return $this->i;
  }

  public function setI(int $i): void {
    $this->i = $i;
  }

  // methode
  public function doSomethingInClass01(): void {
    // wijziging Class01::$i
    $this->i++;
    // weergave
    print "Class01::doSomething in Class01… i=$this->i\n";
  }

}

class Class02 {
  // attribuut
  protected $j = 0;

  // klasse-specifieke methode
  public function doSomethingInClass02(): void {
    // wijziging Class02::j
    $this->j += 10;
    // weergave
    print "Class02->doSomethingInClass02… j=$this->j\n";
  }

}

class Class03 extends Class02 {
  // opname Class01
  private $class01;

  // setter
  public function setClass01(Class01 $class01) {
    $this->class01 = $class01;
  }

  // lokale methode van de klasse
  public function doSomethingInClass03(): void {
    // gebruik van methode uit Class01
    $this->class01->doSomethingInClass01();
    // wijziging Class01::i
    $i = $this->class01->getI();
    $i += 100;
    $this->class01->setI($i);
    // wijziging Class03::j
    $this->j += 1000;
    // weergave
    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();

Opmerkingen

  • regels 3-24: de klasse [Class01] vervangt de trait [Trait01]. Aangezien de klasse [Class01] via een verwijzing in de klassen zal worden opgenomen, zijn er get/set-methoden voorzien voor het attribuut [$i];
  • regels 26-38: de klasse [Classe02] blijft ongewijzigd;
  • regel 40: de klasse [Class03] is een uitbreiding van de klasse [Classe02];
  • regel 42: de klasse [Classe01] wordt via een verwijzing opgenomen in [Classe03];
  • regels 45-47: er wordt een setter gedefinieerd om de verwijzing naar de klasse [Classe01] te initialiseren;
  • regels 50-61: de methode [doSomethingInClass03] doet hetzelfde als hierboven, maar met complexere code;

Resultaten

1
2
3
Class02->doSomethingInClass02… j=10
Class01::doSomething in Class01… i=1
Class03->doSomethingInClass03… i=101, j=1000

Uit dit voorbeeld kunnen we concluderen dat de trait ook hier niet onmisbaar is, maar we moeten wel erkennen dat deze het mogelijk maakt om kortere code te schrijven in de afgeleide klasse.

9.6. Een trait gebruiken in plaats van een abstracte klasse

Vaak komt de volgende gebruikssituatie voor: er wordt een vrij algemene interface I gemaakt die aanleiding kan geven tot meerdere implementaties. Deze delen gemeenschappelijke code, maar verschillen in andere methoden. Dit gebruiksscenario kan op twee manieren worden geïmplementeerd:

  1. we maken een abstracte klasse C die de gemeenschappelijke code van de afgeleide klassen bevat. Klasse C implementeert de interface I, maar bepaalde methoden die in de afgeleide klassen moeten worden gedeclareerd, zijn in klasse C als abstract gedeclareerd en daarom is klasse C zelf ook abstract. Vervolgens maken we de klassen C1 en C2, die zijn afgeleid van C en die elk op hun eigen manier de niet-gedefinieerde (abstracte) methoden van hun bovenliggende klasse C implementeren;
  2. we maken een trait T die vrijwel identiek is aan de abstracte klasse C uit de vorige oplossing. Deze trait implementeert de interface I niet, omdat dit syntactisch gezien niet mogelijk is. Vervolgens maken we de klassen C1 en C2, die de interface I implementeren en gebruikmaken van de trait T. Het enige wat deze klassen nog hoeven te doen, is de methoden van de interface I implementeren die niet zijn geïmplementeerd door de trait T die ze importeren;

Hier volgt een voorbeeld dat laat zien hoe dicht deze twee oplossingen bij elkaar liggen.

Toepassing 1 implementeert de eerder beschreven oplossing 1, [trait-08.php]:


<?php

interface Interface1 {

  public function doSomething(): void;

  public function doSomethingElse(): void;
}

abstract class AbstractClass implements Interface1 {
  // attributen
  protected $attr1 = 11;
  protected $attr2 = 12;

  // getters en setters
  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;
  }

  // geïmplementeerde methode
  public function doSomething(): void {
    print "AbstractClass::doSomething [$this->attr1,$this->attr2]\n";
  }

  // niet-geïmplementeerde methode
  abstract public function doSomethingElse(): void;
}

// afgeleide klasse 1
class Class1 extends AbstractClass {
  // attribuut
  private $attr3 = 13;

  // getter en setter
  public function getAttr3() {
    return $this->attr3;
  }

  public function setAttr3($attr3) {
    $this->attr3 = $attr3;
    return $this;
  }

  // implementatie doSomethingElse
  public function doSomethingElse(): void {
    print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
  }

}

// afgeleide klasse 2
class Class2 extends AbstractClass {
  // attribuut
  private $attr4 = 14;

  public function getAttr4() {
    return $this->attr4;
  }

  public function setAttr4($attr4) {
    $this->attr4 = $attr4;
    return $this;
  }

  // implementatie doSomethingElse
  public function doSomethingElse(): void {
    print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
  }

}

// externe functie
function useInterfaceWith(Interface1 $interface):void{
  $interface->doSomething();
  $interface->doSomethingElse();
}

// tests
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());

Opmerkingen

  • regels 3-8: de interface [Interface1] heeft twee methoden;
  • regels 10-41: de abstracte klasse [AbstractClass] implementeert de interface [Interface1] (regel 10). De klasse heeft twee attributen met bijbehorende getters en setters (regels 12-32) en implementeert de methode [doSomething] van de interface [Interface1] (regels 35-37), maar kan de methode [doSomethingElse] niet implementeren. Deze wordt daarom als abstract gedeclareerd (regel 40). De abstracte klasse [AbstractClass] kan niet worden geïnstantieerd en moet, om enig nut te hebben, verplicht worden afgeleid;
  • regels 44-63: de klasse Class1 is een afgeleide van de abstracte klasse [AbstractClass] en implementeert dus de interface [Interface1] (regel 14). Ze vult de methode [doSomethingElse] in, die door de bovenliggende klasse niet was gedefinieerd (regels 59-61). Ze voegt ook een attribuut toe aan de attributen van de bovenliggende klasse (regels 46-56);
  • regels 66-82: de klasse Class2 is een afgeleide van de abstracte klasse [AbstractClass] en implementeert dus de interface [Interface1] (regel 66). Ze vult de methode [doSomethingElse] in, die haar bovenliggende klasse niet had gedefinieerd (regels 80-82). Ze voegt ook een attribuut toe aan die van haar bovenliggende klasse (regels 68-77);
  • regels 87-90: de functie [useInterfaceWith] ontvangt als parameter een type [Interface1] en roept de twee methoden van deze interface aan;
  • regels 93-94: de functie [useInterfaceWith] wordt de eerste keer aangeroepen met een type [Class1] en de tweede keer met een type [Class2]. Dit is correct, aangezien deze twee typen de interface [Interface1] implementeren;

Resultaten


AbstractClass::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
AbstractClass::doSomething [11,12]
Class2::doSomethingElse [11,12,14]

Nu implementeren we oplossing 2 met het script [trait-09.php]. Dit houdt in dat we de abstracte klasse vervangen door een trait:


<?php

interface Interface1 {

  public function doSomething(): void;

  public function doSomethingElse(): void;
}

trait Trait1 {
  // attributen
  private $attr1 = 11;
  private $attr2 = 12;

  // getters en setters
  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;
  }

  // geïmplementeerde methode
  public function doSomething(): void {
    print "Trait::doSomething [$this->attr1,$this->attr2]\n";
  }
}

// afgeleide klasse 1
class Class1 implements Interface1 {
  // gebruik van de eigenschap
  use Trait1;
  // attribuut
  private $attr3 = 13;

  // getter en setter
  public function getAttr3() {
    return $this->attr3;
  }

  public function setAttr3($attr3) {
    $this->attr3 = $attr3;
    return $this;
  }

  // implementatie doSomethingElse
  public function doSomethingElse(): void {
    print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
  }

}

// afgeleide klasse 2
class Class2 implements Interface1 {
  // gebruik van de eigenschap
  use Trait1;
  // attribuut
  private $attr4 = 14;

  public function getAttr4() {
    return $this->attr4;
  }

  public function setAttr4($attr4) {
    $this->attr4 = $attr4;
    return $this;
  }

  // implementatie doSomethingElse
  public function doSomethingElse(): void {
    print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
  }

}

// externe functie die de interface gebruikt
function useInterfaceWith(Interface1 $interface): void {
  $interface->doSomething();
  $interface->doSomethingElse();
}

// tests
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());

Opmerkingen

  • regels 3-8: de interface [Interface1] is niet gewijzigd;
  • regels 10-41: de trait [Trait1] vervangt de abstracte klasse [AbstractClass] uit oplossing 1. De code is hetzelfde, op de volgende details na:
    • regel 10: de trait [Trait1] implementeert de interface [Interface1] niet. Dit is syntactisch onmogelijk;
    • regels 12-13: het zichtbaarheidsattribuut [protected] van de attributen van de abstracte klasse [AbstractClass] wordt hier [private]. Deze twee attributen zijn bedoeld om afgeleide klassen directe toegang te geven tot de attributen van de bovenliggende klasse (protected) of van de trait (private) zonder dat ze via de getters en setters hoeven te gaan;
    • de trait [Trait1] declareert de abstracte methode [doSomethingElse] niet;
  • regels 41-62: de klasse [Class1] uit oplossing 2 is identiek aan de klasse [Class1] uit oplossing 1, op de volgende details na:
    • regel 41: de klasse [Class1] implementeert de interface [Interface1], terwijl deze in oplossing 1 de abstracte klasse [AbstractClass] uitbreidde;
    • regel 43: deze gebruikt de eigenschap [Trait1] om een deel van de interface te implementeren;
  • regels 65-85: hier geldt hetzelfde als voor [Class1];
  • regels 87-95: de rest van de code blijft ongewijzigd;

Resultaten


Trait::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
Trait::doSomething [11,12]
Class2::doSomethingElse [11,12,14]

We krijgen inderdaad dezelfde resultaten.

9.7. Conclusion

Uit de voorgaande voorbeelden blijkt dat het onduidelijk is in welke gevallen het gebruik van de trait een duidelijk voordeel oplevert. In onze voorbeelden kunnen we er altijd zonder doen door deze te vervangen door een klasse. Het lijkt er echter op dat het gebruik ervan handig is om code te factoriseren tussen verschillende afgeleide klassen, alsof deze code tot een bovenliggende klasse behoort. Dat is wat we in het volgende voorbeeld zullen doen.