KingCrunchs kleine Welt

Gedanken über PHP und was mich sonst bewegt…

PHP5.4: Traits aka “Horizontal Reuse”

| 3 Kommentare

Eigentlich schon seit fast 3 Jahren angekündigt und meines Erachtens die sinnvollste Neuerung in PHP seit Einführung der Objektorientierung: “Traits” (oder “Mixins“, dazu später mehr). Als OOP eingeführt wurde, entschied man sich zu einfacher Vererbung (“Single Inheritance“), weils einfacher umzusetzen ist und weil man davon ausging, dass man damit die meisten Use-Cases abdecken könne. Das kann man auch, aber eben nicht alles und vorallem nicht unbedingt alles besonders schön.

Hinter dem Schlagwort “Horizontal Reuse” verbirgt sich ein Konzept, bei dem es darum geht Methoden zwischen Klassen wiederverwendbar zu machen, die nicht, oder nur sehr entfernt verwandt sind, bzw “verwandt gemacht werden können”. Ein Hund kann hüpfen, ein Lowrider auch. Problem ist nun, dass diese beiden Klassen schwer in einem Vererbungsbaum unterzubringen sind, zumindest so, dass es Sinn ergibt. Um trotzdem nicht die Methode zwei mal implementieren zu müssen, gibt es jetzt die Traits.

Für gewöhnlich wird das Verhalten von Traits beschrieben mit “Copy&Paste vom Compiler” und wenn man sich einfach mit der (eventuell etwas vereinfachten) Beschreibung anfreundet, genügt es auch. Traits werden ähnlich wie Klassen definiert, aber weil sie keine Klassen darstellen, können sie nicht instanziert werden. Vielmehr werden Traits von Klassen benutzt (use) und diese Klasse verhält sich nun so, als würde sie alle Properties und Methoden selbst implementieren, die sie vom Trait übernommen hat. Der Vorteil ist, dass man muss nur eine Komponente pflegen muss (Copy&Paste-Programmierung ist eh verpöhnt) und man kann auf Konstruktionen a la Delegation verzichten, vorallen weil das “Copy&Paste”-Verhalten der Traits die Sichtbarkeiten mit einschließt (Stichwort “private“).

trait Jumping {
    public function jump () {
        $class = get_class($this);
        echo "$class jumps!" . \PHP_EOL;
    }
}
class Dog {
    use Jumping;
}
class Lowrider {
    use Jumping {
        jump as private jumpAlias;
    }
    public function jump () {
        $this->jumpAlias();
        echo "Really!" . \PHP_EOL;
    }
}
$dog = new Dog;
$dog->jump();
$rider = new Lowrider;
$rider->jump();

Dog “implementiert” einfach Jumping. Lowrider dagegen überschreibt zusätzlich die Methode jump, aber weil wir die Originalmethode noch aufrufen möchten — sonst hätten wir den Trait gar nicht erst integrieren müssen — erzeugen wir einen Alias “jumpAlias” dafür und setzen ihre Sichtbarkeit auf private herab [1]. Ein wichtiges Detail an dieser Stelle ist, dass wir die Methode dadurch nicht umbenennen, sondern nur einen Alias setzen. Formal ist jump() noch vorhanden und genau so aufrufbar, wird allerdings von der Methode Lowrider::jump() vollständig überdeckt.

Das heißt also, dass Methoden der Klasse die Methoden der Traits überlagern. Dazu gibts es nichts hinzu zu fügen, aber was passiert, wenn zwei Traits versuchen die selbe Methode zu implementieren? Das geht nicht und fliegt einem via FATAL um die Ohren. Sinn dieses doch recht harten Hinweises ist es, dass der Entwickler explizit und selbstständig solche Konflikte auflösen soll. Da mir diesmal nichts passendes einfällt, gibt es leider nur ein abstraktes Beispiel:

trait Foo {
    public function say () {
        echo "I'm Foo!\n";
    }
}
trait Bar {
    public function say () {
        echo "I'm Bar\n";
    }
}
class MyClass {
    use Foo, Bar {
        Foo::say insteadof Bar;
        Bar::say as barSay;
    }
}

Mehrere Traits werden nach dem use-Schlüsselwort als Kommaliste angegeben und im Block steht welche Methode gegenüber einer gleichnamigen Methode welcher anderen Klasse Vorrang hat. Das funktioniert übrigens auch, wenn der zweite Trait gar keine gleichnamige Methode implementiert.

