Facebook
Twitter
Google+
Kommentare
0

Der Klasse neue Kleider: Traits

Gastartikel von Thomas Worm.

Ich bin 23 Jahre alt und Verbundstudent bei der DATEV eG. Bereits seit Schulzeiten befasse ich mich intensiv mit der Webentwicklung in PHP. Der Erfahrungsschwerpunkt hierbei liegt auf der Programmierung barrierefreier Portalsysteme, die auf Typo3 aufsetzen (wie z.B. huerdenlos.de).

Einführung

Mit der bald erscheinenden PHP-Version 5.4 wird ein neues Sprachkonstrukt in die PHP-Welt Einzug halten, die Traits. Grob gesagt lässt sich mit diesen eine Methodengruppe definieren, die in verschiedenen Klassen durch eine use-Anweisung eingefügt werden kann. Sicherlich stoßen da nun Einige auf die gleiche Frage wie ich: “Wofür brauch ich das denn bitte? Vererbung ist doch durchaus ausreichend!”

Jain. Mit Vererbung lässt sich viel machen, doch Vererbung ist nicht immer das beste Mittel für wiederverwendbaren Code. Auch folgt PHP dem Java-OOP-Modell, bei dem nur einfache Klassenableitung (single inheritance) möglich ist. Wir können also nur von einer einzigen Vaterklasse erben.

Um Traits zu verstehen, muss man sich klar machen welchen Sinn Vererbung und im Gegensatz dazu Traits haben:

  • Bei der Vererbung stehen zwei Klassen in unmittelbarem Zusammenhang, in ihnen fließt sozusagen das selbe Blut. Man kann dies sehr einfach am Menschen betrachten: Es gibt geerbte Eigenschaften und Verhalten, die unmittelbar zu einem Menschen gehören und von den Eltern direkt übernommen werden, wie z.B. Staatsangehörigkeit, Hautfarbe, dass wir sprechen können, usw.. Natürliche kann es hier vorkommen, dass sich auch mal Sachen verändern (Genveränderungen, Umzug in ein anderes Land, …), das sind die Abweichungen, die bei der Vererbung in der Unterklasse modelliert werden.
  • Traits sind eher Merkmale, die sich die einzelnen Klassen zulegen. Beispielsweise entscheiden wir selber, ob wir Fußball spielen, Mathematik studiert haben, Turnschuhe tragen oder ähnliches. Die Klasse legt sich also gewisse Eigenschaften und Fähigkeiten zu, man könnte sagen sie kleidet sich ein. Und dabei ist es unrelevant, von welcher Elternklasse sie abstammt. Jede Klasse kann sich mit einem Trait schmücken, auch wenn sie mit einer anderen Klasse, die das Trait verwendet, gar nicht im Zusammenhang steht.

Klingt alles noch sehr theoretisch, nicht? Schauen wir uns das Ganze doch einfach mal an. Was könnte denn ein sinnvolles Merkmal sein, das sich eine Klasse zulegen möchte? Bei Verwendung von Entwurfsmustern (Design Patterns) stößt man öfter auf Funktionalitäten, die Klassen zur Verfügung stellen müssen, um in einem guten Design miteinander zu arbeiten. Da diese Funktionalität eher ein Verhaltensart ist und kein Grundverhalten, das man erbt, empfiehlt es sich hierfür Traits zu nehmen. Ich stelle dies nun am Beobachter- und Singleton-Muster dar:

Das Beobachter-Muster als Trait – Alarmierendes Rot tragen.

Beim Beobachter-Muster geht es darum, dass bestimmte Beobachter-Klassen (Observer Class) eine andere Klasse (Observable Class) im Blick behalten und von ihr über alle Änderungen informiert werden wollen. Hierzu müssen die Beobachter die Informationen bei der beobachteten Klasse abonnieren und wieder abbestellen können (ich verwende hier register() und unregister()).  Weiterhin müssen die Beobachter die Informationen entgegennehmen können (ich verwende hier notify()). Es ergeben sich somit folgende Interfaces für Beobachter und beobachtete Klasse:

observer.php – Teil 1:

<?php
namespace de\thomasworm\common\observer;

interface ObservableInterface {
  public function register(ObserverInterface $observer);
  public function unregister(ObserverInterface $observer);
}

interface ObserverInterface {
  public function notify(ObservableInterface $sender, $data = null);
}

