Schlechte Zeichen oder Braune M&Ms

»Der Code ist schon sehr schlecht.«

>>Ist doch Geschmackssache.«

» Naja, es sind GOTOs drin.«

»Oh.«

Kathrin Passig im Gespräch mit der Softwareentwicklerin Anne Schüßler

Die Band Van Halen verlangte in ihren Verträgen von allen Veranstaltern, dass im Back-'MgolxTcich eine Schüssel M&Ms bereitstehen musste, die keine braunen M&Ms enthalten durfte. Das klingt zunächst nach einer Legende, ist aber keine. David Lee Roth begründet die Forderung in seiner Autobiografie:

»Van Halen war die erste Band, die riesige Bühnenshows auch in mittelgroßen Locations veranstaltete. Wir fuhren mit neun Sattelschleppern voller Equipment vor, wo bis dahin maximal drei der Standard waren. Und die technischen Probleme waren endlos - der Bühnenunterbau hält das Gewicht nicht aus, der Fußboden bricht ein, die Türen sind nicht groß genug für unsere Geräte.

Unsere Anforderungsliste las sich wie die chinesischen Gelben Seiten, weil wir so viel Equipment hatten, und so viele Leute, die dafür sorgen mussten, dass alles funktioniert. ( ...) Deshalb hatten wir als kleinen Test im Technikabschnitt der Liste stehen: >Im Backstagebereich darf es keine braunen M&Ms geben, bei einem Verstoß steht der Band das Recht zu, die Leistung zu verweigern, bei vollem Anspruch auf die vereinbarte Gage.« Wenn ich also ein braunes M&M in der Schüssel fand, musste die ganze Veranstaltung von Grund auf durchgecheckt werden. Unter Garantie gab es dann irgendeinen technischen Fehler. Die hatten den Vertrag nicht gelesen. Da konnte man sich drauf verlassen. Manchmal fand sich dabei was, was die ganze Show ruinieren konnte. Da gab es buchstäblich lebensgefährliche Probleme.«

David Lee Roth: Crazy From the Heat, S. 110, Übersetzung d.A.

Braune M&Ms gibt esauch im Code. Bestimmte, an sich harmlose Anzeichen deuten fast immer darauf hin, dass etwas Grundlegendes nicht stimmt. So erklärt sich die schroffe Reaktion von Profis auf Kleinigkeiten wie etwa uneinheitliche Formatierung, die auf Anfänger oft übertrieben und erbsenzählerisch wirkt. Die folgende Liste soll Ihnen im günstigsten Fall dabei helfen, solche Symptome in Ihrem Code zu erkennen und zu beheben. Aber selbst wenn Sie gar nichts weiter unternehmen, werden Sie nach der Lektüre wenigstens wissen, woran bessere Programmierer schon nach einem flüchtigen Blick bemerken, dass Ihr Code demnächst die Show ruinieren wird.

Zu große Dateien

Anfänger neigen dazu, alles in eine einzige große Datei zu schreiben, was den Code unübersichtlich macht. Aber es hilft auch nichts, den Code sorgsam auf zwölf Dateien zu verteilen, wenn diese Dateien ihrerseits riesig sind. Wann ist eine Datei zu groß? Als Faustregel reicht das Wissen, dass fünf Bildschirmseiten viel sind. Eine Datei, die zehn wichtige Funktionen enthält (also keine kleinen Helferlein, die nur aus dieser Datei selbst aufgerufen werden), ist ebenfalls ziemlich groß.

Problem: Eine große Datei verlockt zur schludrigen Gliederung des Codes. Neu hinzukommende Funktionen werden einfach oben oder unten angeflickt. Man verbringt viel Zeit damit, in der großen Datei umherzuscrollen. Während sinnvoll in kleine Dateien gegliederter Code leicht zu erfassen ist, enthält eine einzige große Datei Code mit vielen Aufgaben. Der Code wird dadurch schwer zu verstehen.

Abhilfe: Verteilen Sie den Code auf mehrere Dateien. Dabei ist eine Entwicklungsumgebung (siehe Kapitel 20) eine Hilfe. Eine gute Faustregel ist hierfür: ein Aufgabenbereich pro Datei. So werden zum Beispiel alle Funktionen zur Datumsberechnung in einer Datei zusammengefasst, statt sie in der Nähe derjenigen Funktionen zu halten, die sie benötigen. Wichtig ist eine logische, leicht nachvollziehbare Verteilung des Codes auf verschiedene Dateien.

In vielen objektorientierten Sprachen gilt die Regel: jede Klasse lebt in ihrer eigenen Datei. Oftmals sogar in zwei Dateien: eine für die nach außen sichtbaren Interfaces und eine für die privaten Details, die Implementierung, die andere Klassen nichts angeht. Für die genaue Benennung dieser Dateien geben die verschiedenen Sprachen jeweils eigene Konventionen vor.

Beispielsweise wird in C++ eine Klasse in einem Headerfile deklariert (man verkündet der Welt, dass es sie gibt), und in einem Sourcefile steht der eigentliche Code. C++ ist sehr stark von Konventionen und weniger von festen Regeln geprägt. Die Konventionen für die Dateinamen sind, dass sie in CamelCase geschrieben sind und Source- und Headerfile sich nur in der Dateiendung unterscheiden, also

