Viel Wind um Traits – was soll das alles?

Es ist nicht noch nicht lange her, da erschien die neue Beta 2 der PHP-Version 5.4. Diese beinhaltet die mittlerweile doch schon an einigen Stellen im Internet diskustierten Traits. Grund genug, damit ich mir die Sache auch einmal ansehe und aus meiner Perspektive beleuchte. Und nein, ich bringe nicht das Singleton-Beispiel. Dieser Artikel soll auch nicht die Funktionsweise von Traits erklären, sondern deren Sinnhaftigkeit und Anwendung diskutieren. Wer sich mit dem Thema Traits bisher noch nicht beschäftigt hat, ist deshalb mit dieser kleinen Einführung gut bedient: http://blog.itws.de/578/php-5-4-feautre-traits-horizontaler-code-reuse/

Ist das wirklich Mehrfachvererbung?

Man könnte der Meinung sein, Traits seien eine Art weiteres Konzept der Mehrfachvererbung. Während ich mir so meinen Kopf darüber zerbrochen habe, ob PHP diese Funkionalität in der Vergangenheit wirklich gefehlt hat, kam bei mir die Frage auf: Machen wir hier nicht einen Salto rückwärts? Immerhin unterstützt PHP bereits Interfaces und damit ein ausgereiftes Konzept zur Mehrfachvererbung. Mehr noch: Über die Jahre hat sich das Konzept der mehrfachen Klassenvererbung, wie es in C++, Perl oder Python ermöglicht wird, als problematisch erwiesen – hauptsächlich aufgrund von Mehrdeutigkeiten. Allen voran stößt man hierbei immer wieder auf das Diamond Problem. Klären wir also zunächst mal, ob man bei Traits wirklich von Mehrfachvererbung sprechen kann. Nehmen wir an, folgendes würde in PHP funktionieren:

class A {
  public function a() {
   return 'a';
  }
}

class B {
  public function b() {
    return 'b';
  }
}

class C extends A, B {}

$c = new C();
echo $c->a();
echo $c->b();

Obiger Code ist natürlich nicht lauffähig, wäre aber eine mögliche Syntax in PHP für klassenbasierte Mehrfachvererbung. Nun das ganze mit Traits. Die nachfolgende Syntax wird mit PHP 5.4 laufen:

trait A {
  public function a() {
    return 'a';
  }
}

trait B {
  public function b() {
    return 'b';
  }
}

class C {
  use A, B;
}

$c = new C();
echo $c->a();
echo $c->b();

Auf den ersten Blick könnte man Traits also das gleiche Verhalten konstatieren, wie mit einer klassenbasierten Mehrfachvererbung. Dem ist aber nicht so. Einen wichtigen Aspekt habe ich nämlich bisher nicht angesprochen: Die is_a-Beziehung. Um vollständig die Mehrfachvererbung zu simulieren, müsste das Beispiel daher so aussehen:

interface A {
  public function a();
}

interface B {
  public function b();
}

trait traitA {
  public function a() {
    return 'a';
  }
}

trait traitB {
  public function b() {
    return 'b';
  }
}

class C implements A, B {
  use traitA, traitB;
}

$c = new C();
echo $c->a();
echo $c->b();

var_dump($c instanceof A); //--> true
var_dump($c instanceof B); //--> true

Spätestens jetzt sollte jedem Leser klar sein, das Traits nicht mit dem Konzept der Mehrfachvererbung gleichzustellen sind. Die zwei Gründe dafür sind:

  1. Traits stellen keine Relationen zwischen Klassen dar. is_a-Beziehungen werden nicht beeinflusst.
  2. Es wird schlichtweg nichts von oben nach unten vererbt, sondern einfach stur horizontal kopiert. Man könnte Traits deshalb auch als Copy and Paste des Compilers ansehen.

Darüber hinaus muss man noch die Feingranularität erwähnen. Erbt man beispielsweise von einer richtigen Klasse, werden – zumindest in PHP – alle Eigenschaften und Methoden vererbt. Man bekommt also immer alles – ob man das nun möchte oder nicht. Mit Traits kann Funktionalität fein gesplittet werden, ohne dass man mehrere Basisklassen erzeugen müsste. Außerdem können bei der Übernahme von Methoden auch die Modifier geändert werden (z.B. von public auf protected). Zusätzlich können Traits auch nicht instaziiert werden. Sie stellen keine Objekte, sondern lediglich “Codeschnipsel” dar.

