Closures sind Funktionen mit Zustand

Eine Closure ist ein Konzept aus Programmiersprachen, das eine Funktion mit einer Referenz auf eine Umgebung zum Erzeugungszeitpunkt der Funktion kombiniert – oder einfach gesagt: eine Funktion kombiniert mit etwas Zustand. Aber etwas Ähnliches haben wir doch schon gehört, wenn über Objekte in Programmiersprachen gesprochen wird, oder? Ein Objekt ist Zustand kombiniert mit Funktionen. Sind die beiden also austauschbar? Nun, bis zu einem gewissen Grad würde ich sagen: ja. Es gibt Anwendungsfälle, in denen Closures und Objekte austauschbar verwendet werden können, und einen davon sehen wir uns nun an.

Nehmen wir ein einfaches Beispiel. Wir haben eine einfache Liste von Mitarbeitern samt ihrem Gehalt:


$list = array(
    array('name' => 'Blake', 'salary' => 35000),
    array('name' => 'Sakic', 'salary' => 75000),
    // ...
);

Die Aufgabe besteht darin, einen Mechanismus bereitzustellen, um die Liste nach einem konfigurierbaren Kriterium auf der Spalte salary zu filtern. Mit einem objektorientierten Ansatz könnte man so beginnen:


interface SalaryFilter {
     public function filter(array $employee);
}

class SalaryLessThanFilter implements SalaryFilter {
    private $limit;
    public function __construct($limit) {
        $this->limit = (int)$limit;
    }
    public function filter(array $employee) {
        return $employee['salary'] < $this->limit;
    }
}

class SalaryMoreThanFilter implements SalaryFilter {
    private $limit;
    public function __construct($limit) {
        $this->limit = (int)$limit;
    }
    public function filter(array $employee) {
        return $employee['salary'] > $this->limit;
    }
}

class SalaryBetweenFilter implements SalaryFilter {
    private $limitA;
    private $limitB;
    public function __construct($limitA, $limitB) {
        $this->limitA = (int)$limitA;
        $this->limitB = (int)$limitB;
    }
    public function filter(array $employee) {
        return $employee['salary'] > $this->limitA
               && $employee['salary'] < $this->limitB;
    }
}

Eine einfache generische Filterfunktion (ich weiß, ich weiß – ich sagte objektorientierter Ansatz, aber das zwingt uns ja nicht dazu, komplett auf reguläre Funktionen zu verzichten) erlaubt es uns, das Array mit jedem der verfügbaren Filter zu filtern.


function filter(array $list, SalaryFilter $filter) {
    return array_filter($list, array($filter, 'filter'));
}

// for example
print_r(filter($list, new SalaryLessThanFilter(70000)));
print_r(filter($list, new SalaryMoreThanFilter(70000)));
print_r(filter($list, new SalaryBetweenFilter(30000, 80000)));

Wir haben damit eine sehr einfache Implementierung des sogenannten Strategy Patterns gebaut – die Möglichkeit, einen Algorithmus zur Laufzeit auszutauschen. Das ist sicher nicht perfekt, aber es ist ein Beispiel, das einfach genug ist, um die Ähnlichkeiten zwischen Closures und Objekten zu zeigen.

Der „Closure“-Ansatz hingegen könnte so aussehen:


function salaryLessThan($limit) {
    return function(array $employee) use ($limit) {
        return $employee['salary'] < $limit;
    };
}

function salaryMoreThan($limit) {
    return function(array $employee) use ($limit) {
        return $employee['salary'] > $limit;
    };
}

function salaryBetween($limitA, $limitB) {
    return function(array $employee) use ($limitA, $limitB) {
        return $employee['salary'] > $limitA
               && $employee['salary'] < $limitB;
    };
}

Das ist deutlich weniger Code, als der ursprüngliche objektorientierte Ansatz benötigt, und die Filterfunktion ist sogar noch einfacher:


function filter(array $list, callable $filter) {
    return array_filter($list, $filter);
}

print_r(filter($list, salaryLessThan(70000)));
print_r(filter($list, salaryMoreThan(70000)));
print_r(filter($list, salaryBetween(30000, 80000)));

Auch das Erweitern dieser einfachen Liste von Algorithmen ist nun viel einfacher, weil wir nicht mehr auf Klassen beschränkt sind, die das SalaryFilter-Interface implementieren. Wir können jedes beliebige callable verwenden.

Übrigens können wir sogar unsere ursprüngliche Implementierung mit dem neuen Ansatz kompatibel machen – mit der magischen Methode __invoke(), die PHP bereitstellt.


interface SalaryFilter {
     public function filter(array $employee);
     public function __invoke(array $employee);
}

class SalaryLessThanFilter implements SalaryFilter {
    //...
    public function __invoke(array $employee) {
        return $this->filter($employee);
    }
}

class SalaryMoreThanFilter implements SalaryFilter {
    //...
    public function __invoke(array $employee) {
        return $this->filter($employee);
    }
}

class SalaryBetweenFilter implements SalaryFilter {
    //...
    public function __invoke(array $employee) {
        return $this->filter($employee);
    }
}

print_r(filter($list, new SalaryLessThanFilter(70000)));
print_r(filter($list, new SalaryMoreThanFilter(70000)));
print_r(filter($list, new SalaryBetweenFilter(30000, 80000)));

Fassen wir kurz zusammen, was wir bisher gesehen haben. Ausgehend von einem „traditionellen“ objektorientierten Ansatz haben wir Klassen und Objekte in einem komplett funktionalen Ansatz durch Closures ersetzt, das übergeordnete Konzept des Strategy Patterns aber intakt gelassen. Unsere Closures tragen einfach ihren „Zustand“ (das Filterlimit bzw. die Filterlimits) und den Algorithmus (die Vergleichslogik) mit sich, um in unserem Code herumgereicht zu werden. Im Grunde genauso, wie Objekte, die aus unseren Filterklassen erzeugt wurden, ihren Algorithmus (die Klassenmethode filter) und ihren Zustand (die Eigenschaft limit) mit sich herumtragen. Wir können nun sagen, dass in diesem Beispiel Objekte und Closures austauschbar sind und dass Closures in unserem Fall weit weniger Code benötigen als der traditionelle Ansatz.

Deshalb können Closures bei einfachem, Strategy-Pattern-artigem Code, der konfigurierbare und austauschbare Algorithmen erfordert, wirklich sinnvoll sein. Anwendungsfälle könnten Sortieren, Filtern, Berechnungen usw. sein. Verwenden Sie einfach immer das richtige Werkzeug für die jeweilige Aufgabe – objektorientierte und funktionale Ansätze passen, wie ich finde, ziemlich gut zusammen.

Made with in Stuttgart, Germany