SpaceShip.h für das Headerfile und SpaceShip. cpp für das Sourcefile.

Von diesen Konventionen gibt es bei C++ leider verschiedene. Daher könnte das Headerfile auch SpaceShip. hpp oder SpaceShip.hh heißen und das Sourcefile SpaceShip. cc oder SpaceShip. c++.

Sehr lange Funktionen

Alles, was nicht mehr auf eine Bildschirmseite passt, ist zu lang. Roben C. Martin empfiehlt in »Clean Code« ein Maximum von zehn Zeilen. Alles über 50 Zeilen ist definitiv viel zu lang. Wer Funktionen von mehr als 100 Zeilen Länge schreibt, wird im nächsten Leben als zwei Meter zwanzig großer Mensch wiedergeboren und muss ganzjährig in Hotelbetten übernachten.

Problem: Lange Funktionen sind unübersichtlich, schwer verständlich und deuten darauf hin, dass ihr Autor nicht gründlich genug nachgedacht hat. Sie erledigen häufig mehr als eine Aufgabe.

Abhilfe: Eine Funktion sollte genau eine Aufgabe erledigen. Sie können das leicht testen, indem Sie die Aufgabe der Funktion einer anderen Programmiererin erklären. Wenn in dieser Erläuterung das Wort »und« vorkommt, sollten Sie sich überlegen, wie Sie die Funktion aufteilen könnten. Wenn die Funktion beispielsweise einen Datensatz berechnet, sollte sie ihn nicht auch noch auf die Platte schreiben, das ist die Aufgabe einer anderen Funktion. Hat man sehr lange Funktionen, die alles Mögliche tun, dann sollte man aufschreiben, welche Belange in ihnen behandelt werden, und dann zusammengehörige Teile in eigene Funktionen auslagern (siehe Kapitel l5).

Ausführliche Kommentare an einem Codeblock sind oft ein Anzeichen dafür, dass der kommentierte Abschnitt als eigene Funktion besser dran wäre. Es lohnt sich, selbst einzelne Zeilen auszulagern, wenn sie erklärungsbedürftig und deshalb mit längeren Kommentaren versehen sind.

Auch die Zweige von Bedingungen und Schleifen können oft zu selbstständigen Funktionen befördert werden. Im Fall von Bedingungen stehen die Zweige dann in einer Funktion nah beieinander, if und else sind gleichzeitig sichtbar. So bleibt die Funktion, die die Bedingung enthält, übersichtlich und spiegelt die Gesamtzusammenhänge besser wider.

Längere Berechnungen für lokale Variablen und Codebereiche, die Strings zusammenbauen, sind oft ebenfalls Kandidaten für eigene Funktionen.

Zu breite Funktionen

Als Leser dieses Buchs sollten Sie versuchen, sich auf allerhöchstens drei Einrückungsebenen zu beschränken; zwei sind noch besser. Aber auch eine einzige Einrückung, die -lL'h dafür über viele Zeilen erstreckt, ist kein gutes Zeichen.

Problem: Ebenso wie zu lange sind auch zu breite Funktionen unübersichtlich und schwer verständlich und deuten darauf hin, dass ihr Autor nicht gründlich genug nachgedacht hat. Die Breite ist nicht nur ein Formatierungsproblem, sondern zeigt, dass der innerste (= weit eingerückte) Code entweder in zu vielen Schleifen oder zu vielen Bedingungen steckt.

Abhilfe: Häufig hilft es, sich zu überlegen, wie man ein Problem in noch kleinere Probleme aufspalten kann, die man jeweils in eigene Funktionen steckt. Irgendwann werden die Probleme dann unteilbar und ihre Lösung ist häufig genug eine triviale, leicht zu verstehende Funktion. Wenn der Code quadratisch wird, also weder in der Länge noch in der Breite zu viel auf einmal erledigen will, ist das ein gutes Zeichen:

»Ein schlechtes Zeichen ist bei studentischem Code, wenn er sehr lang wird. Sehr einfache, kurze Zeilen mit wenig Abhängigkeiten in der Zeile, dafür dann halt alles ineinandergeschachtelt. Viele von den Variablen sind temporär und werden nie wieder verwendet, da kann man schnell aufräumen, das macht die Sache übersichtlicher. Wenn die Studenten das gelernt haben, produzieren sie im nächsten Schritt 200-Zeichen-Zeilen, in denen dann alles gemacht wird. Da muss man dann den richtigen Tradeoff finden. Am Ende nimmt der Code quadratische Form an.<<

Roland Krause, Bioinformatiker

Beispielsweise ist die folgende Zeile, die eine Schleife einleitet, relativ lang und schwer verständlich:

for (var index = customerRecords.GetlnitialValueQ; index <= customerRecords. GeUastValueQ; index += customerRecords.GetlncrementQ) {

II Code, der in der Schleife ausgeführt wird

}

Man könnte den Code auf zwei Arten quadratischer machen. Zum einen, indem man Zeilenumbrüche einfügt. Das geht nicht in allen Sprachen, macht aber den Code optisch weniger breit:

for (var index = customerRecords.GetInitialValue(); index <= customerRecords.GetlastValueQ; index += customerRecords.GetlncrementQ) {

II Code, der in der Schleife ausgeführt wird

}

