Traits in PHP 5.4.0 - Part 2: Observer Pattern

05.03.2012

Das Observer-Pattern ist wohlbekannt. Dabei bietet ein Objekt über eine Schnittstelle an, Beobachter zu registrieren, die über Veränderungen an dem Objekt informiert werden.

In Java gibt es dafür die Klasse java.util.Observable, von der zu beobachtende Objekte erben können. Leicht ersichtlich, dass hier das Problem auftaucht, dass eine Klasse nicht gleichzeitig von Observable und einer zweiten Klasse erben kann.

Es scheint in PHP keine Standard-Implementierung zu geben, sondern nur das Interface SplSubject. Zu beobachtende Klassen können dieses Interface implementieren.

Klar ist, dass zwei Klassen, die SplSubject unabhängig voneinander implementieren, wohl eine erhebliche Code-Duplikation aufweisen, denn beide würden eine Liste von Observern halten, das Hinzufügen und Entfernen von Beobachtern implementieren und bei Änderungen alle registrierten Beobachter informieren. Das Mittel der Wahl, um diese Duplikation zu verhindern oder zumindest einzuschränken, war Vererbung oder, wenn die Klasse bereits von einer anderen Klasse erbte, Delegation.

Eine beispielhafte, einfache Implementierung von SplSubject:

class SimpleSplSubject{
	private $observers = [];
	private $obj;

	public function __construct($object){
		$this->obj = $object;}

	public function attach(SplObserver $observer){
		foreach($this->observers as $comparedObserver){
			if ($comparedObserver == $observer){
				return;}}
		$this->observers[] = $observer;}

	public function detach(SplObserver $observer){
		$index = array_search($observer, $this->observers, true);
		if ($index !== FALSE){
			unset($observers[$index]);}}

	public function notify(){
		foreach($this->observers as $observer){
			$observer->update($this->obj);}}}

Eine Klasse, die diese Implementierung via Delegation nutzt, sähe z.B. wie folgt aus:

class IntValue implements SplSubject{
	private $value = 0;
	private $subject;

	public function __construct(){
		$this->subject = new SimpleSplSubject($this);}

	public function get(){
		return $this->value;}

	public function plus($value){
		$this->set($this->value + $value);}

	public function minus($value){
		$this->set($this->value - $value);}

	private function set($value){
		$this->value = $value;
		$this->notify();}

	public function attach(SplObserver $observer){
		$this->subject->attach($observer);}

	public function detach(SplObserver $observer){
		$this->subject->detach($observer);}
	
	public function notify(){
		$this->subject->notify();}}

Die Implementierung der SplSubject-Schnittstelle mittels Delegation erfordert also einiges an Aufwand für jede Klasse, für die das geschehen soll – eine Instanzvariable, die mit dem Ziel der Delegation befüllt wird, sowie die Methoden attach, detach und notify, die ihre Aufrufe einfach nur weiterreichen.

Wesentlich weniger aufwendig ist die Variante mit einem Trait, der statt der Klasse die nötigen Methoden implementiert:

trait SimpleSplSubject{
	private $observers = [];

	public function attach(SplObserver $observer){
		foreach($this->observers as $comparedObserver){
			if ($comparedObserver == $observer){
				return;}}
		$this->observers[] = $observer;}

	public function detach(SplObserver $observer){
		$index = array_search($observer, $this->observers, true);
		if ($index !== FALSE){
			unset($observers[$index]);}}

	public function notify(){
		foreach($this->observers as $observer){
			$observer->update($this);}}}

Die Klasse, die diesen Trait nutzt, kommt ohne Delegation aus:

class IntValue implements SplSubject{
	use SimpleSplSubject;

	private $value = 0;

	public function get(){
		return $this->value;}

	public function plus($value){
		$this->set($this->value + $value);}

	public function minus($value){
		$this->set($this->value - $value);}

	private function set($value){
		$this->value = $value;
		$this->notify();}}

Fazit: Traits sind hervorragend geeignet, um Nebenaspekte einer Klasse, die man ansonsten über Delegation und somit einiges an Boilerplate-Code implementieren würde, direkt zu integrieren.

Der Code für die Implementierung als Trait zusammen mit einem Beispiel-Observer und einem Test-Skript als Archivdatei: spl_observer_pattern.tar.bz2