- Auch wenn Code logisch hervorragend ist, kann er auf Dauer schwer zu lesen sein; dieser Artikel verortet solche Ermüdung in visuell wahrnehmbarer Komplexität
- Die Halstead Complexity Metrics zählen Operatoren und Operanden, berechnen daraus Volume und Difficulty und bewerten Implementierungen mit mehr Variablen und Operatoren selbst bei gleichem Verhalten als schwieriger
- Die Cognitive Complexity von SonarSource bewertet die Belastung durch verkürzte Syntax, Unterbrechungen des linearen Ablaufs und verschachtelte Kontrollflüsse anhand von Bedingungen, Schleifen, Ausnahmebehandlung, Kombinationen logischer Operatoren, Rekursion und
goto - Auf Variablenebene erhöhen Variable Shadowing, ähnliche Namen, lange Lebensdauern von Variablen und ungewohnte Nutzungsmuster die Kosten für Leser, den Datenfluss nachzuverfolgen
- Kleine Funktionen, vertraute Muster, das Gruppieren langer Ketten, einfache Bedingungen, zurückhaltender Einsatz von
goto, flache Verschachtelung, gut unterscheidbare Variablennamen und kurze Variablenlebensdauern sind Lesbarkeitskriterien, die sprach- und formatübergreifend anwendbar sind
Grenzen und Ziel von Metriken zur Code-Lesbarkeit
- Für Code-Lesbarkeit gibt es keine einzelne, weithin genutzte und konsensfähige Metrik
- Nützliche Referenzen bestehen meist aus wissenschaftlichen Arbeiten, die in der Praxis kaum breit eingesetzt werden, oder aus eher meinungsbasierten Ansätzen; für Code-Reviews wurde daher ein konkreteres Vokabular für Diskussionen gebraucht
- Ziel ist nicht, eine neue Metrik zu erfinden, sondern visuelle Muster zu sammeln, die jeder verwenden kann, wenn es darum geht, ob Code leicht lesbar ist
- Betrachtet wurden Messungen oder Ideen, die folgende Bedingungen erfüllen
- Sie lassen sich auf einen Quellcodeausschnitt oder eine einzelne Funktion anwenden
- Sie konzentrieren sich nicht nur auf die Kernkomplexität, die sich wie bei Cyclomatic Complexity schwer vom implementierten Algorithmus selbst trennen lässt
- Sie bleiben nicht bei oberflächlichen Stilfragen wie Länge von Variablennamen, Leerraum, Einrückung oder Klammerplatzierung stehen
Halstead Complexity Metrics
- Maurice Halstead schlug Ende der 1970er Jahre die Halstead Complexity Metrics vor, um empirische Messwerte für Quellcode zu erhalten
- Diese Metrik ist sprach- und plattformübergreifend anwendbar und fokussiert stärker darauf, in welcher Form Code geschrieben ist, als auf den implementierten Algorithmus selbst
- Die zentralen Messwerte sind vier Zählgrößen auf Basis von Operatoren und Operanden
- Anzahl eindeutiger Operatoren
n1 - Anzahl eindeutiger Operanden
n2 - Gesamtzahl der Operatoren
N1 - Gesamtzahl der Operanden
N2
- Anzahl eindeutiger Operatoren
- Darauf aufbauend leitete Halstead weitere Kennzahlen wie length, volume und difficulty ab und versuchte sogar, die Zahl der in einer Implementierung enthaltenen Bugs zu schätzen
- Intuitiv gilt: Je mehr Operatoren vorhanden sind, desto mehr potenzielle Wechselwirkungen müssen betrachtet werden, und je mehr Operanden vorkommen, desto schwieriger wird es, mögliche Datenflüsse zu verstehen
-
JavaScript-Beispiel
- Selbst bei derselben Funktion zur Gerade/Ungerade-Prüfung hat eine einfache Implementierung mit
ifundreturnweniger Operatoren und Operanden - 4 eindeutige Operatoren, 7 Operatoren insgesamt
- 5 eindeutige Operanden, 6 Operanden insgesamt
Volume33.30,Difficulty2.50- Eine Implementierung mit Array,
Number, Vergleichsausdruck und Indexen hat mehr Operatoren und Operanden - 7 eindeutige Operatoren, 10 Operatoren insgesamt
- 9 eindeutige Operanden, 12 Operanden insgesamt
Volume71.35,Difficulty3.75- Die erste Implementierung wirkt schon visuell einfacher, und die Werte für
VolumeundDifficultyvon Halstead stützen diesen Eindruck - Ein Nachteil ist, dass nicht in jeder Sprache eindeutig ist, was als operator und operand zählt; für Messungen ist es daher sinnvoll, ein bestimmtes Tool oder eine konkrete Implementierung festzulegen und konsistent zu verwenden
- Selbst bei derselben Funktion zur Gerade/Ungerade-Prüfung hat eine einfache Implementierung mit
-
Praktische Muster aus Halstead
- Kleine Funktionen und wenige Variablen sind im Allgemeinen leichter zu lesen
- Sprachspezifische Operatoren oder syntaktischer Zucker bedeuten zusätzliche Last für Leser und sollten daher nicht übermäßig eingesetzt werden
- Wenn funktionale Bausteine wie
map,reduceundfilter, Lambdas, Iteratoren oder Comprehensions in langen Ketten verbunden werden, kann die Lesbarkeit trotz Kürze leiden - Solche langen Ketten treten häufiger in JavaScript und Rust oder in Python-Code auf, der tief in
itertoolseingetaucht ist
Wie Cognitive Complexity Leseschwierigkeit betrachtet
- Die von SonarSource entwickelte Cognitive Complexity ist eine Metrik, die Leseschwierigkeiten präziser erfassen soll
- Die Kernideen der Metrik sind drei Punkte
- Verkürzte Syntax, die Aussagen zusammenfasst, senkt die Schwierigkeit
- Jedes Verlassen eines linearen Ablaufs erhöht die Schwierigkeit
- Verschachtelte Kontrollflüsse erhöhen die Schwierigkeit
- Es gibt Kritik daran, dass der Name wissenschaftlicher oder objektiver klingt, als die Metrik tatsächlich ist; praktisch kann man sie jedoch als nützliche Heuristik betrachten
-
Das Dichteproblem verkürzter Syntax
- Im Vergleich zu einer Form wie
if (a != null) { myObj = a.myObj; }ist verkürzte Syntax wieMyObj myObj = a?.myObj;kürzer und schneller zu lesen - Die beiden Codes sind in der Praxis jedoch nicht immer vollständig gleichbedeutend
- Im ersten Fall wird
myObjzua.myObjodernull - Im zweiten Fall wird
myObjzua.myObjoderundefined - Selbst Sprachen mit stärkerer Typprüfung wie TypeScript oder Rust verringern nur die Wahrscheinlichkeit von Auslassungen, garantieren aber nicht, dass alle Fälle korrekt behandelt werden
- In Sprachen mit schwächerer Typprüfung wie normalem JavaScript ist die Wahrscheinlichkeit höher, dass solche Corner Cases unberücksichtigt bleiben
- Verkürzte Syntax kann leicht zu schreiben und zu lesen sein, aber es gibt einen Trade-off zwischen Kürze und Dichte
- Im Vergleich zu einer Form wie
-
Elemente, die den linearen Ablauf unterbrechen
- Linearer Code ohne Bedingungen lässt sich leichter überfliegen als Code mit Bedingungen
- Cognitive Complexity betrachtet nicht nur Bedingungen, Schleifen und
goto, sondern auch bedingte Makros,try/except, Sequenzen logischer Operatoren und Rekursion als Schwierigkeitserhöher switchwird als eine Gruppe gezählt, während eineelse-if-Kette mit jedem zusätzlichenelse-ifals schwieriger gilt- Das liegt daran, dass in jedem
else-if-Zweig mehr als ein Vergleich vorkommen kann - Allerdings können auch Fall-through in
switchund fehlendebreak-Anweisungen die Leseschwierigkeit erhöhen - Es macht einen Unterschied, ob in einer Bedingung derselbe logische Operator wiederholt wird oder ob
&&,||und!gemischt werden debug || verbose || consoleModeist eine einfache Bedingungdebug || (verbose && consoleMode)ist wegen der gemischten Operatoren schwerer zu lesendebug || !(verbose && consoleMode)ist durch die zusätzliche Negation noch komplexer
-
Ausnahmebehandlung und goto
try/catcherhöht in Cognitive Complexity die Schwierigkeit, aber mehrerecatch-Blöcke gelten nicht als schwieriger als ein einzelnercatch;tryundfinallywerden ignoriert- Schon das Werfen einer Exception kann Lesekosten verursachen
- Wenn Ausnahmebehandlung über Funktionsgrenzen hinweg wirkt, verflechten sich die Komplexitäten der beteiligten Funktionen
- Leser müssen herausfinden, wo die betreffende Exception abgefangen wird
gotowird üblicherweise als Schwierigkeitserhöher gewertet- Formen wie
goto outodergoto done, die bei Fehlerbedingungen Ressourcen freigeben und die Funktion verlassen, werden von manchen Experten jedoch als nützlich angesehen - Dagegen ist
goto, das Schleifengrenzen auf eine Weise überschreitet, die sich nicht mitcontinueoderbreakausdrücken lässt, für Leser besonders belastend, weil der Kontrollfluss neu rekonstruiert werden muss
Verschachtelung und Funktionsform
- Wenn Bedingungen an sich schon schwer zu lesen sind, sind verschachtelte Bedingungen noch schwerer zu lesen
- Cognitive Complexity addiert zur Punktzahl von Bedingungen und Schleifen zusätzlich Schwierigkeit je Verschachtelungsebene
- Diese Idee wird auch unter Namen wie „Level of Indentation“ oder „Bumpy Road“ beschrieben
- Ab mehr als zwei Verschachtelungsebenen wird Lesen besonders mühsam; Code mit Early Returns liest sich oft flacher
- Die Metrik berücksichtigt die Funktionslänge nicht direkt, aber unter sonst gleichen Bedingungen erfordern lange Funktionen mehr Leseaufwand als kurze
Variablennamen, Lebensdauer und vertraute Muster
-
Unterscheidbare und beschreibende Namen
- Beschreibende Namen sind wichtig, um zu verstehen, was Code tun will; doppelte oder kryptische Namen wirken gegenteilig
- Variable Shadowing ist riskant
- Situationen, in denen Leser erst Scope-Regeln nachvollziehen müssen, um zu erkennen, welche Variable verwendet wird, sollten vermieden werden
- Auch visuell ähnliche Bezeichner sollten vermieden werden
- Namen wie
iundjoderitemunditemslassen sich leicht verwechseln und können Fehler begünstigen - Code, der innerhalb einer Funktion mehrere Varianten desselben Variablennamens wie
node,_nodeundthisNodeverwendet, ist besonders fehleranfällig
-
Kurze Variablenlebensdauer
- Live Variable Analysis betrachtet den Bereich von der ersten Verwendung einer Variable bis zu dem Punkt, an dem sie letztmals verwendet werden kann
- Ist die Lebensdauer einer Variable lang, müssen Leser mehr Variablen und mögliche Werte gleichzeitig im Kopf behalten
- Statt alle Variablen am Anfang einer Funktion zu deklarieren, kann eine Deklaration direkt vor der tatsächlichen Nutzung die Lebensdauer verkürzen
- Der schlechteste Fall ist, wenn Variablen über mehrere Funktionen hinweg leben und an mehreren Stellen verwendet werden
- Haben mehrere Variablen faktisch dieselbe Lebensdauer, kann ein Objekt die passendere Form sein
- Wenn ein Objekt nicht passt, sollte man wenigstens die Zahl der Funktionen und Zeilen minimieren, die Leser verstehen müssen, um die Werte nachzuvollziehen
-
Lange Ketten und Zwischenvariablen
- Ein funktionaler Programmierstil verkürzt zwar Variablenlebensdauern, aber zu lange Ketten oder verschachtelte Callbacks erzeugen wieder Leselast
- Lange Funktionsketten lassen sich in kleine Gruppen aufteilen; gut benannte Zwischenvariablen oder Helper-Funktionen senken die kognitive Belastung
- Die Version mit Zwischenvariablen kann geringfügig weniger effizient sein
- Solange Performance-Tools nicht zeigen, dass genau diese Zeile ein echter Bottleneck ist, spielt ein solcher Mikroeffizienz-Unterschied keine große Rolle
-
Wiederverwendung vertrauter Code-Muster
- Wenn vertraute Code- und Variablenformen wiederverwendet werden, erkennen Leser bekannte Muster und lesen mit weniger Aufwand
- Das steht dem Principle of Least Surprise nahe
- Innerhalb einer Codebase ist es sinnvoll, wiederkehrende Formen wie den Stil von Bedingungen konsistent zu halten
- Wenn man von einem Muster abweichen muss, kann man den Unterschied durch Variablennamen oder Kommentare sichtbar machen
- Denkt man das konsequent zu Ende, führt es zur Verwendung von Template-Funktionen oder generischen Funktionen, damit Leser wiederkehrende Muster nicht jedes Mal neu erkennen müssen
8 visuelle Muster für bessere Lesbarkeit
- Line/Operator/Operand count: Kleinere Funktionen und weniger Variablen bzw. Operatoren sind leichter zu lesen
- Novelty: Neuartigkeit bei Funktionsform, Operatoren und syntaktischem Zucker vermeiden und gemeinsame Muster der Codebase wiederverwenden
- Grouping: Lange Funktionsketten, Iteratoren und Comprehensions mit Helper-Funktionen oder Zwischenvariablen logisch gruppieren
- Conditional simplicity: Bedingungen möglichst kurz halten und innerhalb einer einzelnen Bedingung eher Sequenzen desselben logischen Operators als Mischungen verschiedener Operatoren bevorzugen
- Gotos:
gotonicht verwenden, außer wenn es einem bestimmten Fehlerbehandlungsmuster folgt und die Alternativen schlechter sind - Nesting: Verschachtelte Logik und große Einrückungswechsel minimieren; wenn tiefe Verschachtelung nötig ist, nicht in einer großen Funktion vergraben, sondern in separate Funktionen auslagern
- Variable distinction: Beschreibende, visuell gut unterscheidbare Variablennamen verwenden und Variable Shadowing vermeiden
- Variable liveness: Variablenlebensdauern kurz halten und besonders auf lange Lebensdauern über Funktionsgrenzen hinweg achten
Probleme, die sich in realen Codebasen zeigten
- Codebasen, die starke mentale Ermüdung verursachten, vereinten oft mehrere Antipatterns gleichzeitig
- Konkret fanden sich lange Funktionen, gemischte Sprachelemente und viele Funktionsketten, die besser in Helper-Funktionen aufgeteilt worden wären
- Dadurch nahmen innerhalb großer Funktionen Verschachtelungskomplexität und lange Variablenlebensdauern gemeinsam zu
- Trotz hoher Qualität von Code und Autor wurden ein oder mehrere kritische Bugs gefunden
- Einer davon war eigentlich ein gut sichtbarer Bug, wurde aber wahrscheinlich übersehen, weil er mitten in einer langen und komplexen Funktion steckte und daher schwer zu durchdenken war
- Die Person, die deinen Code in einem Monat am wahrscheinlichsten am häufigsten lesen wird, bist du selbst; gut lesbare Form ist daher direkt mit praktischen Kosten im Arbeitsalltag verbunden
1 Kommentare
Hacker-News-Kommentare
Dass der Artikel behauptet, lange oder mehrere map/reduce/filter-Ketten schadeten der Lesbarkeit, ergibt sich aus dem übrigen Inhalt überhaupt nicht.
Es wirkt, als sei hier die übliche Klage eingeschoben worden, etwas sei schlecht, nur weil man damit nicht vertraut ist; dabei ist es mit ein wenig Gewöhnung oft viel leichter zu lesen und zu schreiben als die Alternativen.
Zum Beispiel dürfte es schwer sein, besser lesbaren Code vorzulegen als
books.filter(book => book.pageCount > 1000).map(book => book.author).distinct().Auch nach Komplexitätsmetriken ist solcher Code fast jeder anderen Variante überlegen, und man sollte zumindest die Grundlagen der funktionalen Programmierung lernen. Man muss keine Monaden erklären, aber man sollte vertraut genug damit sein,
mapundfilternicht pauschal schlechtzureden.Gemeint war ungefähr, dass es ab Ketten mit fünf aufeinanderfolgenden Aufrufen schwierig zu lesen wird; auch das Beispiel im Artikel war etwa so lang.
„multiple“ bezog sich auf verschachtelte Ketten oder Fälle, in denen sich der Typ, mit dem gearbeitet wird, ändert; in solchen Fällen liest man langsamer.
Funktionale Bausteine können elegant sein, aber man kann sie auch übertreiben.
Wenn die Kette zu lang wird, die Bedingungen komplex werden und es viele Listen/Variablen gibt, wird es schwer, alles auf einmal zu verstehen.
In einer prozeduralen Schleife kann man Zwischenergebnissen Namen geben, und diese Namen helfen, die vorherige Verarbeitung gedanklich abzulegen und sich auf den nächsten Schritt zu konzentrieren.
SELECT DISTINCT author FROM books WHERE pageCount > 1000;Persönlich teile ich Ketten gern auf, um Zwischenergebnissen Namen zu geben; Variablennamen dienen dann gewissermaßen als Kommentare.
var longBooks = books.filter(book => book.pageCount > 1000)var authorsOfLongBooks = longBooks.map(book => book.author).distinct()?- setof(Author, Book^Pages^(book_author(Book, Author), book_pages(Book, Pages), Pages > 1000), Authors).Je nach Struktur der Prolog-Datenbank geht es auch kürzer:
?- setof(Author, Pages^(book(_, Author, Pages), Pages > 1000), Authors).Guter Code hat meiner Meinung nach grundsätzlich einen ziemlich großen qualitativen und literarischen Aspekt.
Programmierer oder Akademiker, die an mathematisches Denken gewöhnt sind und quantitative Antworten wollen, empfinden das oft als unangenehm.
Ich mag sowohl Dostojewski als auch Wodehouse; beide sind großartig, aber auf sehr unterschiedliche Weise.
Auch wenn Coding kein ganz so offenes Feld ist, habe ich mit guten Codebases gearbeitet, die sich qualitativ völlig unterschiedlich anfühlten, und oft braucht es Zeit, bis man den Stil einer Codebase „im Gefühl“ hat, ähnlich wie man sich an den Stil eines neuen Autors gewöhnt.
Gemeint war, dass die Funktionen so angeordnet waren, dass beim Lesen der Datei von oben nach unten eine natürliche Erzählung entsteht, und dass die deklarative Implementierung sich anfühlt, als spräche sie den Leser direkt an.
Ich folge einem rein funktionalen Programmierparadigma, und ich denke, dieser Ansatz passt gut zu einem erzählerischeren Stil.
Die Abhängigkeiten/Eingaben einer Funktion sind auf Argumente oder andere reine Funktionen beschränkt, und die Ausgabe steckt nur im Rückgabetyp; dadurch lässt sich die Komplexität Schritt für Schritt führen.
Anders als bei anderen Paradigmen mit Komplexität durch versteckten Zustand passt ironischerweise gerade das mathematisch präziseste Paradigma meiner Ansicht nach auch am besten zu einem erzählerischeren Stil.
Die Form ist egal; wenn man ohne lange Entwicklungserfahrung nicht in angemessener Zeit erkennen kann, was eine Funktion tut, ist es schlicht schlechter Code.
Der im Artikel gezeigte ternäre Operator
return n % 2 === 0 ? 'Even' : 'Odd';fühlt sich für das menschliche Gehirn zum Beispiel an, als müsse man ihn rückwärts lesen, und er ist eher dafür geeignet, dass ein Compiler den Syntaxbaum verarbeitet, als für Menschen.Ein menschlicher Mathematiker würde es als stückweise definierte Funktion schreiben: Wenn
n mod 2 = 0, dann'Even', sonst'Odd'; das ist deutlich klarer.Sie helfen neuen Teammitgliedern, einen konsistenten Stil zu lernen, und halten auch den Teamstil einigermaßen einheitlich.
Der Kommentar zu
.editorconfigist ebenfalls lesenswert: https://news.ycombinator.com/item?id=43333011Das reduziert Diskussionen über Stil-Details in Pull Requests.
Der Artikel ist gut, aber ich finde, er übersieht den mental anstrengendsten Faktor beim Lesen von Code: Veränderlichkeit
Es ist ein großes Geschenk, wenn man beim Lesen einer Methode die Bedeutung einer Variablen genau einmal „festnageln“ und sie während der restlichen Schlussfolgerungen unverändert beibehalten kann
Das Verständnis einer Methode sollte monoton von 0 % auf 100 % wachsen; man sollte die Methode nicht im Kopf neu starten müssen, weil man durcheinanderkommt, wie der Schleifenrumpf in einer bestimmten Iteration den Akkumulator verändert hat
Genau hier liegt auch der eigentliche Grund, warum GOTO schädlich ist. Nicht, weil es schwierig wäre, den gedanklichen Befehlszeiger innerhalb einer Methode zu verschieben, sondern weil es bei GOTO schwer ist, den Zustand veränderlicher Variablen zu kennen
Sowohl veränderliche als auch unveränderliche Variablen können diese Bewegung unterstützen oder behindern; es hängt davon ab, wie sauber der Code diesem Raum entspricht
Unveränderliche Variablen haben den kleinen taktischen Vorteil, dass man sich keine Sorgen machen muss, dass sich ihr Wert ändert oder auf missverständliche Weise geändert wird, aber meiner Erfahrung nach ist dieser Vorteil nicht groß genug, um daraus die Regel „immer Immutability verwenden“ zu machen
Manchmal erlaubt Veränderlichkeit, diesen Informationsraum deutlich sauberer auszudrücken
Aus Sicht des Aufgerufenen, nicht der Aufrufstelle, kann man nicht zurückverfolgen, was zuvor passiert ist, wenn jemand zu einer bestimmten Zeile springen kann. Weil man von überall gekommen sein könnte, braucht man statt lokaler Analyse eine globale Programmanalyse
Wenn Veränderlichkeit die eigentliche Quelle der GOTO-Komplexität wäre, müssten
if-Anweisungen undfor-Schleifen dasselbe Problem habenIch stimme zu, dass Veränderlichkeit und Zustand direkt Komplexität erzeugen, aber GOTO gehört für mich in eine völlig andere und viel schädlichere Kategorie
Ein Muster, das ich persönlich nicht mag, ist, in einem
ifdirekt zurückzugeben und den Rest als impliziten Standardpfad stehen zu lassenif (n % 2 === 0) return "Even"; return "Odd";ist zwar kürzer, aber ich bevorzuge deutlichif ... return "Even"; else return "Odd";Der Grund ist, dass Ersteres asymmetrisch wirkt.
"Even"und"Odd"sind symmetrische Alternativen, daher ist die Variante mitelseintuitiverreturn (n % 2 === 0) ? "Even" : "Odd";zu schreiben ist am leichtesten zu lesen, weil es am wenigsten Boilerplate hatIn Sprachen mit ternärem Operator sollte das leicht zu erkennen sein
Meiner Erfahrung nach erzeugt
elsezusätzliche Verschachtelung und führt leicht dazu, dass man Randbedingungen oder Variablen weiter auswertet, die über den unmittelbaren Bereich des Happy Path hinausgehen. Diesen ganzen Kontext muss man dann im Kopf behaltenDirekt unter dem Funktionsnamen sieht man auf einer Einrückungsebene visuell die Rückgabe, und wenn es keinen frühen Abbruch gibt, vermittelt das das Gefühl, dass ein Ergebnis garantiert ist
Wenn die Rückgabe weiter unten vergraben ist, fühlt sich das irgendwie seltsam an
return "Odd";vomifgetrennt wirkt, und, wenn die Sprache es erlaubt, auch geschweifte Klammern um denif-Rumpf setzenEs gibt Situationen, in denen ich
elsezulasse, aber meistens dann, wenn es Nebenwirkungen gibt; normalerweise wird es klarer, wenn man so lange refaktoriert, bis man es entfernen kannHäufig wird komplexer Code zu einer Guard-Sequenz, die in der Reihenfolge von Wichtigkeit oder Ausführungskosten aussteigt, und trennt die eigentliche Funktions-/Methodenlogik von den Abbruchbedingungen
Die erste Variante fließt nach unten weiter und führt den zweiten Satz von Anweisungen aus.
returnist ein spezieller Operator, der den Kontrollfluss unterbricht, daher bildet der normale Kontrollfluss der ersten Variante die Vollständigkeit der beiden Fälle nicht richtig abIn idiomatischem Rust verwendet man
returnnicht, außer in Ausnahmefällen, die den Kontrollfluss einer Methode unterbrechen; das zweite Beispiel sieht man normalerweise häufiger ohne Return-AnweisungAuch Python wird typischerweise so verwendet, dass bei falschen Argumenten oder falschem Zustand früh zurückgegeben wird und der Return an der Tail-Position der eigentliche Rückgabewert ist
Wegen solcher Konventionen wirkt ein eingerücktes
returnwie ein Ausnahmefall, wenn man die vollständigeif-else-Struktur aufbricht. Folgt man dieser Konvention, erscheinenreturn-Anweisungen natürlich als redundant, außer wenn sie den Kontrollfluss unterbrechen, und Rusts Konvention wird verständlich. In jeder Sprache istreturneine Anweisung, diebreakentsprichtVielleicht geht es nur mir so, aber TypeScript kann Code manchmal schwer lesbar machen
Wenn man das Datenmodell einigermaßen „atomar“ hält und Entwickler gewissenhaft tatsächlich Typen deklarieren und dokumentieren, ist es in Ordnung
Aber sobald Typen über Utility Types aus anderen Typen abgeleitet werden und man explizite Typen weglässt und sich auf Typinferenz verlässt, beginnt das Ganze schnell auseinanderzufallen
Es wird sehr schwer nachzuvollziehen, woher ein Feld kommt, wenn man in einem tiefen Stack mit 4–5 Ebenen von Typ-Indirektion steckt. Ein Teil ist inferiert, ein Teil explizit, ein Teil sind abgeleitete Typen, und Feld-Aliasse sind ebenfalls vermischt
Bei großen Datenmodellen und tiefen Call Stacks ist eine Form wie
function checkDogs(dogs: Dog[]) { ... }, bei der der Rückgabetyp weggelassen wird, völlig unbrauchbar und macht einen wirklich wahnsinnigDer Hauptgrund ist, alle Rückgabepfade in dieser Funktion dazu zu zwingen, diesen Typ einzuhalten
Ich habe oft gesehen, dass beim Hinzufügen einer neuen Bedingung eine Regression entstand, weil ein anderer Branch einen leicht abweichenden Typ zurückgab
Variablendeklarationen mit Typen zu versehen, halte ich dagegen für wenig wertvoll
Im Beispiel ist
const checkedDoggos = checkDogs([])gut, und man kanncheckedDoggosden Funktionstyp erben lassenIch arbeite mit einer Codebase, in der der Linter
const checkedDoggos: DogBreedAndSize[] = checkDogs([])erzwingt; das ist ziemlich lächerlich und bringt kaum NutzenIn JavaScript kann man sich nicht sicher sein und muss ständig im Stack auf und ab gehen und alles im Kopf behalten
Bei der Aussage „Kleine Funktionen mit wenigen Variablen sind im Allgemeinen leicht zu lesen“ sollte man vorsichtig sein
Ich mag es nicht, wenn sich Diskussionen über Lesbarkeit nur auf Mikro-Lesbarkeit konzentrieren. Dann wird Code unter der falschen Annahme, Mikro-Lesbarkeit sei wichtiger als Makro-Lesbarkeit, leicht übermäßig kleinteilig zerlegt
Eine solche Dogmatisierung bringt Programmierer hervor, die den Wald vor lauter Bäumen nicht sehen, und führt zu extrem ineffizientem oder schwer zu debuggendem Code
Sprachen aus der APL-Familie liegen am anderen Extrem, aber der tatsächliche Sweet Spot dürfte irgendwo in der Mitte liegen und sich je nach Person stark unterscheiden
Bei unbekanntem Code wird es schon anstrengend, drei- oder viermal zur Definition zu springen; vielleicht liegt das an mir, aber ich kann mir schwer vorstellen, dass die meisten darin viel besser sind als ich
In der .NET-Kultur, besonders bei „Clean Architecture“, ist dieses Problem erschreckend ausgeprägt. Wenn man ein Feature ändern oder ein Problem nachverfolgen will, ist es über 4 Schichten und 15 Dateien verteilt, und manche Dateien bestehen zu über 60 % nur aus Schlüsselwörtern
Ich weiß nicht, wo man die Grenze ziehen sollte, aber im Allgemeinen bevorzuge ich eine längere Funktion, die andere Empfehlungen beachtet und sich sequenziell lesen lässt, gegenüber Code, der so zerstückelt ist, dass man alle fünf Zeilen nach oben und unten scrollen muss
Bei Typen/Klassen ist es genauso: Ein enum mit vier Werten, das nur in diesem DTO verwendet wird, muss nicht unbedingt in eine andere Datei
Ein interessanter Artikel, aber nicht wirklich zufriedenstellend
Er springt zu schnell zu Schlussfolgerungen und landet wieder bei persönlichen Vorlieben. Ich stimme mehreren Vorlieben zu, aber der Artikel selbst wollte ausdrücklich über Vorlieben hinausgehen
Die Aussage „Sprachspezifische Operatoren oder syntaktischer Zucker belasten den Leser und sollten vermieden werden“ folgt nicht aus den Metriken. Wenn eine Funktion drei verschiedene Operatoren enthält und ein sprachspezifischer Operator alle drei auf einmal ersetzen kann, sinkt der „Aufwand“ der Funktion
Auch Bausteine wie
map/reduce/filterkönnen, wenn sie gut eingesetzt werden, andere Operatoren ersetzen und das „Volumen“ verringern; es kann also in beide Richtungen gehenDas Beispiel
?.wirkt wie eine sehr sprachspezifische Diagnose, die auf das schwer lesbare Sprachdesign von JavaScript verweist. In vielen Sprachen sindnullundundefinednicht getrennt, weshalb man häufig von einem null-safe operator spricht„Variable Shadowing ist schrecklich“ und „lange Lebensdauer zwingt dazu, mehr Variablen im Kopf zu behalten“ können miteinander kollidieren
In manchen Kontexten mag ich Variable Shadowing sehr, weil es die frühere Instanz aus dem Scope entfernt, statt sie weiter zugänglich zu lassen
Es gibt ein tolles Plugin namens Highlight für VS Code
Damit kann man Code per benutzerdefinierter Regex anders einfärben; ein typischer Anwendungsfall dürfte sein,
//TODOgelb zu markierenIch nutze es, um Logs abgeschwächt darzustellen, weil überall eingefügte Logs viel visuelles Rauschen erzeugen
Die von mir gepflegte Library verwendet Logs wie
this.logger?.info('Some logs here');; darauf wende ich eine Transparenz von 0,4 an, sodass sie in den Hintergrund tretenSichtbar sind sie immer noch, aber auf den ersten Blick sticht die eigentliche Business-Logik stärker hervor
Die Einstellung kann man zum Beispiel so anpassen:
"highlight.regexes": { "((?:this\\.)?(?:_)?logger(?:\\?)?.(debug|error|info|warn)[^\\)]*\\)\\;)": { "regexFlags": "gmi", "decorations": [{ "opacity": "0.4" }] } }https://marketplace.visualstudio.com/items?itemName=fabiospa...
Der Aussage, dass das Aufteilen langer Funktionsketten oder Callbacks in kleine Gruppen mit gut benannten Variablen etwas weniger effizient sei, stimme ich nicht zu
Beide Versionen können gleich effizient sein
In beiden Fällen werden dieselben Objekte alloziert, auf dem Heap gespeichert und später von der Garbage Collection erfasst. Der Effizienzunterschied hängt vom Compiler ab
In der zweiten Version sollte der Compiler erkennen können, dass jede Variable nur direkt nach ihrer Deklaration verwendet wird, und diese Objekte wie bei einem Chained Call aus dem Scope behandeln
Natürlich unter der Annahme, dass man den Variablentyp inferieren lässt
Die Kosten, die man in der Praxis sieht, entstehen, wenn man Zwischenwerte explizit materialisiert, um sie im Debugger anzusehen. Wenn man sie zum Beispiel in eine Liste umwandelt, entstehen vermeidbare Allokationen und damit Kosten
Der Versuch, „Lesbarkeit“ zu quantifizieren, ist gut. Von solchen Ansätzen brauchen wir viel mehr
Die derzeit häufigste Definition von Lesbarkeit fühlt sich eher an wie „das, was ich leicht lesen kann“
Vielleicht ließen sich die tatsächlichen Dimensionen von Lesbarkeit finden, indem man sehr vielen Menschen Code zeigt, sie einen Satz auswählen lässt, der beschreibt, was der Code tut, und dabei die Zeit misst
Die Aufgaben, die die meisten Menschen in der kürzesten Durchschnittszeit richtig lösen, wären Beispiele für in der realen Welt gut lesbaren Code und, wichtiger noch, könnten helfen, wirklich schwer lesbare Praktiken zu identifizieren
Die Antwortenden würden sich vermutlich entlang von Achsen wie „Programmiererfahrung“ oder „versteht Paradigma X“ clustern, und wenn sich Trends ändern, könnten sich die Ergebnisse im Laufe der Zeit ebenfalls verschieben
Was wir zu lesen und zu schreiben gelernt haben, prägt, was wir als leicht lesbar empfinden
Was man erreichen will, mit wem man arbeitet, was man vor dem Programmieren bereits konnte, welche anderen Sprachen man kennt und viele weitere Faktoren spielen eine Rolle
Nachdem man die niedrig hängenden Früchte geerntet hat – also über Dinge wie „Variablennamen nicht willkürlich, irrelevant oder irreführend wählen“ hinaus ist –, könnten viele Fragen der „Lesbarkeit“ letztlich eine Frage der Konsensbildung sein
Vielleicht gibt es keine richtige Antwort, die über die konkrete Gruppe von Programmierern hinausgeht, mit der man zusammenarbeiten will
Code-Lesbarkeit ähnelt der Lesbarkeit natürlicher Sprache: Sie ist vor allem für Menschen ein Problem, die die Sprache nicht gut kennen, und lässt sich mit Zeit beheben
Das eigentliche Problem beim Programmieren ist Code-Komplexität, und die lässt sich nicht allein anhand von Metriken einzelner Codefragmente beurteilen
Das Problem liegt eher in den Beziehungen zwischen Funktionen als in den Implementierungsentscheidungen innerhalb eines Funktionskörpers
Herauszufinden, was Code tut, ist meist einfach; schwierig ist es, den Code zu ändern oder Funktionen hinzuzufügen
Diese Schwierigkeit entsteht, weil mehrere Abstraktionsebenen verbergen, wie sie miteinander verbunden sind