Noch übersichtlicher ist es, durch temporäre Variablen an dieser Stelle den Schleifenkopf zu entzerren:

var start = customerRecords.GetlnitialValueQ;

var end = customerRecords.GetlastValueQ;

var increment = customerRecords.GetlncrementQ;

for (var index = start ; index <= end; index += increment ) {

II Code, der in der Schleife ausgeführt wird

}

Der Code ist jetzt nicht nur quadratischer, sondern der Schleifenkopf ist durch die Benennung der temporären Variable auch leichter verständlich geworden.

Tief verschachtelte if/then-Bedingungen

Verwandt mit dem schlechten Zeichen der »tiefen Klammerebenen«, zeigt sich dieser unangenehme Zeitgenosse besonders dann, wenn zwei oder mehr Bedingungen den weiteren Programmablauf beeinflussen. Sich zu sagen, »also wenn A und B wahr sind, dann muss das passieren, wenn nur A, dann jenes und wenn keines wahr ist, dann einfach weitermachen«, ist einfach, und es in Code umzusetzen auch. Leider entsteht dabei Code, der viel leichter zu schreiben als zu lesen ist.

Problem: Hängt der Ablauf des Programms an mehreren Bedingungen, dann muss man diese im selben Codeblock prüfen. Das führt schnell zu Verschachtelungen, deren Sinn später schwer zu begreifen ist.

Ein Beispiel:

if (test_database() == OK ){ if (load_file() == OK ){

/* Code für den Normalzustand */

} else {

/* Fehlerbehandlungscode load_file() */

}

} else {

/* Fehlerbehandlungscode test_database() */

}

Eigentlich relativ einfacher Code mit drei Codezweigen für den Fall, dass test_data-base() und function_B() den Status »OK« zurückgeben, und für die Fälle, dass eine der beiden Funktionen einen Fehlerstatus liefert. Der Code ist in verschiedener Hinsicht problematisch:

•    Die beiden Zweige für >>test_database() liefen >OK<« bzw. >>test_database() liefert einen FehlerstatuS« sind durch den Einschub getrennt.

•    Der Rückgabewert von test_database() wird zuerst getestet, aber der Fehlerfall für load_file() zuerst behandelt.

•    Das if/else-Gestrüpp ist mit einem Blick nicht zu erfassen.

Abhilfe: Alle Permutationen der Bedingungen in einzelne if/else if-Zweige aufgliedern oder gleich in eigene Funktionen auslagern. Dadurch wird der Code zwar etwas länger, aber übersichtlicher.

io wäre das Beispiel verständlicher:

if (test_database() == OK) { parse_file(); } else { /* Fehlerbehandlungscode test_ database() */ } function parse_file() {

if (load_file() == OK ){

/* Code für den Normalzustand */

} else {

/* Fehlerbehandlungscode load_file() */

}

}

Die Tests und Fehlerbehandlung für test_database() und load_file() haben wir entzerrt, indem wir das ganze Filehandling in eine eigene Funktion ausgelagert haben.

Tief verschachtelte if/then-Bedingungen |    169

Mitten im Code auftauchende Zahlen

Nicht alle Zahlen passen sich so schön in unser Dezimalsystem ein wie var lengthMM = lengthM * 1000. Hier kann man, auch ohne das Programm zu kennen, vermuten, dass eine Länge von Metern in Millimeter übersetzt wird. Auch eine Multiplikation mit 3.14159 werden viele noch als Multiplikation mit 11: erkennen. Steht hingegen im Code 2.71828, bemerken schon deutlich weniger Leser Ihres Codes, dass Sie irgendwas mit Logarithmen im Sinn hatten. Und was könnte der Sinn der Zahl 86400 sein, die als Konstante in einer Multiplikation verwendet wird1 In der englischsprachigen Literatur werden solche Zahlen auch als magic numbers bezeichnet.

Problem: Man wird sich schon kurze Zeit später nicht mehr daran erinnern, dass da »86400« steht, weil der Tag 86.400 Sekunden hat.1 Wenn man beim Design eines UserInterface einen Rand von 30 Pixeln definiert hat und später zu dem Schluss kommt, dass er doch lieber 50 Pixel breit sein soll, muss man alle Stellen suchen und ändern, an denen »30<< vorkommt. Und wenn der Code von magic numbers durchsetzt ist, kann es leicht sein, dass dieselbe Zahl in mehreren unterschiedlichen Funktionen vorkommt. Wenn Sie jetzt die Breite des Randes ändern wollen und dabei nicht aufpassen, statten Sie nebenbei einige Kalendermonate mit einer Länge von 50 Tagen aus.

