Closures in PHP 5.3
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.