Traits – ein alter Hut!?

Nachdem wir geklärt haben Traits künftig nicht mehr mit Mehrfachvererbung gleichzustellen, kann man aus jener Sicht auch nicht mehr behaupten, es sei ein Konzept der klassenbasierten Mehrfachvererbung, das aus C++ & Co kopiert wurde. Und dennoch: Eigentlich hat das C++ bereits Jahre lang in Form des Präprozessors. Nachfolgend das obige Trait-Beispiel in C++:

#include <iostream>

#define A char a() { return 'a'; };
#define B char b() { return 'b'; };

class C {
  public:
    A
    B
};

int main() {
  C c;
  std::cout << c.a() << std::endl;
  std::cout << c.b() << std::endl;
  return 0;
}

Außerdem gab es bereits zahlreiche Sprachen vor PHP, die Traits einführten. In diesem Zusammenhang stößt man schnell auf Self oder Scala. Aber auch Sprachen wie Perl planen die Einführung von Traits – mit der künftigen Version 6. PHP folgt hier ehr einem alten Hut, als einem neuem Trend.

Alternativen?

Kann man das denn nicht auch anders machen? Mit includes/requires z.B.?

class B {
  require('./method.php');
}

Nein, das liefert einen Parser-Fehler und funktioniert daher nicht.
Und mit eval? Äh ja, diese Idee verwerfen wir gleich mal wieder, denn eval ist eval, langsam und wäre in Sachen Implementierung deutlich umständlicher. Dann hätten wir da noch Reflections. Diese sind meines Wissens im Moment aber höchstens dazu im Stande gerade einmal den Modifier einer Methode zu ändern – nicht aber ganze solche einem Objekt hinzuzufügen. Der grundlegende Unterschied dabei wäre außerdem das Kopieren der gewünschten Programmteile zur Laufzeit. Traits existieren zur Laufzeit nicht mehr – sie wurden vorher bereits vom Compiler aufgelöst. Allerdings kann man Traits über die Reflection-API zur Laufzeit abfragen. Dafür werden drei neue Methoden zur Verfügung gestellt:

ReflectionClass:: getTraitAliases()
ReflectionClass:: getTraitNames()
ReflectionClass:: getTraits()

Verlassen wir damit den Absatz der verrückten Ideen und kommen zu den Problemen mit Traits.

Probleme und Eigenheiten im Umgang mit Traits

Namenskonflikte stehen für mich unter einem besonderem Licht. PHP bietet hierfür eine Erweiterung der use-Direktive an. Innerhalb von use A,B,C,… { … } kann eine Methode oder Eigenschaft entweder per instead-Schlüsselwort direkt bestimmt, oder per as-Schlüsselwort auch umbenannt werden, um Konflikte zu vermeiden.

Oben hatte ich bereits kurz das Diamond-Problem angesprochen. Erbt eine Klasse von zwei anderen Klassen, die widerum ein und die selbe Basisklasse besitzen, muss irgendetwas entscheiden, welche Methoden der Ober-Ober-Klasse nun die richtigen sind. Auch wenn Traits nur eine horizontale Code-Kopie darstellen, können auch hier die gleichen Probleme auftreten, wenn man z.B. verschachtelte Traits nutzt. Daher sind die obigen Lösungen von Namenskonflikten unabdingbar. Beispiel:

<?php

trait A {
   public function a() {
     return 'a';
   }
}

trait B {
  use A;
}

trait C {
  use A;
}

class D {
  use B, C {
    B::a insteadof C;
  }
}

$d = new D();
echo $d->a();

Ein anderer Sachverhalt, den man sich bewusst machen sollte: Traits überschreiben gleichnamige Methoden einer Oberklasse:

trait T {
  public function a() {
    return 'a in T';
  }
}

class A {
  public function a() {
    return 'a in A';
  }
}

class B extends A {
  use T;
}

