Imperative vs. deklarative Programmierung

Was ist der Unterschied zwischen imperativer und deklarativer Programmierweise? Wir betrachten im Folgenden die Hintergründe und erläutern die Unterschiede mit Beispielen.

Imperativ vs Deklarativ

Wenn wir die Lage soweit vereinfachen, dass es nur zwei unterschiedliche Arten und Weisen gibt, Quellcode zu verfassen – eine imperative und eine deklarative – kommen wir zu folgender Situation. Diese beiden Herangehensweisen basieren gewissermaßen auf unterschiedlichen philosophischen Betrachtungsweisen die sich wie folgt voneinander unterscheiden:

Imperative Programmierung: Es geht darum, der “Maschine” einen genauen Ablauf zu vermitteln, mit dem ein gewünschtes Ergebnis erreicht werden soll.
: Hier geht es darum, der “Maschine” zu vermitteln, was man erreichen möchte, und den Computer herausfinden zu lassen, wie das im einzelnen am besten zu bewerkstelligen ist.

Beispiele für imperativen und deklarativen Code

Nehmen wir ein einfaches Beispiel. Sagen wir, wir möchten alle Zahlen in einem Array verdoppeln.

Imperativer Ansatz

Wir könnten dies in einem imperativen Stil wie folgt tun:

var zahlen = [1,2,3,4,5]
var verdoppelt = []

for(var i = 0; i < zahlen.length; i++) {
var neueZahl = zahlen[i] * 2
verdoppelt.push(neueZahl)
}
console.log(verdoppelt) //ausgegeben wird: [2,4,6,8,10]

Wir iterieren explizit über die Länge des Arrays “zahlen”, ziehen uns jedes Element aus dem Array heraus, verdoppeln es und fügen den doppelten Wert dem neuen Array “verdoppelt” hinzu, wobei das verdoppelte Array bei jedem Schritt erweitert wird, solange bis wir fertig sind.

Deklarativer Ansatz

Ein eher deklarativer Ansatz würde die Array.map-Funktion verwenden und könnte wie folgt aussehen:

var zahlen = [1,2,3,4,5]

var verdoppelt = zahlen.map(function(n) {
return n * 2
})
console.log(verdoppelt) // ausgegeben wird: [2,4,6,8,10]

map() generiert ein neues Array aus einem vorhandenen Array, wobei jedes Element im neuen Array erstellt wird, indem die Elemente des ursprünglichen Arrays an die Funktion weitergeleitet werden, die an map übergeben wurde – In unserem Fall function (n) {return n * 2}.
Die Map-Funktion abstrahiert den Prozess des expliziten Iterierens über das Array und wir können uns auf das konzentrieren, was wir erreichen wollen. Beachte, dass die Funktion, die wir übergeben, keinen externen Zustand verändert(Call by Value). In unserem Fall nimmt sie lediglich eine Zahl und gibt diese Zahl verdoppelt zurück.
Es gibt andere gebräuchliche deklarative Abstraktionen für Listen, die in Sprachen mit einer vorwiegend funktionalen Ausrichtung verfügbar sind. Um beispielsweise alle Elemente in einer Liste imperativ zu addieren, könnten wir Folgendes tun:

var zahlen = [1,2,3,4,5]
var summe = 0

for(var i = 0; i < zahlen.length; i++) {
summe += zahlen[i]
}
console.log(summe) //Ausgabe ist 15
Oder wir könnten es auf eine deklarative Art und Weise tun, und die Reduce-Funktion verwenden:
var zahlen = [1,2,3,4,5]

var ergebnis = zahlen.reduce(function(summe, n) {
return summe + n
}, 0);
console.log(ergebnis) //Ausgabe ist 15

Die Funktion reduce() bricht eine Liste unter Verwendung der übergebenen Funktion auf einen einzelnen Wert herunter. Sie adaptiert die übergebene Funktion und wendet sie auf alle Elemente im Array an. Bei jedem Aufruf ist das erste Argument (in diesem Fall summe) das Ergebnis des Aufrufs der Funktion für das vorherige Element und das zweite (n) ist das aktuelle Element. Im vorliegenden Fall fügen wir also für jedes Element n zu summe hinzu und geben bei jedem Schritt das Ergebnis zurück, so dass wir am Ende das aufsummierte Array erhalten.

Auch in diesem Fall abstrahiert reduce() vom WIE und kümmert sich sowohl um den Iterationsprozess als auch um die Verwaltung seiner Zustände. Für uns als Entwickler existiert eine allgemeine, generische Möglichkeit, eine Liste in einem einzelnen Wert zusammenzufassen. Alles, was wir tun müssen, ist anzugeben, WAS wir erwarten.