//..

?>

Soweit dürfte das bekannt aussehen. Viele würden jetzt anfangen die Funktionalität von Observable in einer abstrakten Elternklasse zu implementieren und dann nachher davon abzuleiten. Doch nun sind Traits die bessere Wahl:

observer.php – Teil 2:

<?php

//...

trait ObservableTrait {

  /**
   * Array to store observers
*/
  private $_observers = array();

  /**
   * Registers an Observer in Observable
   * @param ObserverInterface $observer Observer to register
   */
  public function register(/** ObserverInterface */ $observer) {
     // Only register if not yet registered
     if (!in_array($observer, $this->_observers, TRUE))
     {
       array_push($this->_observers, $observer);
     }
  }

  /**
  * Unregisters an Observer in Observable
  * @param ObserverInterface $observer Observer to unregister
  */
  public function unregister(/** ObserverInterface */ $observer) {
    $key = array_search($observer, $this->_observers, TRUE);
    // Only unregister if registered
    if ($key !== FALSE) {
      $this->_observers[$key] = null;
      unset($this->_observers[$key]);
    }
  }

  /**
  * Notifies all observer
  * @param mixed $data Optional data to send to observer
  */
  protected function notifyObservers($data = null) {
    foreach ($this->_observers as $observer) {
      $observer->notify($this, $data);
    }
  }
}

?>

Der Trait kümmert sich darum, dass Beobachter abonnieren und abbestellen können und stellt eine intern nutzbare Methode zur Verfügung, mit der alle Beobachter benachrichtigt werden können.

Kleine Anmerkung: ObserverInterface als Type Hint ist bei mir auskommentiert, da es PHP so viel besser gefiel. (Eventuell ein Bug in der von mir verwendeten Version?)

Nun haben wir schon alle Vorbereitungen getroffen, damit wir das Beobachter-Muster anwenden können. Ein kleines Beispiel:

demo.php:

<?php

namespace de\thomasworm\samples\observerdemo;

require('observer.php');
use de\thomasworm\common\observer\ObservableInterface,
    de\thomasworm\common\observer\ObserverInterface,
    de\thomasworm\common\observer\ObservableTrait;

class NewsService implements ObservableInterface {
  use ObservableTrait;
  public function say($text) {
    $this->notifyObservers('Hello World!');
    $this->notifyObservers($text);
  }
}

class MyObserver implements ObserverInterface {
  private $_name = '';
  public function __construct($name) {
    $this->_name = $name;
  }

  public function notify(ObservableInterface $sender, $data = null) {
    echo ($this->_name . ' notified' . (empty($data) ? '' : ': '.$data) . "\n");
  }
}

$news = new NewsService();
$obs1 = new MyObserver('Observer 1');
$obs2 = new MyObserver('Observer 2');

$news->register($obs1);
$news->register($obs2);
$news->say('1 + 2');

$news->unregister($obs1);
$news->say('2');
?>

gibt folgende – korrekte – Ausgabe:

Observer 1 notified: Hello World!
Observer 2 notified: Hello World!
Observer 1 notified: 1 + 2
Observer 2 notified: 1 + 2
Observer 2 notified: Hello World!
Observer 2 notified: 2

Für die beobachtete Klasse hätte man auch nur einen Trait statt Interface und Trait verwenden können, allerdings finde ich es korrekter die Anforderungen an die beobachtete Klasse zu beschreiben (und das macht man eben über ein Interface).

Mehrere Traits verwenden – Kleidung zulegen, die einzigartig macht.

Da Entwurfsmuster so schön sind implementieren wir gleich noch eines: Das Singleton-Muster. Beim Singleton-Muster geht es um einfach instanziierte Klassen, das heißt Klassen, von denen in einem Laufzeitsystem nur eine Instanz existieren darf. Typischerweise bekommt eine solche Klasse die Methode getInstance(), welche genau diese Instanz zurück gibt. Es ergibt sich somit folgendes Interface:

singleton.php – Teil 1:

<?php

namespace de\thomasworm\common\singleton;

interface SingletonInterface {
  public static function getInstance();
}

// ...

?>