Was noch fehlt sind Properties, die auch so funktionieren, wie man es erwartet, und man kann Traits auch aus anderen Traits zusammen setzen. Weil das etwas langweilig wäre, erläuter ich das an einem anderen Beispiel, das gleich auch zwei Use-Cases abbildet: “Cross-Dependencies” und “Helper”. “Logger” gehören gerne zu den “Cross-Dependencies”, also zu der Art von Komponenten, die sich für gewöhnlich durch verschiedene Schichten ziehen, aber nichts Funktionales zu eben diesen beitragen. Das Problem der Querabhängigkeiten ist, dass man sie nun mal implementieren muss, zB in die Datenbankschicht, obwohl sie mit mit Datenbanken nichts zu tun haben. Mit Traits kann man den Code wenigstens auf ein Minimum reduzieren.

trait Logging {
    private $_logger;
    protected function log ($level, $msg) {
        $x = $this->_logger;
        if (!is_null($x)) {
            $x($level, $msg);
        }
    }
}
trait LogHelper {
    use Logging;
    protected function notice ($msg) {
        $this->log('NOTICE', $msg);
    }
    protected function warning ($msg) {
        $this->log('WARNING', $msg);
    }
    protected function error ($msg) {
        $this->log('ERROR', $msg);
    }
}
class Foo {
    use LogHelper {
      notice as protected;
      error as protected;
    }
    public function __construct () {
        $this->_logger = function ($level, $msg) {
                echo "[$level] $msg\n";
            };
    }
    public function emitNotice () {
        $this->notice ('This is a notice');
    }
    public function emitWarning () {
        // Will fail, because its not imported above
        $this->warning ('This is a warning');
    }
    public function emitError () {
        $this->error ('This is an error');
    }
}

Bei diesem Code-Beispiel gibt es noch eine Besonderheit: Die Klasse initialisiert die private Eigenschaft des Logging-Traits. Erstens: Es funktioniert. Ja, tatsächlich sind die privates auch direkt in der Klasse selbst zugreifbar, was auch Sinn ergibt, denn die Klasse “besteht” auch aus den Methoden und Eigenschaften des Traits. Zweitens: Ein Trait hat keinen Konstruktor. Ergibt auch Sinn, denn es ist schießlich keine vollwertige Klasse, die instanziert werden kann. Ergo muss also die Klasse, die — wie wir grad schon festgestellt haben auch aus dem Code des Traits “besteht” — auch die Eigenschaften der Traits in einen sinnvollen Zustand bringen.

Wen es zum Abschluss noch interessiert: Wieso denn jetzt “Mixin”? Eigentlich bietet ein Trait per Definition nur Verhalten, also ausschließlich Methoden. Das letzte Beispiel zeigte es schon, die PHP-Implementierung kennt auch Properties. Mixins dagegen kennen Properties, aber jetzt gibt es aber das Problem, dass Mixins dagegen eigentlich kein Methoden-Alias, kein Ausschluss von Methoden, usw kennt. Es geht in erster Linie nur um einen kleinen Namensdisput, der aber in beide Richtungen nicht 100%ig hinhaut. Es könnte allerdings sein, dass die Traits morgen schon nicht mehr “Traits” heißen ;)

Ich habe den Code übrigens mit dem seit alpha2 — mehr oder weniger offiziell released — eingebauten Webserver getestet :) Jetzt fehlt nur noch IDE-Support (zu PhpStorm schiel). Und gegen DocBlox-Unterstützung hätte ich auch nichts einzuwenden.

—-

[1] Ich habe bisher keine Möglichkeit gefunden überschriebene Trait-Methoden aufzurufen, deshalb zunächst dieser work-around über die Umbenennung. Auf jeden Fall sollte man erstmal traitName::methode() vermeiden, weil das macht recht merkwürdige Sachen…

https://wiki.php.net/rfc/traitsmodifications

https://wiki.php.net/rfc/horizontalreuse

https://wiki.php.net/rfc/nonbreakabletraits

3 Kommentare

  1. Hi King,

    danke für diesen tollen Artikel,
    dank dir hab ich jetzt endlich verstanden was Traits sind
    und für was sie gut sind :)! Vielen Dank!

    Grüße
    David

  2. Gut und verständlich geschrieben; ausführlich Beispiele.

    Verständnisfrage:

    Wieso muss das im letzten Codebeispiel in den Zeilen 16/19 nicht
    [code]
    $this->log(...);
    [/code]
    heißen?

Hinterlasse eine Antwort

Pflichtfelder sind mit * markiert.


Bad Behavior has blocked 203 access attempts in the last 7 days.

Page optimized by WP Minify WordPress Plugin