Falls Du mit Funktionen wie map() oder reduce() nicht bereits vertraut bist kann sich das zunächst merkwürdig anfühlen. Als Programmierer sind wir unter Umständen sehr daran gewöhnt genau zu spezifizieren, wie Dinge passieren sollten. “Iteriere über diese Liste”, “Wenn das dann”, “aktualisiere diese Variable mit diesem neuen Wert”. Warum solltest Du also diese etwas bizarr anmutende Abstraktion lernen müssen, wenn Du bereits weißt, wie man der Maschine sagt, wie genau sie etwas zu tun hat?
In vielen Situationen ist imperativer Code absolut in Ordnung. Wenn wir Geschäftslogik schreiben, müssen wir meistens Imperativcode schreiben, da es selten eine adäquate generischere Abstraktion über unsere Geschäftsdomäne geben wird.
Aber wenn wir uns die Zeit nehmen, deklarative Abstraktionen zu lernen (oder gegebenenfalls selbst zu bauen!), können wir effiziente und mächtige Abkürzungen nehmen, wenn wir Code schreiben. Zum einen kommen wir dann normalerweise mit weniger Zeilen aus, was der Übersichtlichkeit und der Entwicklungsgeschwindigkeit dienlich sein sollte. Aber wir denken und operieren auch auf einer höheren Ebene, in der Sphäre von dem, was wir wollen, und nicht im Nebel dessen, wie es konkret passieren soll – In vielen Fällen ermöglicht das eine fokusiertere Arbeitsweise.

Beispiel SQL

Du hast es vielleicht noch nicht realisert, aber vermutlich hast Du ganz klassische deklarative Abstraktionen bereits effektiv benutzt, zum Beispiel bei der Formulierung von SQL-Statements. SQL kann man sich als deklarative Abfragesprache zum Arbeiten mit Datensätzen vorstellen. Würde man eine komplette Anwendung in SQL schreiben? Wahrscheinlich nicht. Aber für das Arbeiten mit Datensätzen ist diese Sprache unglaublich leistungsfähig.
Betrachte eine Abfrage wie:

SELECT * from hunde
INNER JOIN besitzer
WHERE hunde.besitzer_id = besitzer.id

Und nun stell dir vor, wie du mit einer imperativen Vorgehensweise zu diesem Ergebnis gelangen würdest
//alleHunde = [{name: 'Fifi', besitzer_id: 1}, {...}, ... ]
//alleBesitzer = [{id: 1, name: 'Alex'}, {...}, ...]

var hundeMitBesitzern = []
var hund, besitzer

for(var hi=0; hi < alleHunde.length; hi++) {
hund = alleHunde[di]

for(var bi=0; bi < alleBesitzer.length; bi++) {
besitzer = alleBesitzer[bi]
if (besitzer && hund.besitzer_id == besitzer.id) {
hundeMitBesitzern.push({
hund: hund,
besitzer: besitzer
})
}
}}
}

Nicht sonderlich schön!
Ich sage nicht, dass SQL immer leicht zu verstehen ist, oder notwendigerweise schlüssig, wenn man es zum ersten Mal sieht, aber es ist in jedem Fall viel klarer als dieses Durcheinander. SQL ist jedoch nicht nur kompakter und einfacher zu lesen, sondern bietet uns viele weitere Vorteile indem wir uns auf das gewünschte Ergebnis konzentrieren können und die Datenbank bzw. das Datenbankmanagement-System (DBMS) das WIE für uns optimieren lassen. Wenn wir es tatsächlich produktiv einsetzen würden, wäre unser imperatives Beispiel langsam, weil wir für jeden Hund in der Liste über die vollständige Besitzerliste iterieren müssten.
Im SQL-Beispiel können wir die Datenbank anweisen sich damit zu befassen, wie man die richtigen Ergebnisse erhält. Wenn es sinnvoll ist, einen Index zu verwenden (vorausgesetzt, wir haben einen eingerichtet), kann die Datenbank dies tun, was zu einem großen Leistungsgewinn führt. Wenn es vor einer Sekunde genau die gleiche Abfrage schon einmal durchgeführt hat, kann das Resultat fast augenblicklich aus einem Cache kommen.

Fazit

Deklarative Programmierung ermöglicht es uns, zu beschreiben, was wir wollen. Eine zugrunde liegende Software entscheidet, wie genau dies passieren soll. In vielen Bereichen kann diese Herangehensweise zu echten Verbesserungen bei der Code-Entwicklung führen, nicht nur in Bezug auf die Anzahl der Codezeilen oder (potentielle) Performance-Vorteile, sondern durch das Schreiben von Code auf einer höheren Abstraktionsebene. So konzentrieren wir uns viel mehr auf das, was wir wollen und das ist manchmal alles, was uns als Problemlöser wirklich interessieren sollte.

Manchmal ist es allerdings trotzdem in Ordnung, sich an das Wie zu halten. Wenn wir Quellcode auf hohe Leistung trimmen müssen, werden wir kaum umhin kommen, das genauer zu spezifizieren. Oder für die Geschäftslogik, wo es oftmals nichts gibt, was eine generische deklarative Bibliothek abstrahieren könnte, schreiben wir Imperativcode.

Rückmeldungen