$b = new b();
echo $b->a(); //-> A in T

Dies ist eigentlich selbstverständlich, wenn man ein wenig darüber nachdenkt. Immerhin wird nichts anderes gemacht, als eine neue, gleichnamige Methode in die erbende Klasse eingefügt. Würde man das manuell durchführen, wäre das Ergebnis das gleiche. Was aber, wenn man die Methode direkt in der eigenen Klasse hat, die auch das Trait benutzt? In diesem Fall wird das Trait einfach überschrieben:

trait T {
  public function a() {
    return 'a in T';
  }
}

class B {
  use T;
  public function a() {
    return 'a in B';
  }
}

$b = new b();
echo $b->a(); //-> A in B

Brauchen wir das eigentlich?

An diesem Absatz scheiden sich einmal wieder die Geister. Man findet sich fast ein wenig in jene Zeit zurückversetzt, in der PHP-Programmierer über Sinn- und Zweck von Template-Engines wie Smarty lange diskutierten, oder den Nutzen der Einführung von Goto debattierten. Traits sind sicherlich ein interessantes und vor allem hilfreiches Feature, wenn es darum geht Code nicht zu duplizieren. Nun kann man sagen, wer seinen Code nicht duplizieren möchte, kann diesen auch einfach auslagern und wiederverwenden:

class A {
  public static function a() {
    return 'a';
  }
}

class B {
  public function a() {
    return A::a();
  }
}

class C {
  public function a() {
    return A::a();
  }
}

Wie im Beispiel gezeigt, funktioniert das eigentlich recht einfach. Allerdings benötigt man dafür mehr Code, als mit Traits. Und bei Zugriff auf klasseninterne Variablen muss man diese entweder übergeben, oder sich etwas Komplexeres ausdenken. Dazu kommt, für jede übernommene Methode muss man zumindest den Rumpf implementieren – eine stupide Angelegenheit. Traits nehmen uns also viel Arbeit ab.

Fazit

Das Ziel von Traits ist die leichte Wiederverwendung von Code und das Ersparen von Schreibarbeit. Durch die Trennung von Code-Kopierung und is_a-Relationen ist man flexibler als früher. Dazu kommt, dass man sehr feinkörnig bestimmen kann, welche Methoden man wiederverwenden möchte. Im Hinterkopf sollte man aber immer die möglichen, problematischen Konstellationen haben. Traits sind wahrscheinlich Fluch und Segen zugleich. Wie auch bei der klassenbasierten Mehrfachvererbung kann leicht unwartbarer Programmcode entstehen. Letztendlich liegt es also in der Verantwortung jedes Programmierers zu entscheiden, ob das neue PHP-Feature in seinem Code Sinn macht – oder eben nicht.

Weiterführende Links

3 thoughts on “Viel Wind um Traits – was soll das alles?

  1. Auch ich war erst, wie du es hier zu sein scheinst, skeptisch. Jedoch haben Traits ihre Stärken in der “horizontalen”. Es ergeben sich neue Möglichkeiten für Methoden, die man möglichst breit gefächert über seine Klassen verteilen möchte, etwa einen Type-Checker oder den Config-Access. Interessant wird es auch bei Validator-Regeln, die aus anderen Regeln bestehen. Etwa: Die gruppierte Validator-Regel-Klasse “ValidZipCodeDE” besteht aus den Validator-Regeln “ValidNumeric()” und “ValidLength()”.

  2. Moin, einer der besten Artikel die ich bisher zum Thema Traits in PHP 5.4 finden konnte. Dann könnte ich bestimmte Methoden von verschiedenen Klassen meines Blogsystems ja auch auslagern. Nehmen wir an, die Klassen “Article”, “Category” und “Page” haben jeweils die Methoden “getID”, “getName”, “getTime”, “getBody” usw.

    Dann könnte man diese Methoden ja in einen Trait auslagern. Das würde doch Sinn machen, oder? Ich denke schon. Ich frag abe mal zur Sicherheit nochmal nach. :)

    PS: Ich finde die anderen Neuerungen in PHP 5.4 (wie zum Beispiel die kurze Array-Syntax) ziemlich gut.

    MFG BlackY =)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>