Software vorausschauend entwickeln

Seite 4: Deklarativ und simpel

Inhaltsverzeichnis

Das alles erinnert stark an die sogenannte deklarative Programmierung, in der Entwickler das gewĂĽnschte Ergebnis formulieren statt den Weg dortin, wie es bei einem imperativen Ansatz der Fall ist. Daher haben sich fĂĽr deklarative und imperative Programmierung die Bezeichnungen Was- und Wie-Programmierung eingebĂĽrgert.

Selbst auf dem Niveau einer einzelnen Schleife lässt sich der Unterschied zwischen beiden Herangehensweisen hervorragend veranschaulichen. Ist beispielsweise für eine Liste von Zahlen die Summe der zugehörigen Quadratzahlen zu berechnen, lässt sich das deklarativ elegant durch den Einsatz von map und reduce erledigen, indem lediglich das gewünschte Ergebnis beschrieben wird:

const sum = [ 1, 2, 3, 4, 5 ].
map(n => n * n).
reduce((sum, n) => sum + n);

Die imperative Implementierung wirkt hingegen aufwendig, da beispielsweise anzugeben ist, wie man durch das Array der Zahlen iterieren möchte:

const numbers = [ 1, 2, 3, 4, 5 ];

let sum = 0;

for (let i = 0; i < numbers.length; i++) {
const square = numbers[i] * numbers[i];

sum += square;
}

Der nun längere Code enthält Details, die für die eigentliche Aufgabe nicht relevant sind. So ist beispielsweise die Existenz der Zählvariablen i der Tatsache geschuldet, dass eine for-Schleife eine Laufvariable braucht. In der eigentlichen Anforderung kommt i hingegen nicht vor, es handelt sich folglich um ein technisches Artefakt, das durchscheint, weil das Abstraktionsniveau des Codes aus fachlicher Sicht nicht hoch genug ist.

Außerdem ergibt sich bei der deklarativen Variante ein angenehmer Nebeneffekt: Da die Implementierung nicht vorgibt, wie die Liste der Zahlen zu durchlaufen ist, könnte die Laufzeitumgebung entscheiden, das in einer anderen Reihenfolge zu erledigen als in der imperativen Version, beispielsweise rückwärts oder durcheinander. Das kann sinnvoll sein, wenn das Iterieren rückwärts durch eine Schleife performanter ist als vorwärts. Mit der gleichen Begründung wäre sogar denkbar, dass die Laufzeitumgebung die Ausführung von map und reduce parallelisiert, was für die for-Schleife hingegen zumindest nicht ohne aufwendige Analyse des Codes und gegebenenfalls Unterstützung durch den Entwickler möglich ist. Programmierer Joel Spolsky beschreibt diesen Effekt in seinem Blogeintrag "Can Your Programming Language Do This?".

Letztlich läuft alles auf die Forderung hinaus, Komplexität zu vermeiden und simplen Code zu schreiben. Zwar fordert das auch das weit verbreitete KISS-Prinzip, das besagt, dass Aufgaben durch eine möglichst einfache Lösung zu erfüllen sind. Allerdings stellt sich in dem Zusammenhang die Frage, was der Begriff "einfach" genau meint. Im Englischen lässt sich etwas besser zwischen simple und easy unterscheiden, im Deutschen werden die Wörter simpel und einfach allerdings häufig gleichgesetzt.

Das führt zu der (falschen) Annahme, dass triviale Lösungen, die man ohne großes Nachdenken erreichen kann, stets vorzuziehen seien. Mit dem Argument retten sich Programmierer häufig, wenn es darum geht, vermeintlich exotische Sprachkonstrukte wie Lambdaausdrücke oder Entwurfsmuster zu meiden. Oftmals gipfelt das beispielsweise in Code, der aus endlosen if-else-Kaskaden besteht, statt das wesentlich elegantere und pflegeleichtere Strategy-Entwurfsmuster zu nutzen.

Um dem Problem aus dem Weg zu gehen, lassen sich aber vier konkretere Forderungen stellen, die greifbarer als das KISS-Prinzip sind, und das sogenannte CUTE-Prinzip ausmachen:


  • Comprehensive: Code sollte verständlich im Hinblick auf die eigentliche Problemstellung und die Domäne sein und möglichst wenig technische Konstrukte enthalten, die lediglich wegen einer fehlenden Abstraktion erforderlich sind. Zudem sind die fachlichen Konzepte explizit auszudrĂĽcken, um die Intention einzufangen und zu bewahren. Die Implementierung rĂĽckt dadurch in den Hintergrund, sodass sich aus dem Code besser ergibt, warum er geschrieben wurde und welchen Zweck er verfolgt.
  • Unidimensional: Code sollte eine niedrige Komplexität aufweisen und nach Möglichkeit auf Verschachtelung, Schleifen und ähnliches verzichten. Je linearer sich der Lesefluss gestalten lässt, desto besser. Praktischerweise lässt sich die Forderung durch den Einsatz gängiger Metriken stĂĽtzen – die zyklomatische Komplexität oder den Halstead-Index etwa [2], [3].
  • Terse: Code sollte kurz, kompakt und aufden Punkt gebracht sein. Weniger Code lässt sich nicht nur schneller lesen, nachvollziehen und verstehen, sondern er enthält auch weniger Fehler als langer. AuĂźerdem fallen Codereviews leichter und Tests sind schneller geschrieben, da weniger Code zu testen ist. Ein gutes Hilfsmittel besteht im Auslagern logischer Funktionseinheiten in eigene Funktionen mit passendem Namen.
  • Elegant: Code sollte im mathematischem Sinne "schön" sein und passende Sprachkonstrukte verwenden. Beispielsweise sind die Funktionen map und reduce häufig nicht bekannt oder gängig, gehören aber zum Alltag eines jeden funktionalen Entwicklers. Es handelt sich also nicht um exotische Sprachkonstrukte, fĂĽr die es entschuldbar wäre, sie nicht zu kennen.

Insbesondere die letzte Forderung ist schwer zu konkretisieren, da Eleganz per se im Auge des Betrachters liegt. Sie sollen allerdings auch keine strikten, messbaren Regeln darstellen, sondern vielmehr einen Leitfaden bieten, an dem man sich in die richtige Richtung entlang hangeln kann.