Abhilfe: Viele Codeanalysetools erkennen und bemängeln die Verwendung von magic numbers. Stecken Sie Zahlen in Variablen oder, noch besser, falls Ihre Programmiersprache das unterstützt, in Konstanten, also beispielsweise const secondsPerDay = 86400;. Diese Konstanten sollten Sie dort unterbringen, wo man sie leicht findet - das ist insbesondere dann hilfreich, wenn die Zahlen später einmal geändert werden sollen. Wenn Sie in einer objektorientierten Sprache programmieren, sollten Sie sich überlegen, ob diese Werte vielleicht besser Eigenschaften eines Objekts (Member-Variablen) sein sollten. Ansonsten sollten Sie sich angewöhnen, feste Zahlenwerte typischerweise an den Anfang einer Datei zu schreiben. Seltene Werte wie die Anzahl der Sekunden eines Tages sollten lieber nicht als Ergebnis im Code stehen, sondern dem Compiler oder Interpreter als Aufgabe überlassen werden, wenn sie sich aus einer Berechnung herleiten lassen. Man wird es später leichter verstehen, wenn 24 * 60 * 60 im Code steht.

Komplexe arithmetische Ausdrücke im Code

Ähnlich wie Zahlen, die ohne Kontext im Code auftauchen, sind auch größere Berechnungen ohne Kommentare oder hilfreiche Gliederung leichter zu schreiben als später nachzuvollziehen. Insbesondere wird sich der Leser schwer tun, sie auf Richtigkeit zu prüfen.

l

Problem: Zeitgenossen ohne Liebe zur Mathematik können Formeln noch schwerer lesen und verstehen als Programmcode, das färbt auch auf in Programmcode umgesetzte Formeln ab. Formeln sind eine stark kondensierte Art, eine Berechnung auszudrücken. Werden sie zu lang, fangen viele Leser an, Teile zu überfliegen, oder blenden die ganze Formel aus. Das kostet speziell bei der Fehlersuche viel Zeit.

Abhilfe: Vereinfachen Sie den Code und/oder lagern Sie ihn in eine Funktion aus, deren Name idealerweise gleich beschreibt, was dort vor sich geht. Komplexität lässt sich reduzieren, indem Zwischenergebnisse temporären Variablen zugewiesen werden. Wenn diese Variablen gut benannt sind, dokumentieren sie gleichzeitig die einzelnen Rechenschritte.

So nicht:

kiloWattPerWeek = wattPerYear I (52 * 1000)

So schon eher:

weeksPerYear = 52

wattPerWeek = wattPerYear I weeksPerYear kiloWattPerWeek = wattPerWeek I 1000

Globale Variablen

Im Code tauchen viele globale Variablen auf, also Variablen, die über den gesamten Code hinweg sichtbar sind, nicht nur innerhalb einzelner Funktionen. (Mehr zum Geltungsbereich von Variablen steht im Kapitel 26.)

Problem: Viele ungeübte Programmierer lernen globale Objekte oder Variablen zu schätzen und verwenden sie gerne und reichlich für die Kommunikation zwischen verschiedenen Programmteilen. Beispielsweise benötigen die meisten Programme Einstellungen, die für das ganze Programm zentral verwaltet werden, zum Beispiel die Zugangsdaten für die Datenbank. Egal, ob der Benutzer solcheglobalen Einstellungen ändern kann oder sie im Code festgeschrieben sind: In jedem Fall muss sichergestellt sein, dass unterschiedliche Programmteile immer dieselben Werte verwenden.

Die einfachste Lösung für dieses Problem sind globale Variablen. Sie sind zunächst praktisch, machen den Code aber fehleranfällig und schwer wartbar. Ein konkreter Nachteil: Leicht legt man versehentlich in einer Funktion eine lokale Variable mit demselben Namen wie eine global bereits existierende an und löst damit unbeabsichtigte und -chwer zu debuggende Veränderungen an der falschen Variable aus. Ein etwas abstrakterer Nachteil: Die Existenz von globalen Variablen verleitet die Entwicklerin, sich sowohl in ganz tiefen Architekturschichten (zum Beispiel in Helferfunktionen, die die Verbindung zur Datenbank aufbauen) als auch in höheren (zum Beispiel im User-Interface) auf -ie zu verlassen. Auf Dauer wird der Code an allen Stellen von irgendwelchen globalen Variablen abhängig sein, die an ganz anderen Stellen definiert wurden. Die Folge ist, dass man Code schlecht in anderen Projekten wiederverwenden kann und er durch das Unterwandern der Modularität schwerer verständlich ist.

Abhilfe: Verwenden Sie am besten überhaupt keine globalen Variablen, auch wenn sie so schön praktisch sind. Wenn eine mühsam zu beschaffende Information an mehreren Stellen gebraucht wird, legt man eine get()-Funktion an, die nur beim ersten Mal die teure Operation ausführt und bei allen weiteren Aufrufen immer nur das Ergebnis vom ersten Mal zurückgibt.

Wenn man auch nur ganz vage über sein Programm als Module in unterschiedlichen Schichten nachdenkt (zum Beispiel von der niedrigsten Schicht der Helfer über Datenbankfunktionen und Berechnungen bis hin zu User-Interface-Funktionen als höchster Schicht), dann hat man einen großen Schritt getan, ein besserer Programmierer zu werden.

Reparaturcode

Das Ergebnis einer Berechnung ist nicht das, was es eigentlich sein soll, es hat zum Beispiel einen >>Off-by-one<<-Fehler, d. h., es ist konsistent um 1 zu groß oder zu klein. Statt das grundlegende Problem in der Berechnung selbst zu lösen, haben Sie Code geschrieben, der das falsche Ergebnis danach korrigiert.

