Closures in PHP 5.3

Sunday, 03 August 2008, 11:58 von Blackflash

Seit PHP 5.3 (derzeit als Alpha verfügbar) gibt es zwei neue und stark korrelierende Features: Closures und Lambda-Funktionen. Diese möchte ich mit diesem Artikel eingehend erläutern und mit einem praktisch, wenn auch sehr allgemein gehaltenem, Beispiel erläutern.
Beginnen wir mit den Begriffsdefinitionen:

  • Lambda-Funktionen sind anonyme Funktionen (besitzen also keinen Namen), die als Callback verwendet und in Variablen gespeichert werden können.
  • Closures sind Programmteile (hier: Lambda-Funktionen), die gewisse Variablen eines Sichtbarkeitsbereiches konservieren.

Lambda-Funktionen scheinen auf dem erstem Blick syntaktischer Zucker für die Funktion create_function zu sein, es gibt aber bedeutende Unterschiede. Zum einen hat jede Funktion, die mittels create_function definiert wurde, einen Namen und ist somit global verfügbar. Auch pseudozufällige Funktionen beheben dieses Manko nicht, da man mit get_defined_functions an den Namen gelangen könnte. Zum anderen stellen Lambda-Funktionen einen eigenen Datentyp da (bei Type-Hints ist der Datentyp Closure anzugeben).
Um zu verstehen, was Closures bedeuten, muss man wissen, was mit den Sichtbarkeitsbereichen gemeint ist, dazu ein instruierendes Beispiel: Der Sichtbarkeitsbereich einer Funktion umfasst alle Variablen, die per Parameter übergeben wurden oder global verfügbar sind bzw. verfügbar gemacht werden können (Superglobale, global-Schlüsselwort, Variablen statischer Klassen, ...). Closures bieten nun die Möglichkeit, Variablen aus dem aktuellen Sichtbarkeitsbereich in der Closure zu konservieren, d.h. gewisse Variablen aus dem aktuellen Sichtbarkeitsbereich sind weiterhin verfügbar für die Closure, auch wenn die Closure den Sichtbarkeitsbereich, in dem sie definiert wurde, bereits verlassen hat. Um zu bestimmen, welche Variablen konserviert werden sollen, wird das Schlüsselwort use genutzt.

Mein Beispiel bezieht sich auf den Einsatz des Kommando-Entwurfsmusters. Es ist dabei nicht zwingend notwendig, dieses Entwurfsmuster bereits zu kennen, da die Implementierung verständlich ist. Zuerst werden die Teilnehmer des Entwurfsmusters erläutert:

  • Aufrufer: Führt die Kommandos aus.
  • Empfänger: Auf ihm werden die Kommandos ggf. ausgeführt.
  • Klient: Erzeugt Kommandos, verknüpft sie ggf. mit einem Empfänger und leitet sie zu einem Aufrufer weiter.
  • Kommando: Stellt eine (gekapselte) Funktionalität zur Verfügung, ggf. unter Nutzung eines bestimmten Empfängers.

Als Klient dient eine Fabrikmethode. Der Aufrufer wird die Kommandos, die er vom Klienten erhält, sofort ausführen und der Empfänger ist ein Objekt, das nur einige Eigenschaft enthält, die verändert werden.
Wir entwerfen für die Kommandos eine abstrakte Klasse Command mit den wesentlichsten Methoden:

abstract class Command { private $receiver; public function getReceiver () { return $this->receiver; } public function setReceiver ($receiver) { $this->receiver = $receiver; return $this; } abstract public function execute (); }

Die Methoden dürften selbsterklärend sein. Nun noch eine minimalistische Implementierung für den Aufrufer:

$client = new Client($receiver); foreach(new CommandIterator() as $command) { list($name, $args) = $command; $client->getCommand($name, $args)->execute(); }

Mittels des CommandIterators ist es dem Aufrufer möglich, den nächsten Befehl zu holen. Diese Befehle bestehen aus einem zwei-elementigen Arrays, die als Argumente für den Klienten dienen. Die Variable $receiver enthält den Empfänger. Anschließend werden die Kommandos erstellt und ausgeführt. Nun fehlt noch der letzte Teilnehmer: Der Klient.

class Client { private $receiver; public function __construct ($receiver) { $this->receiver = $receiver; } private function createCommand ($name, $args) { switch($name) { default: return NopCommand(); } } public function getCommand ($name, $args) { return $this->createCommand()->setReceiver($this->receiver); } }