Um die Singleton-Implementierung nicht in jeder Klasse einzeln vornehmen zu müssen empfiehlt es sich wieder ein entsprechendes Trait zu implementieren. Was gibt es hierbei zu beachten? Wir wollen, dass nur eine Instanz der Klasse instanziiert wird, das heißt wir müssen den Zugriff auf den Konstruktor sperren, deshalb deklarieren wir diesen als private, so dass er nur noch innerhalb der Klasse selbst erreichbar ist. Auf selbe Weise muss das Klonen der Instanzen unterbunden werden. Die Instanz wird in einer statischen Variable der Klasse gespeichert und getInstance() kümmert sich um die Rückgabe der Instanz bzw. auch deren Erzeugung, falls sie noch nicht existiert. Dieses Verfahren nennt man Lazy Creation, da die Instanz erst bei Bedarf erzeugt wird – PHP darf also bei Programmstart erst einmal faul sein.

singleton.php – Teil 2:

<?php

// ...

trait SingletonTrait {

  /**
   * Stores the only instance of singleton class.
   */
  private static $_instance = NULL;

  private function __construct() {};
  private function __clone() {};

  /**
   * Gets - and if necessary creates - the only instance of singleton class.
   * @return Object The only instance of singleton class.
   */
  public static function getInstance() {
    if (self::$_instance === NULL) {
      self::$_instance = new self;
    }
    return self::$_instance;#
  }

}

?>

Wofür ist das nun sinnvoll? Unsere Klasse NewsService könnte ein zentraler Anlaufpunkt für die Datenverteilung sein. Würde jede Programmkomponente eigene Instanzen von NewsService registrieren, so müssten auch ihre Beobachter sich bei jeder Instanz registrieren. Es empfiehlt sich daher den NewsService zu einem Singleton werden zu lassen, hierzu können wir zu den bereits vorhandenen Interfaces das SingletonInterface und zu den vorhandenen Traits das SingletonTrait hinzufügen:

Auszug demo.php

<?php

// ...

require('singleton.php');
use de\thomasworm\common\singleton\SingletonInterface,
    de\thomasworm\common\singleton\SingletonTrait;

// ...

class NewsService implements ObservableInterface, SingletonInterface {
  use ObservableTrait, SingletonTrait;

  // ...

  }

// ...

?>

Führt man das Programm nun aus erhält man eine entsprechende Fehlermeldung, da vom Singleton keine Instanz mehr per new erzeugt werden darf:

Fatal error: Call to private de\thomasworm\samples\observerdemo\NewsService::__construct() from invalid context in demo.php on line 33

Der Demo-Code muss also entsprechend angepasst werden. Ich habe hier auch noch eine Variable $news2 eingeführt, damit man sieht, dass es wirklich nur eine einzige Instanz des Singleton NewsService gibt:

Auszug aus demo.php:

<?php

// ...

// war: $news = new NewsService();
$news = NewsService::getInstance();

$obs1 = new MyObserver('Observer 1');
$obs2 = new MyObserver('Observer 2');

$news->register($obs1);
$news->register($obs2);
$news->say('1 + 2');

$news2 = NewsService::getInstance();

$news2->unregister($obs1);
$news2->say('2');

?>

Wir erhalten wieder die ursprüngliche Ausgabe.

Trait-Konflikte – Wenn sich die Farbe der Kleidungsstücke beißt.

So wie es Kleidungsstücke gibt, die sich unter einander nicht vertragen, weil die Farbe beißt, gibt es auch Traits, die sich untereinander nicht vertragen, weil sie zum Beispiel gleiche Funktions- oder Variablennamen beinhalten. Auch dies möchte ich noch am Beispiel präsentieren.

Nehmen wir an unsere Implementierung nach dem Observer-Muster soll erweitert werden. Der Beobachter soll sich nun auch merken, welche Klassen er beobachtet, damit es leichter ist – z.B. bei Zerstörung des Beobachters – auch alle Informations-Abonnements aufzuheben. Dazu führen wir einen neuen ObserverTrait ein:

observer.php

<?php

// ...

trait ObserverTrait {

  /**
   * Array to store observables.
   */
  private $_observables = array();

  /**
   * Register me at Observable
   * @param ObservableInterface $observable Observable to register at
   */
  protected function register(/** ObservableInterface */ $observable) {
    if (!in_array($observable, $this->_observables, TRUE)) {
      $observable->register($this);
      array_push($this->_observables, $observable);
    }
  }