Problem: Auch wenn solche Flickschusterei häufig gutgeht, ist sie doch ein schlechtes Zeichen. Sie verrät Lesern das eigentliche Problem, nämlich, dass Sie sich nicht trauen, die Berechnung von Grund auf neu zu schreiben oder so zu korrigieren, dass auf Anhieb das richtige Ergebnis herauskommt. Auch wenn das im einen oder anderen Fall folgenlos bleibt, lässt es darauf schließen, dass Sie sich auch an andere, ernstere Bugs nur ungern heranwagen.

Ein einfaches Beispiel: Sie wollen aus einem Array einen String machen, der die einzelnen Array-Elemente durch Kommata getrennt enthält.

var elements = [1, 5, 7, 3]; var formattedStr = "";

for (var i = 0; i < elements.length(); i++) {

formattedStr = formattedStr + elements[i] + ", ";

}

formattedStr = formattedStr.subString (o, formattedStr.length -2);

Sie bauen also erst einen String zusammen, der so aussieht: »1, 5, 7, 3, << und kappen dann die letzten zwei Zeichen, um zum gewünschten Ergebnis zu gelangen: >> 1, 5, 7, 3«. Das funktioniert in diesem Fall, könnte aber vermieden werden:

var elements = [1, 5, 7, 3]; var formattedStr = '"';

for (var i = 0; i < elements.length(); i++) { if (i != o) {

formattedStr = formattedStr + ", ";

}

formattedStr = formattedStr + elements[i];

}

(Falls Ihnen an dieser Stelle auffällt, dass auch das noch nicht die optimale Lösung ist: Nur Geduld, sie folgt im nächsten Punkt.)

Wenn Sie, wie im zweiten Beispiel, in der Schleife zwischen dem ersten und allen anderen Elementen unterscheiden, dann können Sie gezielt ein Komma weniger anflanschen. Genauso gut könnte man in der if-Bedingung testen, ob i = elements.length() - 1 ist, das liest sich nur schwerer.

Abhilfe: Nehmen Sie solche folgenlosen Fehler ernst. Sie erkennen leicht selbst, wo Sie Berechnungsergebnisse mit dem Hammer zurechtgedengelt haben.

Ersetzen Sie solche Stellen durch besseren Code. In Sprachen, die for each-Schleifen kennen, können Sie häufig auch diese einsetzen, dann können Sie sich den Array-Index sparen und sind das ganze Problem los.

Eigene Implementierung vorhandener Funktionen

Der Code enthält Funktionen, die ähnlich sind und häufig auch ähnlich heißen wie solche, die die Sprache schon bereitstellt.

Problem: Als Einsteiger haben Sie nicht den Überblick über all die Funktionen, die die Sprache oder von Ihnen verwendete Bibliotheken bereitstellen, und das ist auch okay. Im Laufe der Zeit führt das leider dazu, dass Ihr Code Funktionen enthält, die Ähnliches leisten wie vorhandene, aber subtil anders heißen oder anders aufgerufen werden.

Da Sie wahrscheinlich mehr Fehler machen als die Erfinder der Sprache Ihrer Wahl, und da deren Code noch von anderen Entwicklern geprüft und in der Praxis erprobt ist, wird Ihre Implementation schlechter sein als die vorhandene.

Das Beispiel von oben, in dem aus einem Array ein String gebaut werden sollte, kann man in vielen Sprachen noch drastisch vereinfachen. Statt das Array Stück für Stück an den String anzukleben

var elements = [ 1, 5, 7, 3]; var formattedStr = “";

for (var i = 0; i < elements.length(); i++) { if (i != 0) {

formattedStr = formattedStr + ", ";

}

formattedStr = formattedStr + elements[i];

}

nutzen Sie besser eine Funktion wie join():

var elements = [1, 5, 7, 3];