NopCommand ist dabei ein leeres Kommando (d.h. die execute-Methode ist zwar "implementiert", enthält aber keinen Code). Die private Methode createCommand ist zum Erzeugen der Kommandos gedacht und enthält einen switch um für jeden Namen eines Kommandos eine möglicherweise einzigartige Instanzierung vornehmen zu können, da nicht jedes Kommando zwingend gleichartig instanziert werden kann. Um jetzt Kommandos hinzuzufügen muss man für jedes Kommando eine eigene Klasse hinzufügen. Die Erfahrung zeigt, dass es viele Kommandos gibt, die nur wenige Zeilen Code enthalten. Gibt es eine Möglichkeit dieses Problem zu beseitigen? Ja, und ich werde es mit Hilfe von Closures aufzeigen. Zuerst brauchen wir eine Klasse, welche die Kommando-Klasse erweitert und die Closure ausführt.

class ClosureCommand extends Command { private $closure; public function __construct (Closure $closure) { $this->closure = $closure; } public function execute () { $closure = $this->closure; $closure($this->getReceiver()); } }

Jede Closure erwartet also einen Empfänger als Argument, aber wo sind die Argumente? Die werden bei Definition der Closure bereits festgelegt, der Vorteil daran ist, dass kein Zugriff auf Variablen gewährt wird, die nicht zwingend notwendig sind: Ein Grundprinzip der Objektorintierten Programmierung.
Nun schauen wir uns einfach mal zwei triviale Beispiele an (Ausgaben des Namens und Setzen des Namens):

switch($name) { // ... case 'setName': $name = $args[0]; $closure = static function ($receiver) use ($name) { $receiver->name = $name; }; return new ClosureCommand($closure); case 'echoName': $closure = static function ($receiver) { echo $receiver->name; }; return new ClosureCommand($closure); // ... }

Weshalb static function? Nur mit function wäre $this in der Closure verfügbar, was zwar meistens nur einen ideellen Unterschied ausmacht, aber eben objektorientiert die bessere Variante ist. Am Verhalten ändert es jedenfalls nichts.
An dem Beispiel sieht man sehr schön, wie sich Closures rentieren können, aber sollte man den gesamten Code bei der Erzeugung implementieren? Meines Erachtens ist es nur sinnvoll, wenn es sich um wenig Code handelt und es zudem nicht allzu viele solche Codestücke gibt. Häufig ist es so, dass etliche Kommandos existieren, die sich inhaltlich sehr nahe stehen. Dazu ist eine Klasse denkbar, welche die Closures zurückgeben, die in den Kommando-Objekten eingesetzt werden.

class ReceiverChanger { private $counter = 0; public function echoChanges ($args) { return function ($receiver) { echo $this->counter; }; } public function setName ($args) { $name = $args[0]; return function ($receiver) use ($name) { $receiver->name = $name; $this->counter++; }; } public function echoName ($args) { return function ($receiver) { echo $receiver->name; }; } }

Aktualisiert sieht die createCommand folgendermaßen aus:

private function createCommand ($name, $args) { switch($name) { // ... case 'setName': case 'echoName': case 'echoChanges': return new ClosureCommand($this->receiverChanger->$name($args)); // ... } }

Es wird angenommen, dass $this->receiverChanger eine Instanz der Klasse ReceiverChanger ist.
Alternativ ist auch eine Variante möglich bei der die Methoden keine Closures zurückgeben, sondern nur die gewünschte Funktionalität enthalten.

class ReceiverChanger { private $counter = 0; public function getClosure ($name, $args) { return function ($receiver) use ($name, $args) { $this->$name($receiver, $args); }; } public function echoChanges ($receiver, $args) { echo $this->counter; } public function setName ($receiver, $args) { $receiver->name = $args[0]; $this->counter++; } public function echoName ($receiver, $args) { echo $args[0]; } }

Der zugehörige Ausschnitt der createCommand sieht folgendermaßen aus:

private function createCommand ($name, $args) { switch($name) { // ... case 'setName': case 'echoName': case 'echoChanges': return new ClosureCommand($this->receiverChanger->getClosure($name, $args)); // ... } }

Ich hoffe, ich konnte die Aussage plausibel machen, dass Closures nützlich und praktikabel sind. Natürlich sind analoge Lösungen ohne Closures möglich, aber es ist deutlich komplizierter und genau darauf kommt es an: Die Closures machen einem das (Programmierer)Leben einfacher.

Kommentare


Kommentiere!

Your Name:


Your Email:


Your URL:


Spam Prevention:
Enter the text above into the box below.
If you are unable to read it, refresh the page.


Your Comment: