Was versteht man unter „elegantem“ Code?
Die anderen Antworten sind alle gut, aber beziehen sich mehr oder weniger auf die Qualität des produzierten Codes. Das ist sehr wichtig und jeder Entwickler sollte sich niemals mit weniger als funktionierendem, lesbarem, testbarem, kommentiertem und performantem Code, in absteigender Priorität, zufrieden geben.
Es gibt aber noch einen anderen Aspekt auf den die “Eleganz” des Codes einen Einfluss hat. Der Einfluss auf einen selber in der Zukunft. Um dies zu beschreiben muss ich etwas ausholen. Als junger Elektroingenieur ist mir aufgefallen, dass die Anzahl der Lösungen für eine brauchbare Schaltung sehr groß ist. Man muss sich für eine Lösung entscheiden. Welche Lösung soll ich wählen? Aus Spaß entschied ich mich immer für die eleganteste, um mein Gefühl für Kunst zu befriedigen (Meine Eltern waren sehr künstlerisch veranlagt). Später habe ich, als Software Ingenieur, ähnliche Kriterien bei der Erstellung von Software eingesetzt. Was waren diese Kriterien?
Sauber formatierter, eingerückter und gefälliger Quellcode mit Kommentarblöcken vor jedem neuem Gedanken, die immer die gleiche Umrandung haben.
Lösungen die den Code auf ein Minimum reduzieren. Dies reduziert die Anzahl der Zustände und der Abhängigkeiten im Code. Tikhon Jelvis, nennt es ‘simplicity’.
Überschaubarer Code - wird meistens durch 2. erreicht.
Modularer Code - heißt sauber getrennt in verschiedene unabhängige Teile.
Symmetrie - gleiche Anforderungen werden immer durch gleichen Code implementiert (nicht einmal diesen und später einen anderen Algorithmus einsetzen).
Zuerst machte ich dies nur um ein latentes Kunstgefühl zu befriedigen. Aber dann stellte sich heraus, dass ein seltsamer Effekt eintrat, wenn ich eine elegante Schaltung oder ein elegantes Programm nach Monaten bei der Inbetriebnahme eines größeren Projekts wieder unter die Lupe nehmen musste. Der erste Eindruck war ein Glücksgefühl - “das ist doch eine tolle Schaltung/Programm”. Das war aber nicht alles: gleichzeitig mit dem Glücksgefühl war auch die gesamte Arbeitsweise der Innereien der Schaltung/Programm wieder im Kopf präsent. Dies macht das Debuggen bei der Inbetriebnahme von großen Projekten sehr viel einfacher und bändigt die Komplexität.
In meiner langen Praxis als Software Consultant musste ich oft Programme die andere geschrieben hatten in Gang bringen und habe sehr viel hässlichen Code gesehen. Ich habe oft beobachten können wie die Entwickler hässlichen Codes nicht in der Lage waren ihren eigenen Code zu verstehen. Ich bin fest davon überzeugt dies war darauf zurückzuführen, dass sie beim Schreiben des Codes keine Freude hatten und damit nicht in der Lage waren freudige und damit elegante Tüpfel in ihren Code zu integrieren, was dazu führt die Gedankengänge während der Entwicklung wieder zu beleben.
Dies ist das Ende meiner eigenen Antwort.
==============================================================
Ich bin von Quora gebeten worden die folgende Antwort von Tikhon Jelvis auf die Frage What do programmers mean by 'elegant' code? aus den Englischen zu übersetzen. Als ich fertig war konnte ich die Übersetzung nicht senden, weil Quora nicht erlaubt zwei Antworten vom selben Autor auf eine Frage zu liefern. Da ich mir viel Mühe gemacht habe den äußerst interessanten Beitrag genau zu übersetzen habe ich mir erlaubt die Übersetzung hier anzuhängen.
Tikhon Jelvis, studierte Programmiersprachen und forschte in der Synthese von Programmen.
Es ist sehr eng mit Eleganz in der Mathematik verbunden.
Eleganter Code ist einfach, gibt Dir neue Einsichten und ist im Allgemeinen zusammensetzbar und modular. Diese Eigenschaften, obwohl sie fast willkürlich aussehen können, sind tatsächlich tief verwandte, praktisch verschiedene Facetten der gleichen zugrunde liegenden Idee.
Einfachheit
Die wichtigste Eigenschaft ist vielleicht Einfachheit. Aber beachte: Einfach ist nicht das Gleiche wie leicht
. Nur weil ein Code sehr einfach ist, heißt das nicht, dass es für Dich leicht zu verstehen ist. Leichtigkeit ist relativ; Einfachheit ist absolut.
Dies ist besonders relevant für Haskell: Oftmals stammt der eleganteste Haskell-Code von der Vereinfachung eines Problems bis hin zu einer bekannten, universellen Abstraktion, die oft aus der Mathematik stammt. Wenn Du mit der Abstraktion nicht vertraut bist, verstehst Du den Code möglicherweise nicht. Es könnte eine Weile dauern, bis Du es kapierst. Aber es ist immer noch einfach.
Einfacher Code wie dieser ist oft auch knapp und präzise, aber das ist eine Frage der Korrelation, nicht der Ursache. Es geht in eine Richtung: der meiste elegante Code ist knapp und präzise, aber knapper Code ist nicht unbedingt elegant.
Eine Möglichkeit, über Einfachheit nachzudenken, ist, dass es weniger "bewegliche Teile" gibt, weniger Stellen, um Fehler zu machen. Das ist der Grund, warum viele von Haskells Abstraktionen so wertvoll sind - sie beschränken das, was Du möglicherweise tun kannst, schließt häufige Fehler aus und verkleinert den Suchraum.
Erwäge den Unterschied zwischen dem Zuordnen (mapping) über eine Liste und dem Verwenden einer for-Schleife: Mit der Schleife könntest Du die Indizierung durcheinanderbringen, einen ‘off-by-one’ Fehler produzieren oder sogar etwas ganz anderes machen, wie über mehrere Listen gleichzeitig iterieren oder einfach etwas n mal wiederholen. Mit einer ‘map’ kannst Du nur eine Sache tun: eine Liste umwandeln. Einfacher gesagt! Es gibt Dir weniger Stellen, an denen Du einen Fehler machen kannst, und Code, der auf den ersten Blick leichter zu lesen ist, da Du sofort die "Form" des Codes erkennst, wenn Du ‘map’ siehst.
In der Tat ist das wahrscheinlich mein Lieblingstest für die Einfachheit: vorausgesetzt dass ich mit den relevanten Abstraktionen und Idiomen vertraut bin, wie einfach ist es, den Code auf den ersten Blick zu lesen? Code wird häufiger gelesen als geschrieben, aber noch häufiger überflogen, als er gelesen wird. Das macht die Fähigkeit, das Wesentliche eines Ausdrucks schnell zu erkennen - ohne gleich alle Details zu verstehen - unglaublich nützlich.
Einsicht
Eleganter Code schafft neue Einsichten in seinen Wirkungsraum.
Manchmal ist dies eine überraschende Verbindung zwischen zwei Dingen, die unterschiedlich erscheinen. Manchmal ist es eine neue Art, über das Problem nachzudenken. Manchmal ist es ein geschicktes Idiom, das ein normalerweise unangenehmes Muster einfängt. Fast immer ist es eine Idee, die Du auf anderen Code anwenden kannst oder ein allgemeines Muster, das Du bereits anderswo gesehen hast.
Abgesehen von den unmittelbar praktischen Gründen, die hauptsächlich im Abschnitt "Einfachheit" dargestellt sind, bin ich aus folgenden Grund von elegantem Code angezogen: Es ist der beste Weg, neue Dinge zu lernen. Dank ihrer Einfachheit und Allgemeinheit neigen diese Dinge dazu, ziemlich tiefschürfend zu sein. Es sind nicht nur sinnlose Details.
Eleganter Code zeigt auch das Wesen des Problems an, das er löst. Es ist ein klares Spiegelbild der tieferen Struktur, die entweder der Lösung oder dem Problemraum zugrunde liegt, nicht nur etwas, das zufällig funktioniert hat. Wenn Dein Problem, zum Beispiel, eine Art von Symmetrie aufweist, wird eleganter Code dies irgendwie zeigen oder nutzen. Deshalb wird das QuickSort-Beispiel
, das leider einige Probleme hat, so oft vorgeschoben. Es reflektiert die Struktur und insbesondere die Symmetrie von QuickSort wunderbar, die von der imperativen Version im Implementierungsdetail weitgehend verdeckt wird. Die Schlüsselzeile quicksort lesser ++ [p] ++ quicksort greater spiegelt die Form der resultierenden Liste wider.
Zusammensetzbarkeit
Die letzte Eigenschaft eleganten Codes, besonders eleganten funktionalen Codes, ist Zusammensetzbarkeit und Modularität. Es schafft es auf großartige Weise, die natürlichen Stresslinien in einem Problem zu finden und es in mehrere Teile zu zerlegen. In mancher Hinsicht ist das wieder der selbe Punkt: eleganter Code spiegelt die Struktur dessen, was er tut.
Wirklich eleganter Code kombiniert dies mit neuen Einsichten und gibt Dir die Möglichkeit ein untrennbar scheinendes Problem in zwei Teile zu spalten. Zufälligerweise siegt Faulheit hier wirklich.
Ein großartiges Beispiel hierfür ist die Aufteilung bestimmter Algorithmen in zwei Phasen: Erstellen einer großen Datenstruktur und anschließendes Zusammenfalten. Denke nur an heapsort: Erstelle einen Heap und lese dann Elemente daraus. Dieser spezielle Algorithmus ist für sich alleine elegant und lässt sich sehr leicht direkt in zwei Teilen implementieren. Für viele andere Algorithmen besteht die einzige Möglichkeit, sie zu trennen und die gleichen asymptotischen Grenzen beizubehalten, darin, die Datenstruktur mittels ‘lazy evaluation’ zu konstruieren und zu falten.
Conal Elliott hat über dieses Thema einen tollen Vortrag
, der es Wert ist anzuschauen. Er enthält einige spezifische Beispiele für die Aufspaltung von Algorithmen, die untrennbar scheinen, in eine Faltung und eine Entfaltung, von denen die meisten nur mittels ‘lazy evaluation’ funktionieren.
Ich denke, Modularität ist eine der besten Möglichkeiten, Bugs zu vermeiden, und um das zu verdeutlichen, werde ich einfach die Bilder aus dem Vortrag wiederverwenden. Das erste repräsentiert Code, der weniger modular ist; das zweite repräsentiert Code, der modularer ist. Du siehst, warum ich das zweite eleganter finden würde!
Stell Dir diese Diagramme als Teile Deines Codes vor, mit tatsächlichen oder potenziellen Verbindungen zwischen den Teilen. Wenn Dein gesamter Code ein einziger großer Ball ist, könnte jedes Teil potentiell von jedem anderen Teil abhängen; Wenn es Dir aber gelingt, sie in zwei Module mit klaren Modulgrenzen zu unterteilen, geht die Gesamtzahl der möglichen Verbindungen weit nach unten.
Nicht sehr modular, ziemlich komplex - nicht sehr elegant.
Einfacher und eleganter.
Ein Beispiel
Aber das war alles ziemlich abstrakt. Lass mich Dir ein Beispiel geben, das all diese Ideen einfängt und Eleganz geschickt darstellt.
Nehmen wir an, das wir eine Menge Datensätze mit Buchmetadaten haben:
data Book = { author, title :: String 
            , date :: Date 
            {- ... -} 
            } 
Wir wollen unsere Büchersammlung zuerst nach Autor, dann nach Titel und dann nach Datum sortieren. Hier ist der wirklich elegante Weg, dies zu tun:
sortBy (comparing author <> comparing title <> comparing date) 
Wir können das Vergleichen verwenden, um jedes Feld in eine Vergleichsfunktion vom Typ Book -> Book -> Ordering umzuwandeln und dann den Halbgruppen-Operator <> verwenden, um diese Vergleichsfunktionen zu kombinieren.
Es macht genau was Du erwartest - aber wenn Du nicht mit Halbgruppen (monoids) und dem Typ Ordering vertraut bist, weist Du vielleicht nicht, warum es das tut, was Du erwartest.
Auf der anderen Seite gibt es die wirklich explizite Version, die jedes <> durch Pattern-Matching auf EQ, LT und GT. Für jemanden, der mit den relevanten Abstraktionen nicht vertraut ist, könnte dies einfacher erscheinen - aber es ist auch komplexer und laut. Weniger elegant.
Dieses Beispiel ist einfach, weil es sauber über alle Verbindungen abstrahiert, die benötigt werden, um die Vergleichsfunktionen zu kombinieren. Auf den ersten Blick ist leicht zu erkennen, nach welchen Feldern wir sortiert haben und mit welchen Prioritäten.
Es bietet Einsichten, weil es den natürlichen Weg nutzt, Ordering Werte zu kombinieren - die Art, wie sie einen Halbwert bilden. Darüber hinaus ist der Weg vom Ordering Halbwert zum Book -> Book -> Ordering Halbwert umsonst. Wenn wir wissen, wie man irgendeinen Typ o kombiniert, wissen wir, wie man Funktionen a -> o kombiniert. Also die Abstraktion, die die Verbindungen verbarg? Das meiste davon haben wir kostenlos aus Bibliotheken, die überhaupt nicht spezifisch für Ordering sind!
Schließlich ist diese Version definitiv modularer und zusammensetzbarer als die Alternativen. Es ist sehr einfach, verschiedene Vergleichsfunktionen mit diesem Muster zu kombinieren. Wir können trivial Teile von ihnen extrahieren, um ihre eigenen Funktionen zu sein. Es ist sehr einfach zu refactorieren. Alles gute Dinge.
Hoffentlich ist das eine schöne Illustration dessen, was Menschen mit Eleganz meinen und warum es oft in Sprachen wie Haskell auftaucht.
Ende der Übersetzung durch John E Wulff.