  /**
   * Unregister me at Observable
   * @param ObservableInterface $observable Observable to unregister a
   */
  protected function unregister(/** ObservableInterface */ $observable) {
    $key = array_search($observable, $this->_observables, TRUE);
    if ($key !== FALSE) {
      $observable->unregister($this);
      $this->_observables[$key] = null;
      unset($this->_observables[$key]);
    }
  }

  /**
   * Unregister me at all Observables. Should be run when Observer will be destroyed.
   */
  protected function unregisterAll() {
    foreach ($this->_observables as $observable) {
      $observable->unregister($this);
    }
    unset($this->_observables);
  }

}

?>

Und ein kleines neues Beispiel dazu, in dem eine Sender genannte beobachtete Klasse zwei Empfaenger genannte Beobachter erzeugt werden. Die Empfaenger registrieren sich bei Instanziierung über den Konstruktor beim Sender. Für den Fall, dass wir einen Empfaenger zerstören wollen, bieten wir die Funktion prepareDestroy() an, die alle Registrierungen bei beobachteten Klassen aufhebt:

demo2.php

<?php

namespace de\thomasworm\samples\observerdemo;

require('observer.php');
use de\thomasworm\common\observer\ObservableInterface,
    de\thomasworm\common\observer\ObserverInterface,
    de\thomasworm\common\observer\ObservableTrait,
    de\thomasworm\common\observer\ObserverTrait;

class Sender implements ObservableInterface {
  use ObservableTrait;

  public function say($text) {
    $this->notifyObservers($text);
  }
}

class Empfaenger implements ObserverInterface {
  use ObserverTrait;

  private $_name = '';

  public function __construct($name, $observable) {
    $this->_name = $name;
    $this->register($observable);
  }

  public function notify(ObservableInterface $sender, $data = null) {
    echo ($this->_name . ' notified' . (empty($data) ? '' : ': '.$data) . "\n");
  }

  function prepareDestroy() {
    $this->unregisterAll();
  }

  function __destruct() {
    echo ($this->_name . ' destroyed!' . "\n");
  }
}

$s = new Sender();

$e1 = new Empfaenger('Observer 1',$s);
$e2 = new Empfaenger('Observer 2',$s);

$s->say('1 + 2');

$e2->prepareDestroy();
unset($e2);

$s->say('1');

?>

Es erfolgt die gewünschte Ausgabe:

Observer 1 notified: 1 + 2
Observer 2 notified: 1 + 2
Observer 2 destroyed!
Observer 1 notified: 1
Observer 1 destroyed!

Wem sich der Sinn des Traits mit unregisterAll() noch nicht erschließt, der sollte einfach mal den prepareDestroy()-Aufruf aus dem Beispiel löschen. In der Ausgabe wird Observer 2 destroyed! dann erst am Ende zu sehen sein. Warum? PHP zerstört Objekte erst, wenn es keine Referenz mehr auf sie gibt. Da der Sender aber aufgrund der Registrierung noch eine Referenz auf die Empfaenger-Instanz enthält zerstört PHP das Objekt erst zum Programmende. Der Trait stellt für uns also eine Möglichkeit dar die Zerstörbarkeit des Objektes zu bewahren.

Was hat das nun mit Trait-Konflikten zu tun? Bisher nichts, aber jetzt kommt ein schlauer Programmierer, der möchte, dass eine Klasse sowohl Beobachter als auch beobachtet sein kann. Der Sender könnte beispielsweise so abgeändert werden:

Auszug aus demo2.php – verändert:

class Sender implements ObservableInterface, ObserverInterface {
  use ObservableTrait, ObserverTrait;

  public function say($text) {
    $this->notifyObservers($text);
  }

  public function notify(ObservableInterface $sender, $data = null) {
    echo('Habe erhalten... ' . $data . "\n");
  }
}

Mit einem schönen Schmunzeln – wir können uns ja bereits denken was passiert – erhalten wir beim Ausführen eine Fehlermeldung:

Fatal error: Trait method register has not been applied, because there are collisions with other trait methods on de\thomasworm\samples\observerdemo\Sender in demo2.php on line 21

Was ist passiert? Die zwei verwendete Traits implementieren Methoden unter demselben Namen. PHP kann nun nicht einfach entscheiden, welche Methode es nimmt oder welche Methode es verwirft, wir müssen also selbst Hand anlegen und PHP mitteilen, welche Methoden er verwenden oder umbenennen soll:

