Die Vor- und Nachteile von Traits
Traits wurden mit PHP 5.4 eingeführt und werden gefühlt selten eingesetzt. Hier können wir es mit einem Henne, Ei Problem zu tun haben - so lang keiner Traits einsetzt, sieht niemand wie man Traits einsetzt - oder das Problem liegt an dem etwas schlechten Ruf, der ihnen Anhängen.
Wir wollen uns hier einmal Traits von verschiedenen Sichtweisen annähern und ein paar Beispiele aufzeigen, wo Traits vom Nutzen sein können, oder wo sie sich eher als Krücke bei einem gesunden Bein herausstellen.
Grundbetrachtung, was ist ein Trait
Sucht man nach Einsatzmöglichkeiten von Traits, so werden vor allem zwei Punkte genannt, bei denen sie Abhilfe von Problemen schaffen sollen. Das erste wäre die nicht vorhandene Mehrfachvererbung von Klassen, wie es sie z.B. bei Python gibt. Der zweite ist ähnlicher Natur, hat aber nicht direkt etwas mit der Vererbung zu tun. Copy and Paste Programming, das heißt dass Vorhanden sein von gleichen Code-Bereichen in mehreren Strukturen wird oft als unsauber und schwer pflegbar angesehen.
Dem dritten Punkt bin ich im Internet nicht häufig (wenn überhaupt) begegnet, trotzdem denke ich, dass man dem Paradigma des Duck Typings mit Traits noch näher kommt. Duck-Typing beschreibt ein Konzept der objektorientierten Programmierung, bei dem der Typ eines Objektes nicht durch seine Klasse beschrieben wird, sondern durch das Vorhandensein bestimmter Methoden oder Attribute[1].
“When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.”
„Wenn ich einen Vogel sehe, der wie eine Ente läuft, wie eine Ente schwimmt und wie eine Ente schnattert, dann nenne ich diesen Vogel eine Ente.“
Um uns mit dem Thema genauer befassen zu können, sollen wir erst einmal einen einfachen Einsatz eines Traits genauer betrachten.
Grundlegender Einsatz eines Traits
<?php
trait myTrait
{
public function init()
{
echo 'I`m alive';
}
}
class a
{
use myTrait;
public function __construct()
{ $this->init();
}
}
$instance = new a();
Ausgabe:
I'm alive
https://3v4l.org/2XDc8
An diesem Beispiel sehen wir, dass ein Trait ein ausgelagerter Code-Bereich ist, den wir flexibel in Klassen einsetzen können. Wir könnten hier also für verschiedene init Varianten entsprechende Traits anbieten, ohne den Rest der Klasse zu berühren. Die Basis könnte hier dann eine abstrakte Klasse bieten, die eine init Funktion vorraussetzt und verschiedene Traits für entsprechende Einsatzbereiche
abstract class Image
{
/** int */
protected $width;
/** int */
protected $height;
abstract public function init();
public function showImage()
{
[...]
}
}
trait defaultAvatarImageTrait
{
public function init()
{
$this->width = 100;
$this->height = 100;
}
}
trait defaultPreviewImageTrait
{
public function init()
{
$this->width = 320;
$this->height = 256;
}
}
class AvatarImageEntity extends Image
{
use defaultAvatarImageTrait;
}
class PreviewImageEntity extrends Image
{
use defaultPreviewImageTrait;
}
Beispiele sind immer ein wenig Konstruiert und somit wäre in der realen Wildbahn dieser Einsatz soweit ein wenig in Frage zu stellen, da dieser Trait mit Sicherheit kein zweites mal im Einsatz wäre. Es soll aber auch nur auf die generelle Funktionsweise hingewiesen werden; in späteren Beispielen werden wir eher sinngebende Möglichkeiten sehen.
Die Sache mit den Enten: Reflection von Traits
Behandelt man Objekte nach dem Duck-Typing Prinzip, müssen wir wissen, welche Aufgaben diese Klassen übernehmen können. Klassisch übernehmen Interfaces hierbei die Aufgabe; d.h. eine Klasse kann Schwimmen, wenn sie z.B. ein SwimAwareInterface besitzt. Klappt dieses aber auch mit Traits?
<?php
trait mytrait {
public function init() {
echo 'look mum, I`m a lumberjack' . PHP_EOL;
}
}
class a {
use mytrait;
public function __construct() {
echo 'I`m alive' . PHP_EOL;
$this->init();
}
}
$instance = new a();
var_dump($instance instanceof mytrait);
var_dump(class_uses($instance));
Ausgabe:
I'm alive
look mum, I'm a lumberjack
bool(false)
array(1) {
["mytrait"]=>
string(7) "mytrait"
}
https://3v4l.org/JH2E2
In diesem Beispiel sehen wir, dass wir nicht mit 'instanceOf' überprüfen können, sondern mit in_array(class_uses({class})), was der Performance natürlich nicht dienlich ist. Zu erwähnen wäre noch ReflectionClass, welche aber zum aktuellen Stand nicht gut dokumentiert und nicht auf jedem System vorhanden ist. Die Handhabung ist hier aber auch ähnlich langsam, da wir nur ein Array mit den Traits erhalten. Eine weitere Schwäche ist in beiden Fällen der Punkt, dass nur die Tratis der aktuellen Klasse, nicht aber einer Eltern-Klasse angezeigt werden würden.
Aus diesen Gründen kann man den Einsatz von Traits eigentlich nur in Kombination mit der Verwendung von Interfaces raten. Aber schauen wir uns noch kurz weitere Probleme bei der Vererbung an.
Probleme bei der Vererbung von Sichtbarkeiten von Funktionen bei Klassen (public, protected, private)
<?php
class a {
final public function __construct() {
$this->init();
}
protected function init() {
echo 'I`m protected' . PHP_EOL;
}
}
class b extends a {
public function init() {
echo 'free es a bird' . PHP_EOL;
parent::init();
}
}
class c extends a {
private function init() {
echo 'I`m the invisible man';
}
}
$instance = new b();
$instance = new c();
Ausgabe:
Fatal error: Access level to c::init() must be protected (as in class a) or weaker in /in/l6r21 on line 24
Process exited with code 255.
https://3v4l.org/l6r21
In diesem Beispiel sehen wir, dass wir beim ableiten klassen nur stärker einschränken können - init kann in klasse b nicht public gemacht werden, da diese in der Elternklasse a schon protected ist. Bei Traits verhält es sich leider nicht so, wie wir an folgendem Beispiel sehen können:
<?php
trait mytrait {
protected function init() {
echo 'look mum, I`m a lumberjack' . PHP_EOL;
}
}
class a {
use mytrait;
public function __construct() {
echo 'I`m alive' . PHP_EOL;
$this->init();
}
}
class b extends a {
public function init() {
echo 'And now to something compleatly different';
}
}
$instance = new b();
Ausgabe:
I`m alive
And now to something compleatly different
https://3v4l.org/rnZ0X
Hier wird die protected funktion aus dem Trait, welche in Klasse a gesetzt wird einfach durch die Klasse b überschrieben, obwohl die Funktion hier nur auf public gesetzt ist. Dieses kann zum einen leider schnell zur Verwunderung führen, wenn eine Funktion aus einem Trait in Unwissenheit überschrieben wird, zum anderen kann es somit auch schnell zu Sicherheitsproblemen kommen. Aber vielleicht kommen wir der Sache mit einem Trick entgegen.
<?php
trait mytrait {
final public function init() {
echo "look mum, I'm a lumberjack" . PHP_EOL;
}
}
class a {
use mytrait;
public function __construct() {
echo "I'm alive" . PHP_EOL;
$this->init();
}
}
class b extends a {
public function init() {
echo "And now to something compleatly different";
}
}
$instance = new b();
Ausgabe:
Fatal error: Cannot override final method a::init() in /in/sWkLa on line 17
Process exited with code 255.
https://3v4l.org/sWkLa
Mit dem Keyword final können wir also ein wenig entgegenwirken, aber ist dieses Bulletproof?
<?php
trait mytrait
{
final protected function init() {
echo 'The final countdown' . PHP_EOL;
}
}
class a
{
use mytrait;
public function __construct() {
$this->init();
}
final public function init() {
echo 'We didn`t stop the fire';
}
}
$instance = new a();
Ausgabe:
We didn't stop the fire
https://3v4l.org/IQpC5
<?php
trait mytrait
{
final protected function init() {
echo 'The final countdown' . PHP_EOL;
}
}
class a
{
use mytrait;
public function __construct() {
$this->init();
}
}
class b extends a
{
final public function init() {
echo 'We didn`t stop the fire';
}
}
$instance = new b();
Ausgabe:
Fatal error: Cannot override final method a::init() in /in/0t0KU on line 19
Process exited with code 255.
Wir sehen dass auch dieses nicht bis ins letzte Funktioniert. Ist die Funktion in der Klasse selbst auch als final deklariert, so können wir diese überschreiben; erst wenn wir die klasse selbst wieder ableiten, ist das final im Trait sicher und verhindert eine neue deklaration.
Um also 100%ig sicher zu gehen, sollte daher gelten, dass Funktionen im Trait und Funktionsbeschreibung im Interface Bijektiv zu betrachten sind (in keinem befinden sich mehr oder weniger Funktionen) und diese Funktionen dürfen nie in der Klasse verändert werden. Das man aber direkt in einer Klasse eine Funktion aus einem Trait überschreibt, welche man unmittelbar eingebunden hat, wird wohl seltener der Fall sein - gute IDEs werden einen da sicher auch unterstützen. Ein Trait vollkommen blind einsetzen, sollte man auch nicht - wer aber ein Aspekt einer Klasse vollständig über die Kombination Interface-Trait abhandelt, sollte keine Probleme bekommen.
Noch mehr Probleme
Namen sind Schall und Rauch - und somit wird es früher oder später einmal vorkommen können, dass man in einer Klasse eine Anzahl von Traits >= 2 einsetzen möchte und in die Verlegenheit kommt, dass in zwei Traits eine Funktion mit dem selben Namen schon vorhanden ist. Dieses ist durch den Einsatz von Aliase per as absolut kein Problem. Das Merkwürdige ist, dass hier die Möglichkeit besteht, den Zugriff einer Funktion zu verändern, wie folgendes Beispiel zeigt:
<?php
trait mytrait
{
protected function invisible()
{
echo "I'm the invisible man";
}
}
class a
{
use mytrait { invisible as public init; }
}
$instance = new a();
$instance->init();
Ausgabe:
I'm the invisible man
https://3v4l.org/9fpbD
Trotz längerer Recherche ist mir kein Fall unter gekommen, wo es Sinn bringend war, dieses zu tun; daher kann hier nur der Rat sein, die Stufe nie schwächer werden zu lassen.
Performance
Neben der Frage des Stiles ist die Frage der Performance mit Sicherheit eine wichtige. Hier hat sich in der Zeit einiges getan. Merkwürdiger weise haben einige bei Traits von einem Copy'n'Paste, welches der Interpreter macht gesprochen. Die Beispiele zeigen aber, dass ein wenig mehr passiert, da zumindest Benennungen und Sichtbarkeiten überprüft werden.
Was die Performance direkt angeht, so muss man zwischen den einzelnen Versionen im 7er Strang unterscheiden. Leider ist man aus verständlichen Gründen bei ev4l ein wenig in den Möglichkeiten beschränkt, aber bei einem Test mit einer Klasse und einer Klasse in der die Funktion in einen Trait ausgelagert ist bei 10.000 Wiederholungen kann man schon gut die Tendenz erkennen.
Klasse klassisch
<?php
class a {
protected $collection = [];
public function setAttribute(string $key, string $value) {
$this->attribute[$key] = $value;
}
public function getAttribute(string $key) {
return empty($this->attribute[$key]) ? 'Nope' : $this->attribute[$key];
}
}
for ($i = 0; $i < 10000; $i++) {
$instance = new a();
$key = chr(mt_rand(33, 126));
$instance->setAttribute(
$key,
chr(mt_rand(33, 126))
);
echo $instance->getAttribute($key);
}
Performance:
https://3v4l.org/cPXi7/perf#output
Klasse mit Trait
<?php
function make_seed()
{
list($usec, $sec) = explode(' ', microtime());
return $sec + $usec * 1000000;
}
mt_srand(make_seed());
trait mytrait {
protected $collection = [];
public function setAttribute(string $key, string $value) {
$this->attribute[$key] = $value;
}
public function getAttribute(string $key) {
return empty($this->attribute[$key]) ? 'Nope' : $this->attribute[$key];
}
}
class a {
use mytrait;
}
for ($i = 0; $i < 10000; $i++) {
$instance = new a();
$key = chr(mt_rand(33, 126));
$instance->setAttribute(
$key,
chr(mt_rand(33, 126))
);
echo $instance->getAttribute($key);
}
Performance:
https://3v4l.org/1UqfB/perf#output
Wir können hier beobachten, dass im klassischem Fall die Verarbeitungszeit langsamer, in dem Fall mit dem Trait schneller werden. Hier wird man also weiter beobachten müssen, wie es sich in Zukunft mit einer möglichen Version 7.4 und spätestens mit der Version 8.0 mit einem JIT verhalten wird. Immerhin können wir von einer Varlangsamung von 50% bzw. um eine Beschleunigung von 10% bei einer Differenz von auch ca 10% sprechen.
Testbarkeit
Nachdem wir den generellen Einsatz und dann die Performance betrachtet haben, werfen wir noch kurz einen Blick auf die Testbarkeit von Traits. Die ersten beiden Ideen, die einem evt. kommen könnten wäre ein Trait direkt in eine Testklasse einzuinden, oder aber eine Klasse extra für ein Trait anzulegen, welche dann getestet werden könnte. Werfen wir einen kurzen Blick auf die erste Option:
<?php
trait SomethingWithTextTrait
{
public function testTextWidth($text)
{
// [...]
}
}
class ImportantTextTest extends PHPUnit_Framework_TestCase
{
use SomethingWithTextTrait;
}
Wir müssen hier gar nicht mehr beschreiben, es wird aber klar, dass hier eine reguläre Funktion aus einem Projekt auf einmal als Test-Case angesehen werden würde und unweigerlich zumindest einen Fehler 'Test without assert' werfen würde.
Die zweite Möglichkeit wäre mit Sicherheit ein probates Mittel, wirkt aber unschön, da wir irgendwo eine lose Klasse rumfliegen hätten, die keine weitere Funktion besäße, als ein Trait zu besitzen. Aber hier bietet PHP mit anonymen Klassen abhilfe:
<?php
trait SomethingToTest
{
public function testFunktion()
{
return true;
}
}
class TraitClassTest extends PHPUnit_Framework_TestCase
{
protected $testClass = null;
public function setup()
{
$this->testClass = new class { use SomethingToTest; };
$this->assertTrue($this->testClass->testFunktion());
}
}
Da wir durch die Betrachtung aus Abschnitt I von protected/private Funktionen eher Abstand nehmen wollen, sollten die meisten Fälle somit abgegolten sein. Sollte man aus Gründen trotzdem einmal eine Funktion in einem Trait nutzen wollen, die als protected oder private deklariert ist, so besteht noch immer die Möglichkeit diese über ein Mock zu testen. Dieses gilt auch, wenn innerhalb des Traits eine abstrakte Funktion beschrieben wird [2].
<?php
trait AbstractTrait
{
public function concreteMethod()
{
return $this->abstractMethod();
}
public abstract function abstractMethod();
}
class TraitClassTest extends PHPUnit_Framework_TestCase
{
public function testConcreteMethod()
{
$mock = $this->getMockForTrait('AbstractTrait');
$mock->expects($this->any())
->method('abstractMethod')
->will($this->returnValue(TRUE));
$this->assertTrue($mock->concreteMethod());
}
}
Ridiculous?
Ein weiterer Fall von 'Auch wenn mir jetzt keine Situation einfällt, aber würde es funktionieren' wäre Trait innerhalb von Traits. Hier können wir es kurz machen und sagen: Ja es geht - aber warum.
<?php
trait MyFirstTrait
{
public function something()
{
echo 'And I`m not an alien' . PHP_EOL;
}
}
trait MySecondTrait
{
use MyFirstTrait;
public function somethingElse()
{
echo 'I have to admit:' . PHP_EOL;
$this->something();
}
}
class a
{
use MySecondTrait;
}
$instance = new a();
$instance->somethingElse();
Ausgabe
I have to admit:
And I`m not an alien
https://3v4l.org/GRICM
Fazit
Unproblematisch sind Traits mit Sicherheit nicht, werden sie unüberlegt, in großer Anzahl in verzweigten Klassen eingesetzt. Hält man sich aber ein paar Grundregeln und bewegt man sich eh im SOLID Feld, so können sie mit ein paar Verhaltensregeln ein hilfreiches Werkzeug sein. Einen guten Einsatz zeigt das Zend-Framework mit dem EventManagerAwareInterface und EventManagerAwareTrait bzw. dem ListenerAgregateInterface und ListenerAggregateTrait. Hier entfällt die Notwendigkeit von entsprechenden Services oder Listener aus ZendFramework direkt abzuleiten. Wir können z.B. einen BasisService erstellen, der erst einmal nicht auf Events eingehen muss. Ein abgeleiteter Service erhält jetzt aber die Möglichkeit über Interface und Trait auf Events reagieren zu können, ohne dass wir in Ableitungs-Verlegenheit zu geraten.