var formattedStr = elements.join (“, ");

Abhilfe: Hier hilft nur Lesen. Am besten ein »In a Nutshell«-Buch von O’Reilly zu Ihrer ''prache, als zweitbestes jede Menge fremden Code. Wann immer Sie Basisfunktionen wie sortieren, Aufspalten von Strings, Filtern, formatierte Ausgabe oder Typumwandlung betreiben, sollten Sie nachsehen, ob es dafür nicht schon etwas gibt (siehe auch Kapitel l9).

Eigene Implementierung vorhandener Funktionen |    173

Sonderfälle

Ihr Code gibt eigentlich immer das richtige Ergebnis zurück, aber nur eigentlich. Immer wenn er einen String mit genau 71 Zeichen behandeln soll, geht es schief. Deshalb haben Sie eine Abfrage eingebaut, die diesen einen Sonderfall anders behandelt.

Problem: Es gibt einen Grund für diesen Fehler. Und mit ziemlich hoher Wahrscheinlichkeit verursacht dieser Grund nicht nur den einen Fehler, den Sie kennen, sondern noch ein paar andere. Das Symptom will Ihnen eigentlich helfen, aber Sie tapezieren drüber und vergessen danach, dass es existiert. Außerdem funktioniert Ihre Fehlerbehebung wahrscheinlich gar nicht, und bald scheint der hässliche Fleck wieder durch die Tapete. Über kurz oder lang ist der ganze Code zugekleistert mit Sonderfällen, die Sie dann bei allen Veränderungen und Erweiterungen wieder berücksichtigen müssen.

Abhilfe: Finden Sie das eigentliche Problem. Beheben Sie es.

Inkonsistente Schreibweisen

Funktionen oder Variablen sind mal in CamelCase, mal underscore_separated, mal deutsch und mal englisch benannt. Es werden abwechselnd Tabs und Leerzeichen zur Einrückung verwendet, um Operatoren stehen manchmal Leerzeichen und manchmal nicht, zum Beispiel $i=l und $i = 1.

Problem: Hier hat sich entweder eine Auseinandersetzung zwischen mehreren Programmierern abgespielt, oder aber der Autor tackert seit Jahren nur hastige Ergänzungen an den Code. Oder er war gedanklich schon beim Feierabendbier. Gleichgültigkeit in Kleinigkeiten ist ein Anzeichen dafür, dass wahrscheinlich auch in wichtigeren Fragen geschlampt wurde. Es gibt viele unterschiedliche Konventionen, aber keine Entschuldigung dafür, mehreren gleichzeitig zu folgen oder sich ganz neue auszudenken.

Abhilfe: Suchen Sie in den FAQs zur venvendeten Sprache nach Konventionen und orientieren Sie sich daran. Oder lesen Sie Kapitel 4 und Kapitel 5, entscheiden Sie sich für eine Konvention und bleiben Sie dann dabei. Eine Entwicklungsumgebung vereinfacht das Geradeziehen eines solchen Durcheinanders.

Funktionen mit mehr als fünf Parametern

Ein Beispiel aus der Dokumentation von tcpdf. org, einer Bibliothek zur Erzeugung von PDFs:

Image ($file, $x='', $y='', $w=o, $h=O, $type='', $link='’, $align='', $resize=false, $dpi=300, $palign='’, $ismask=false, $imgmask=false, $border=0, $fitbox=false, $hidden=false, $fitonpage=false, $alt=false, $altimgs=array()).

Problem: Lange Parameterlisten sind ein Zeichen dafür, dass hier jemand versäumt hat, Daten nach Zusammengehörigkeit in Objekte oder Arrays zu stecken. Oder es weist da-

rauf hin, dass nicht genügend Funktionen existieren, die man nach Werten fragen kann. Zusätzliche Parameter verursachen zusätzliche Arbeit fürjeden, der die Funktion aufrufen möchte. Roben C. Martin gibt in »Clean Code« null als ideale Parameteranzahl an, ein Parameter ist etwas schlechter als keiner, zwei sind etwas schlechter als einer, drei sollte man vermeiden und für alles über drei braucht man sehr gute Gründe. Weil es aber auch in allgemein akzeptiertem und häufig eingesetztem Code immer wieder Funktionen mit erstaunlich vielen Parametern gibt, haben wir die Grenze hier mit fünf etwas höher gelegt.

Abhilfe: Sehen Sie nach, ob die Empfängermethode die Berechnung vielleicht selbst anstellen kann. Oder befördern Sie den Teil des Codes, der den Parameter erzeugt, zu einer eigenen Funktion. Oder geben Sie der Funktion statt diverser einzelner Parameter gleich ein ganzes Objekt bzw. ein Array mit, das zusammengehörige Werte enthält.

Beispielsweise hätte die oben zitierte Funktion davon profitiert, wenn der Entwickler sinnvolle Defaultwerte für alle PDF-Einstellungen definiert hätte. x- und y-Werte wären dann 0, wenn derjenige, der die Funktion aufruft, sie nicht anders definiert. Neben dem Dateinamen würde die Funktion dann nur noch mit einem assoziativen Array aufgerufen, das die abweichenden Einstellungen enthält:

$settings = {

$dpi => 600,

$fitonpage => true

} ;

$img = Image ($file, $settings);

Code-Duplikation

Identischer oder sehr ähnlicher Code taucht mehrfach im Code auf.

Problem: Kopierter Code ist unpraktisch, weil spätere Änderungen an mehreren Stellen vorgenommen werden müssen. Das vergisst man bis dahin üblicherweise. Außerdem verwirren die ähnlichen Passagen Leser des Codes so, wie Reihenhaussiedlungen die Orientierung nicht gerade befördern. Man verbringt dann viel Zeit damit, auf der Suche nach der richtigen Stelle im Code herumzuirren.

Abhilfe: Codewiederholungen sind ein Anzeichen für Denkfaulheit. Ein bisschen Denkfaulheit aber ist eine nützliche Sache, und deshalb gilt hier die »Rule of Three«2: Es ist in Ordnung, Code zweimal zu verwenden. Sobald Sie sich dabei ertappen, denselben Code an eine dritte Stelle zu kopieren, sollten Sie über eine Vereinheitlichung nachdenken.

Beginnen Sie die Vereinheitlichung, indem Sie den gemeinsamen Teil der mehrfachen Funktionen in eine eigene Funktion auslagern. Diese Funktion wird dann von den bisherigen Funktionen aufgerufen, in denen jetzt nur noch für die jeweilige Aufgabe spezifischer Code steht.

Zweifelhafte Dateinamen

Die Dateien, auf die sich ein Programm verteilt, tragen Namen wie SuperActionMasterMainLoop, AllMiscellaneousHelperFunctions und ExtraMagicHacks.

Problem: Dateinamen sind wie ein Inhaltsverzeichnis. Wenn dieses Inhaltsverzeichnis versagt, muss sich der Leser die ganze Struktur des Programms erst in mühsamer Handarbeit erschließen. Außerdem ist es um die Namen der Methoden und Variablen in solchen Dateien oft auch nicht besser bestellt.

Abhilfe: Lesen Sie Kapitel 5 und beherzigen Sie es.

Leselabyrinth

Es gibt in den meisten modernen Sprachen keine Notwendigkeit, eine bestimmte Reihenfolge der geschriebenen Funktionen einzuhalten, daher ist die Versuchung groß, eine Funktion einfach an die Ste11e zu schreiben, wo man den Texteditor gerade offen hat. Verwandte Funktionen, die zu anderen Zeiten geschrieben wurden, können unter Umständen an ganz anderen Stellen stehen.

Problem: Es ist sehr schwer, beim Lesen des Codes zu verstehen, wo das Programm anfängt und was in welcher Reihenfolge passiert.

Abhilfe: Strukturieren Sie das Programm so um, dass logisch zusammengehörende Teile nahe beieinander liegen. Folgen Sie dabei den Abhängigkeiten: Wenn eine Funktion drei andere braucht, versteht man den Code leichter, wenn man diese Funktionen nicht erst lange suchen muss. Ordnen Sie den Code außerdem grob nach seinem lnteressantheits-grad. Es gibt häufig sehr langweiligen Code in Helperfunktionen, der zwar vorhanden sein muss, aber niemanden interessiert, weil er beispielsweise nur Strings formatiert aneinanderschraubt. Verbannen Sie diesen Code ganz ans Ende der Datei. Code hingegen, der auch anderswo gebraucht wird, also z.B. das Interface einer Klasse oder die Deklarationen von Funktionen, die von außen sichtbar sein sollen, gehören an den Anfang der Datei bzw. des Codeblocks. Viele Funktionen treten paarweise auf: Die eine initialisiert beispielsweise eine Struktur oder ein Objekt, die andere gibt es wieder frei. Oder eine Funktion setzt eine Variable, die andere gibt den Zustand zurück. Diese Funktionen sollten Sie unmittelbar hintereinander platzieren, denn Änderungen an der einen Funktion erfordern sehr häufig auch Änderungen an der anderen, und die unmittelbare Nachbarschaft erinnert daran.

Ratlose Kommentare

Kommentare wie /* weiß auch nicht, warum der nächste Codeblock da stehen muss, aber wenn man ihn rausnimmt, gehts nicht mehr */ zeigen, dass der Autor seinen Code nicht richtig verstanden hat und sich auch nicht die Mühe macht, sich dieses Verständnis zu erarbeiten. Unverstandener Code enthält aber häufig Fehler, die sich im normalen Ablauf bisher nur nicht gezeigt haben.

Problem: Dieses spezielle schlechte Zeichen wirkt wie ein Appell des ursprünglichen Programmierers an die Umwelt, ihm auf die Sprünge zu helfen - aber es war niemand da. Also hat er den Code aus Angst, ihn zu verschlechtern, lieber nicht angefasst. Angst vor eigenem Code ist aber ein nahezu sicheres Zeichen, dass dieser Code nicht gut ist.

Gelegentlich werden solche Kommentare auch von anderen Programmieren angefügt, die den Code später lesen (/* TODO: was passiert hier, und vor allem, warum? */). Das ist dann eher ein Zeichen dafür, dass der Code schwer verständlich ist, nicht notwendigerweise, dass er schlecht ist.

Abhilfe: Wenn Sie solche Kommentare in Code vorfinden, sollten Sie misstrauisch werden. Müssen Sie diesen Code weiterentwickeln, lesen Sie ihn aufmerksam, um ein besseres Verständnis als der Originalautor zu entwickeln. Wenn es geht, sollten Sie ihn so umschreiben, dass er funktioniert, weil gewisse Bedingungen zutreffen, nicht obwohl.

Sehr viele Basisklassen oder Interfaces

Das Problem ist nur für klassenbasierte objektorientierte Sprachen wie C++ oder Java relevant. Eine Klasse sollte nicht mehr als zwei Basisklassen bzw. Interfaces haben. UserInterface-Klassen dürfen notfalls vier haben, aber das sollte Ihnen bereits Anlass geben, den Code mit erhöhter Aufmerksamkeit zu betrachten.

Problem: Dieses schlechte Zeichen ist ein lndiz dafür, dass Sie das Klassendesign noch einmal überdenken könnten. Die Probleme, die solches schlechtes Design mit sich bringt, sind vielfältig und haben gemeinsam, dass sie Änderungen am vorhandenen Code unnötig kompliziert machen.

Abhilfe: Wenn Sie überprüfen wollen, ob eine Klasse wirklich von einer bestimmten anderen Klasse abgeleitet sein sollte, hilft es, die beiden Klassennamen laut auszusprechen und dazwischen ein »is a« zu schieben. Wenn dabei merkwürdige Aussagen wie »Car is a Wheel« entstehen, sollten Sie von der Vererbung vielleicht Abstand nehmen. ''■tattdessen bietet es sich vielleicht an, die ehemalige Basisklasse als Typ einer Member-Variable der ehemalig abgeleiteten Klasse zu verwenden. Eine solche Beziehung können Nie als »has a« aussprechen: Car has a wheel.

Sehr viele Methoden oder Member-Variablen

Problem: Die Klasse ist schwer zu verstehen und schlecht zu warten. Wann >>sehr viel« und dann >>zu viel« beginnt, kann man nicht verbindlich festlegen. Wenn Sie Ihren Code mit einem gewissen zeitlichen Abstand betrachten und sich in der Vielzahl der Funktionen und Variablen nicht mehr zurechtfinden, dann sind es zu viele.

Abhilfe: Zunächst sollten Sie prüfen, ob die Klasse nicht in mehrere Klassen aufgespalten werden kann. Vielleicht versucht die Klasse, mehrere Probleme zu lösen, die besser von separaten, spezialisierten Klassen erledigt werden sollten? Sehen Sie nach, ob vielleicht bestimmte Member-Variablen nur als Gruppe sinnvoll sind. Oft treten diese Haufen auch an anderen Stellen gemeinsam auf. In so einem Fall lohnt es sich, sie in einer Klasse zusammenzufassen, in die dann oft auch ein Teil der Methoden verschoben werden kann.

Auskommentierte Codeblöcke und Funktionen

Problem: Sie sind häufig ein Indikator dafür, dass sich in irgendeiner Funktion unklare Fehler verbergen oder ein Refactoring nicht zu Ende geführt wurde (siehe Kapitel 15).

Abhilfe: Es ist nicht schlimm, wenn man eine Funktion erst einmal kopiert und dann die eine Version auskommentiert und die andere umbaut. Stößt man auf unklare Fehler, kann man die ursprüngliche Version wieder einkommentieren. So hat man einen schnellen Test dafür, ob die Fehler in der neuen Version lauern oder ob man etwa fehlerhafte Daten in die Datenbank geschrieben hat, die jetzt ihr Unwesen treiben. Tut die überarbeitete Funktion aber, was sie soll, dann ist es Zeit, die auskommentierte Version gnadenlos zu löschen. Wenn man ein Versionskontrollsystem besitzt, löscht man übrigens viel bereitwilliger, weil man sich jederzeit wieder auf den letzten funktionierenden Stand retten kann.

Browservorschriften

Wer sich schon einmal mit HTML, CSS und JavaScript beschäftigt hat, den Basiszutaten jeder schmackhaften Webapplikation, der weiß, dass unterschiedliche Browserhersteller Dinge unterschiedlich handhaben - und teilweise sogar Standards fehlerhaft auslegen. Das hat einer ganzen Generation von Entwicklern erhöhten Blutdruck beschert und macht es bis heute unangenehm kompliziert, Webapplikationen für alle wichtigen Browser zu erstellen (obwohl sich die Lage schon deutlich gebessert hat). Viele Entwickler gehen den einfachen Weg und versuchen, ihren Benutzern einen bestimmten Browser vorzuschreiben.

Problem: Das verärgert zum einen die Benutzer, die diesen Browser nicht benutzen dürfen (etwa am Arbeitsplatz), auf deren Betriebssystem er nicht läuft, oder die einfach keinen anderen Browser benutzen wollen, weil sie mit ihrer Wahl ganz zufrieden sind.

Abhilfe: Nutzen Sie Validatoren! Es gibt sie für alle wesentlichen Webtechnologien (siehe Kapitel 13). Testen Sie ausgiebig mit den wichtigsten Browsern. Spekulieren Sie nicht aufgrund Ihrer eigenen Vorlieben und denen Ihrer Freunde, welche das sein könnten, sondern nutzen Sie aktuelle Statistiken, die Sie z.B. im Wikipedia-Eintrag »Webbrowser« verlinkt finden. Verfallen Sie darüber nicht versehentlich ins andere Extrem, indem Sie versuchen, es allen recht zu machen. Mit Support für seltene Browser reiben Sie sich auf und verschwenden Ihre Arbeitskraft am falschen Ort.

Um die Unterschiede in den javaScript-Fähigkeiten der verschiedenen Browser auszugleichen, sollten Sie sich mit Bibliotheken wie jQuery, Underscore oder Prototype beschäftigen. Zwar werden die JavaScript-lmplemcntierungen in ihren Fähigkeiten ungefähr seit 2010 sehr viel ähnlicher, aber es gibt trotzdem weiterhin Unterschiede und möglicherweise müssen Sie auch ältere Browser unterstützen.

Es gibt eine ganze Reihe von JavaScript-Bibliotheken, die nur dafür existieren, Unterschiede zwischen Browsern auszugleichen und auf älteren Versionen Fähigkeiten nachzurüsten; sie werden als »Polyfills« bezeichnet. Benötigen Sie also ein bestimmtes Browserfeature, dann sollten Sie im Web suchen, ob Sie für Benutzer älterer Browser eine Polyfill-Bibliothek einbinden können.

1

Xäherungsweise, aber bitte beachten Sie Kapitel 17 und chreiben Sie keine Kalenderfunktionen, die eine Tageslänge von 86.400 Sekunden fest voraussetzen.

2

Erstmals beschrieben in Martin Fowlers »Rcfactoring«.