Auszug aus demo2.php – verändert:

<?php

// ....

use ObservableTrait, ObserverTrait {
  ObservableTrait::register insteadof ObserverTrait;
  ObservableTrait::unregister insteadof ObserverTrait;
  ObserverTrait::register as public registerMe;
  ObserverTrait::unregister as unregisterMe;
}

// ...

?>

Es gibt verschiedene Möglichkeiten Traits beim Einbinden abzuändern:

  • Wahl beim Konflikt:
    Bindet man zwei Traits ein, die Methoden mit demselben Namen besitzen, so muss explizit mitgeteilt werden, dass eine Traitmethode an Stelle der anderen verwendet werden soll. Hierfür gibt es das Schlüsselwort insteadof. Erst wenn alle Konflikte auf diese Weise aufgelöst wurden, lassen sich konkurrierende Traits einbinden. Im Beispiel habe ich mich für die Verwendung der ObservableTrait-Methoden unter Originalnamen entschieden, da sich sonst weitere Probleme ergeben würden (Die Observer erwarten ja, dass die beobachtete Klasse diese Methoden entsprechend anbietet, es besteht also Abhängigkeit. Auf die Methoden aus ObserverTrait besteht dagegen keine Abhängigkeit.)
  • Vergeben eines Alias:
    Methoden aus Traits können einen weiteren Alias erhalten. Über diesen Alias kann man auf die Methoden zugreifen, wenn unter dem eigentlichen Methodennamen die Methode eines anderen Traits verwendet wurde. Es handelt sich hierbei wirklich nur um einen Alias, durch das Umbenennen wird der alte Methodennamen nicht aufgegeben, d.h. eine Methode – ohne Konflikt zu anderen Traits – die einen Alias erhält, ist trotzdem unter ihrem ursprünglichen Methodennamen aufrufbar. Der Alias wird über das Schlüsselwort as zugewiesen. Im Beispiel wurden die Methoden aus ObserverTrait mit einem Alias versehen, um Zugriff hierauf zu erhalten (ihr Originalname wurde ja mit den Methoden aus ObservableTrait überlagert).
  • Verändern der Sichtbarkeit:
    Die Sichtbarkeit von Methoden, die durch Traits vorgegeben wird, lässt sich ebenfalls über as abändern. Dies kann in Kombination mit einer Aliasvergabe erfolgen, muss aber nicht. Um auf registerMe() auch außerhalb der Klasse zugreifen zu können wurde im Beispiel die Sichtbarkeit auf public geändert.

Nachdem nun also die Trait-Einbindung abgeändert ist lässt sich der Sender selber als sein eigener Beobachter registrieren:

Auszug demo2.php – verändert:

<?php

// ...

$s = new Sender();
$s->registerMe($s);

// ...

?>

Die Ausgabe ist korrekt:

Habe erhalten... 1 + 2
Observer 1 notified: 1 + 2
Observer 2 notified: 1 + 2
Observer 2 destroyed!
Habe erhalten... 1
Observer 1 notified: 1
Observer 1 destroyed!

Schlussbemerkungen und Fazit

Es gäbe hier noch einiges Mehr zu erwähnen wie zum Beispiel Trait-Einbindung in Traits, was aber den Rahmen sprengen würde. Die Grundlagen sind dargestellt und es zeigt sich, dass Traits ein sehr mächtiges Mittel sind. Trotz der Vorteile, die sie bieten – meiner Meinung nach besonders im Umfeld mit Entwicklungsmustern – sollte man sie wohl bedacht einsetzen. Eine falsche Verwendung wird mit schlecht wartbaren Code bestraft. Getreu dem alten Motto “Komposition vor Vererbung” gilt auch “Komposition vor Traits”.

Oder anders gesprochen im bildlichen Vergleich: Entsprechender Charme hat beim Flirten mehr Wirkung als die Kleidung, die man an hat. In diesem Sinne: Viel Spaß beim Bepacken des PHP-Kleiderschrankes für Klassen ;-)

Ähnliche Artikel:

  1. PHP 5.3 Feature: Late static binding (LSB)
Über den Autor

PHP Gangsta

Der zweitgrößte deutsche, eher praxisorientierte PHP-Blog von Michael Kliewe veröffentlicht seit Mitte 2009 Artikel für Fortgeschrittene.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen