Adam Barr: The Problem With Software
Adam Barr: The Problem With Software -
Why Smart Engineers Write Bad Code.
Adam Barr arbeitete 20 Jahre lang bei Microsoft. In diesem Buch beschreibt er die Entwicklung, die die Software-Industrie von den ersten Tagen an bis heute genommen hat. Es soll kein Versuch sein, die komplette Geschichte nachzuzeichnen. Der Autor ordnet die Kapitel chronologisch und legt dabei seine persönlichen Erfahrungen als Programmierer dar. Er konzentriert sich auf einige spezifische Momente, die besonders wichtig und repräsentativ waren.
Kapitel 1 - Die frühen Tage (Early Days)
Der Vater des Autors habe ihn gelegentlich an Samstagen zu der McGill Universität mitgenommen. Dort habe er auf einem Wang Minicomputer Spiele wie Bowling, Football und Star Trek gespielt. Die erste Software, die er geschrieben habe, sei für einen Hewlett-Packard Taschenrechner gewesen (HP-19C). Das sei seine erste Erfahrung mit der Assemblersprache gewesen.
Gegen Ende 1980 habe er sich über einen Fernschreiber, der mit dem zentralen Großrechner der McGill-Uni verbunden war, die Programmiersprache WATFIV beigebracht, eine Version von Fortran. 1982 habe seine Familie einen IBM PC gekauft, bei dem die Programmiersprache BASIC dabei gewesen war.
Die Hauptquelle seines Wissens über BASIC sei das gedruckte Bedienungshandbuch gewesen, das mit dem IBM PC ausgeliefert wurde. Es habe kurze Code-Ausschnitte enthalten, von denen jeder die richtige Syntax und die Benutzung eines Teils der Sprache behandelte. Anhand dessen habe er alles selber herausfinden müssen. Das Lesen der Handbücher sei der einzige Weg gewesen, wie man sich das Programmieren beigebracht habe. Keiner habe sich Gedanken darüber gemacht, warum eine Code-Sequenz auf eine bestimmte Art geschrieben worden sei, solange die funktionierte. Die meisten Programmierer schrieben Code zur eigenen Benutzung. Sie behielten die Funktionsweise des Programms in einer Art mentalem Bild im Kopf und machten sich keine Gedanken darüber, wie leserlich der Code für Andere war. Der Autor selber habe sich jedenfalls darüber keine gemacht.
Die IBM PCs hatten, verglichen mit heute, unglaublich wenig Arbeitsspeicher. Es gab welche, die nur 16 kilobyte hatten. Deswegen sei es verständlich, dass man alle seine Variablen nur mit einem Zeichen benannt habe. BASIC habe Kommentare erlaubt, um zu erklären, was der Code täte, aber die seien als Platz verschwendender Luxus angesehen worden. Um Speicher zu sparen, haben manche der frühen BASICs sogar erlaubt, die Leerzeichen im Code wegzulassen.
BASIC habe es schwierig gemacht, große Programme zu schreiben, weil es nur erlaubt habe, seine eigenen (zur Sprache gehörenden) APIs aufzurufen und es dem Programmierer nicht ermöglichte APIs zu programmieren, die von anderem Code aufgerufen werden konnten. BASIC habe nur Subroutinen gekannt, die (ähnlich wie GOTO-Statements in Fortran) als Referenz eine Liniennummer benötigten. Und sie unterstützten keine Parameter, sondern referenzierten die Variablen. Alle Variablen in BASIC seien globale Variablen gewesen.
Die Programmiersprache BASIC, wie sie am häufigsten in den frühen 80er-Jahren existierte, sei dafür nicht gebräuchlich gewesen, den Code zwischen Leuten zu teilen, die sich nicht kannten und sich über Details nicht austauschten. Das sei ein grundlegendes Problem einer Programmiersprache, die Liniennummern als Ziele für Subroutinen und GOTO-Statements nutzt. Die Erfinder der Sprache, John Kemeny und Thomas Kurtz, hätten das Problem erkannt und bereits in den späten 70er-Jahren eine verbesserte Version von BASIC herausgebracht. Die hatte richtig benannte Subroutinen mit Parametern und anderen Veränderungen, die GOTOs meist unnötig machten.
Unglücklicherweise hätten die Versionen von BASIC, die mit den PCs ausgeliefert worden waren und mit denen die meisten Menschen die Sprache lernten, sich bereits in ihre eigenen Richtungen abgespalten. Die Erfinder von BASIC seien nicht glücklich gewesen mit dem IBM PC BASIC, weil es sie teilweise an frühere Versionen der Sprache erinnert habe.
Am Ende des ersten Kapitels erwähnt der Autor den holländischen Computerwissenschaftler Edsger Dijkstra. 1975 habe der geschrieben, dass es "Praktisch unmöglich ist, Studenten, die vorher BASIC ausgesetzt waren, beizubringen, wie man gut programmiert: als potentielle Programmierer sind sie geistig verstümmelt jenseits jeder Hoffnung auf Genesung." Ausserdem habe Dijkstra Fortran eine "infantile Störung" genannt. [Von mir aus dem Englischen übersetzt.]
Kapitel 2 - Ausbildung eines Programmierers (The Education of a Programmer)
1984 ging der Autor an die Princeton Universität um Informatik zu studieren. Es habe nur einen Kurs gegeben, bei dem es darum ging, wie man Software schreibt. Dort habe der Autor die Programmiersprache Pascal gelernt. Der große Vorteil von Pascal gegenüber dem BASIC der frühen 80er-Jahre sei gewesen, dass Pascal das Weitergeben von Parametern an Subroutinen (procedures) unterstützte. Die Variablen, die in einer Subroutine deklariert worden seien, waren lokale Variablen. So dass man sich darüber habe keine Gedanken machen müssen, ob nicht jemand anders die Variable bereits außerhalb der Subroutine mit demselben Namen deklariert hat. Das habe es möglich gemacht, eine Subroutine (procedure) aufzurufen, die jemand anders geschrieben hat, ohne Details darüber zu haben, wie er die Subroutine implementiert hat - die Basis dafür, Code in Schichten aufzubauen. Das sei mit dem IBM PC BASIC mit seinen Subroutinen, die auf Liniennummern basierten und bei dem alle Variablen global waren, nicht möglich gewesen.
Der Autor geht auf den Trend der Strukturierten Programmierung (structured programming) in Programmiererkreisen der 70er und 80er-Jahre ein. Er zitiert einige Bücher der damaligen Zeit und kommt zu dem Schluss, man könne den Unterschied zwischen strukturierter und unstrukturierter Programmierung auf das Loswerden von GOTO-Statements reduzieren.
Der Autor zitiert Dijkstra bezüglich GOTO und paraphrasiert: der Quelltext sei statisch, während der Zustand des Computers beim Ausführen des Quelltextes dynamisch sei. Diesen dynamischen Zustand nenne Dijkstra Prozess. Beim Lesen des Quelltextes müsse es so leicht wie möglich sein, im Kopf diesen Prozess (den Zustand aller Variablen und deren Bedeutung) zu behalten, während der Computer einen bestimmten Code (line of code) ausführt. Solange man der Sequenz der Instruktionen, den Selektionen (IF Statements) und den Iterationen (loops) folgen könne, sei das relativ einfach. Wenn man dem Code aber erlaube, beliebig zu einer anderen Stelle durch GOTO-Statements zu springen, sei es hart, den Zustand des Prozesses (des dynamischen Zustands des Computers beim Ausführen des Quelltextes) zu kennen, während der durch die angepeilte Stelle geht. Weil diese Stelle auf mehreren Wegen erreicht werden kann, und man nicht wissen kann, in welchem Zustand der Prozess an all den verschiedenen Punkten war, von denen aus der gesprungen kam.
Diese Programmierweise voller GOTO-Statements sei als "Spaghetti-Code" verhöhnt worden. Versuchte man dem Pfad des Code zu folgen, sei es gewesen, als würde man dem Strang einer einzigen Spaghetti-Nudel in einer Schüssel folgen wollen. Sie würde an unbekannte Stellen verschwinden und irgendwo anders wieder zum Vorschein kommen. Man hätte keine Klarheit darüber, was genau zwischendurch passiert sei, und ob man überhaupt noch demselben Strang folgen würde. Der Autor zitiert Kemeny und Kurtz, die Erfinder von BASIC. Sie hätten anerkannt, dass die Tatsache, dass jede Linie im Quelltext (line of code) eine Liniennummer hat - was also jede line of code zu einem potenziellen Ziel eines GOTO-Statements machte - der "eine sehr ernste Fehler" gewesen sei, den sie im Entwurf der Sprache gemacht hätten.
Der Autor fährt fort und erklärt, dass GOTO-Statements in der Programmierung durchaus ihren Sinn haben. Während höhere Programmiersprachen aus Selektion (IFs) und Iteration (loops) aufgebaut seien, operiere die Assemblersprache auf einer niedrigeren Ebene. Die führe Operationen an Registern im Prozessor aus, vergleiche die und springe an andere Stellen im Programm. Dieses "Springen" sei nichts anderes als ein GOTO-Statement. Und ein Konstrukt wie IF in einer höheren Programmiersprache sei aus GOTO-Statements (jumps) in der Assemblersprache aufgebaut.
In dem Pascal-Einführungskurs habe der Autor das Weitergeben von Parametern an eine benannte Subroutine (procedure) zu schätzen gelernt. Die anderen Kurse des Studiums behandelten speziellere Themen: Compilerbau, Funktionsweise eines virtuellen Speichermanagers und wie dreidimensionale Graphik auf ein zweidimensionales Display projiziert werden kann. Alles interessante Themen, aber die Kurse, die diese Themen behandelten, fokussierten sich auf die speziellen Algorithmen, die für das jeweilige Problem gebraucht wurden. Da der Autor in diesen Bereichen später in seiner Arbeitskarriere aber nicht gearbeitet habe, habe er auf dieses Wissen in seiner täglichen Arbeit nicht zurückgreifen können. Niemand habe dem Autor und seinen Kollegen beigebracht, wie man große Programme entwirft und die bis zu einem bestimmten Termin (deadline) zum Laufen bringt. Sie hätten aber Aufträge bekommen, die einerseits große Programme benötigten, andererseits aber auch bis zu einem bestimmten Termin funktionierten mussten. Und sie hätten versucht, so gut sie konnten, dies zu realisieren.
Im zweiten Jahr des Studiums habe der Autor zum ersten Mal einen Kurs belegt, in dem er die Programmiersprache C benutzt habe, die er dann letztlich die meiste Zeit sowohl seiner Uni- als auch Berufskarriere benutzt habe. Der Professor habe nur die Ziele der ersten Aufgabe erklärt. Auf eine zögerliche Nachfrage eines Studenten, wie die denn die Programmiersprache C lernen sollten, habe der Professor auf das Buch The C Programming Language der beiden Erfinder der Programmiersprache C verwiesen, Brian Kernighan und Dennis Ritchie. Der Autor habe C gelernt, indem er sich die Beispiele darin angeschaut und die darunterliegende Motivation zu ergründen versucht habe. Er habe Dinge ausprobiert und, falls die nicht funktionierten, sie hinzukriegen versucht. Derselbe Prozess, den er benutzt habe, um vier Jahre zuvor IBM PC BASIC zu lernen.
Der Autor habe nur einen Kurs gehabt, der das Anpassen eines Programms beinhaltete, das jemand anders geschrieben hat. Genau das täte aber ein professioneller Programmierer die große Mehrheit seiner Zeit. Der Autor sei darauf unvorbereitet gewesen, sich mit einem großen Programm hinzusetzen und zu überlegen, was der ursprüngliche Autor sich gedacht hat.
Der Code aus seiner Zeit in Princeton sei eine Projektion seiner BASIC-Erfahrung auf C: kurze Variablennamen, die ihre Bedeutung nicht erklären, keine Kommentare, um zu erklären, was vor sich geht oder verschiedene Bereiche des Programms zu skizzieren, und immer wieder Code, der in eine shared function (ein Begriff von C für eine API) gehört hätte. Der Code habe funktioniert und einem einzigen Zweck gedient: eine Note für einen Kurs einzufahren und nie wieder angeschaut zu werden.
All diese Geschichten über seine Bildung hätten eine Gemeinsamkeit: in all den Fällen habe er sich das Wissen selber beigebracht. Obwohl er sein Informatikstudium erfolgreich abgeschlossen habe, habe ihm die Weisheit bitter gefehlt, die er dann durch Erfahrung während seiner Programmierkarriere erlangt habe. Alle Programmierer hätten es sich selber beigebracht: die das Internet entwarfen, die Windows konzipierten und auch diejenigen, die die Software schrieben, die in einer Mikrowelle lief - alle hatten sie sich das Programmieren selber beigebracht.
Mehrere seiner Klassenkameraden in Princeton arbeiteten dann schließlich auch bei Microsoft. Mit einem von ihnen, der auch in seiner Freizeit in der Highschool programmiert hatte, habe der Autor eine Diskussion darüber geführt, dass sie hätten Princeton auslassen und direkt nach der Highschool für Microsoft arbeiten gehen sollen. Angenommen Microsoft hätte sie damals angestellt, wären sie im Jahre 1988 mit weit mehr Erfahrung (und Geld) angekommen, als sie direkt nach dem Abschluss ihres Studiums hatten. Das Wichtigste sei aber, dass sie 1984 nicht viel weniger qualifiziert gewesen wären als 1988. Das sei teilweise auch dem einmaligen Zeitfenster geschuldet, dass es 1984 nicht viele Menschen gab, die mehrere Jahre Programmiererfahrung auf einem IBM PC hatten. Der Hauptgrund aber sei, dass zur Vorbereitung darauf, große Stücke kommerzieller Software zu entwickeln, das Schreiben von Videospielen in BASIC genauso nützlich gewesen sei wie ein Informatikstudium abzuschließen. Sicherlich hätte sie eine vierjährige Anstellung bei Microsoft 1988 bei weitem qualifizierter dastehen lassen, als wenn sie vier Jahre darauf verwendet hätten, Abschlüsse in Informatik zu erwerben. Gelegentlich habe Microsoft einen Entwickler eingestellt, der im Hauptfach etwas wie Musik studiert habe. Und der sei genauso erfolgreich gewesen wie diejenigen, die Informatik studiert hatten.
Zum Schluss des Kapitels schreibt der Autor, diejenigen Programmierer, die sich alles selbst beigebracht haben, seien arrogant. Und warum auch nicht, fragt er. Durch reine Intelligenz, ohne dass sie hätten eine Lehre machen, ihre Beiträge zahlen, eine standardisierte Zertifizierung durchlaufen oder sogar ein Studium abschließen müssen, seien Programmierer in der Lage, große Geldsummen zu bekommen, um einer Aktivität nachzugehen, der die meisten von ihnen sowieso in ihrer Freizeit nachgegangen wären. Welche bessere Validierung ihrer eigenen Großartikeit könne es geben? Der Leser solle diesen Gedanken im Kopf behalten, während der Autor die Arbeitsweise eines professionellen Programmierers genauer beleuchten werde.
Kapitel 3 - Schichten (Layers)
Der Autor sei erst ein Jahr nach der Uni ein Programmierer geworden. Zu jenem Zeitpunkt habe er bereits seit einem Jahrzehnt Programme geschrieben, ein Informatikstudium abgeschlossen und ein Jahr lang für ein kleines Software Start-Up gearbeitet. Ohne dass er es gewusst habe, sei das alles nur Übung für seinen letzten Test gewesen.
Sein Vorgesetzter bei der Firma Dendrite Americas habe ihn in sein Büro gerufen. Die Firma schrieb Software, die es Vertretern von Pharma-Firmen erlaubte - bewaffnet mit ihren Laptops - ihre Verkaufsmeetings mit den Ärzten zu planen. Jeden Abend wählten die sich in den zentralen Rechner der Firma ein, um die Notizen hochzuladen, die sie den Tag über gesammelt hatten, und um Updates ihrer Ärztedatenbank herunterzuladen. Ziemlich fortgeschritten für die späten 80er-Jahre. Ohne erkennbares Muster würde in manchen Fällen die Straßenanschrift eins Arztes durch die eines anderen ersetzt werden. Niemand sei in der Lage gewesen, herauszufinden, was los war, und der Vorgesetzte wollte es den Autor versuchen lassen.
Die Herausforderung in solchen Situationen sei, nicht so sehr das Problem zu beheben, sondern es zu finden. Fehler (bugs) in Software würden durch Reproduzierschritte (repro steps) beschrieben: die Sequenz, die der Benutzer befolgt, um den Fehler zu reproduzieren. Die könnten grob in zwei Kategorien aufgeteilt werden: Fehler, die jedes Mal passieren, und Fehler, die nur manchmal passierten, obwohl man dieselben repro steps macht. Diejenigen, die jedes Mal passierten, seien bei weitem den anderen vorzuziehen, zumindest aus der Sicht eines Programmierers, der versucht, diese Fehler zu lösen. Wenn man den Fehler dazu bringen kann, zuverlässig vorzukommen, könne man eingrenzen, wo das Problem ist. Software-Fehler, die periodisch auftauchen, würden einen dazu bringen, sich die Haare zu raufen.
Der Autor fährt fort, es gebe noch eine andere Art, Software-Fehler aufzuteilen: Fehler (bugs) in einem Programm, das man selber geschrieben hat und Fehler im Programm eines anderen Menschen. Im letzteren Fall wisse man gar nichts über die Details und fange bei Null an. Das Problem, mit dem der Autor sich konfrontiert sah, war ein periodischer Fehler in einem Programm, das ein anderer Mensch geschrieben hatte. Zudem kam der Druck von zahlenden Kunden, die auf die Beseitigung des Fehlers warteten. Der Autor verrät den Ausgang: nach einigen Tagen habe er den Fehler gefunden und eine Flasche Sekt sowie den Respekt seiner Kollegen bekommen.
Er bringt kurze Codebeispiele in der Programmiersprache C#. Der Code rufe eine API (in C# sei der Begriff für eine API method) auf, die ein Pop-Up-Fenster mit einer Nachricht erscheinen lässt. Der Autor verdeutlicht, dass Software in Schichten aufgebaut ist. Die API sei eine Schicht unter der Schicht, aus der heraus Code diese API aufruft. Diese API enthalte Code, der wiederum anderen Code aufrufe, der sich dutzende Levels tiefer befindet.
Der Code in einem Programm bestünde generell aus dem Herausfinden der gewünschten Parameter einer API (method in C#), dem Aufrufen dieser API und dem Benutzen der Information, die die API geliefert hat, um zu entscheiden, was als nächstes zu tun sei. Wenn der Code komplizierter werde, nehme die Anzahl der Code-Schichten zu, und die API-Aufrufe seien der Kleber, der die Schichten zusammenhalte. Viele Code-Beispiele zeigten nur eine Schicht, was unüblich in echten Programmen sei. Code gehe kaum über fünf Zeilen, ohne eine API aufzurufen.
Die APIs hielten all die Schichten der Software zusammen, und Missverständnisse zwischen diesen Schichten seien eine Hauptursache für unerwartete Probleme, fährt der Autor fort. Diese Fehlkommunikation könne verschiedene Formen annehmen. Angefangen mit dem Unwissen darüber, welchen ordnungsgemäßen Wert eine API in einem Parameter erwartet. Über das Unwissen darüber, wie die API diesen Parameter in ihrer internen Logik interpretieren wird. Bis zum Missverständnis darüber, wann und in welcher Form eine API einen Wert zurückgeben wird. Viele Fehlersuchen endeten folgendermaßen: nachdem der Programmierer über seinen eigenen Code mit dem feinsten Kamm drübergegangen ist und festgestellt hat, dass alles in Ordnung ist, öffnet er die Dokumentation, schlägt sich mit der flachen Hand auf die Stirn und sagt: "Oh, mir war nicht bewusst, dass die API, die ich aufgerufen habe, auf diese Art und Weise funktioniert."
Die Entscheidungen an dieser Stelle seien in den Händen der Programmierer, die den Code in der API schreiben. Derjenige, der die API aufrufe, müsse mit dem leben, was diese andere Person entschieden hat. Oft könne man den Code der API, die man aufrufe, gar nicht sehen. Man bekomme die API nur als kompilierten Code, ohne den Quelltext (source code). Leider kümmerten sich die Menschen, die Code schreiben, der anderen Menschen APIs zur Verfügung stellt, nicht besonders um Klarheit ihrer APIs nach außen. Sie blieben stattdessen in der internen Implementierung ihrer API stecken. Weil es, fügt der Autor den Grund dafür an, für jeglichen Code, den man schreiben wolle, wahrscheinlich mehrere Wege existierten, ihn zu schreiben - und nicht viel Weisheit darüber, welchen dieser Wege man wählen soll.
Der Autor zitiert aus einem Buch eines Psychologen: Mehr Auswahl zu haben, mache Menschen nicht glücklicher, sondern belaste sie. Programmierer hätten regelmäßig diese Belastung, fährt der Autor fort, weil es mehrere Wege gebe, auch den einfachsten Code zu schreiben, und es sei hart zu wissen, was der "richtige" Weg sei.
Der Autor greift ein Code-Beispiel von weiter oben im Buch auf und verdeutlicht anhand des Beispiels, dass es besser ist, an die zukünftige Benutzung des Codes zu denken und z.B. einem Wert, den man im konkreten Fall benutzt, auch eine Variable zuzuweisen, obwohl man es im konkreten Fall nicht müsste. Aber in der zukünftigen Weiterentwicklung des Codes vielleicht gerne auf diesen Wert zurückgreifen wollen würde. Der Autor ist dafür, möglichst viel Zukünftiges bereits im konkreten Fall zu bedenken. Er zitiert ein Buch, nach dem die Hälfte der zukünftigen Unterhaltskosten eines Programms darauf verwendet würden, sich mit den Details eines Programms erneut vertraut zu machen, die man in der Zwischenzeit wird vergessen haben. Solche geringfügigen Veränderungen im Code seien besser vorzunehmen, solange man den Code noch frisch im Kopf habe.
Der Autor kommt auf Codeüberprüfung (code review) zu sprechen. In Wirklichkeit gehe es dabei darum, dass andere Programmierer ihre persönlichen Meinungen darüber abgeben, wie sie den Code geschrieben hätten. Dabei stützten die sich auf nichts anderes als ihre eigenen Erfahrungen. Und da sie wüssten, wie flexibel das Schreiben eines Codes ist, meinten sie eher, dass ihre Vorschläge angenommen werden sollten. Egal, wie weit der Prozess des Code-Schreibens bereits fortgeschritten ist (no matter how late in the game it is).
Kapitel 4 - Der Dieb in der Nacht (The Thief in the Night)
Die Programmierer der Generation des Autors seien am meisten darüber besorgt, wie effizient ihre Programme liefen: sie müssten so schnell wie möglich laufen und dabei möglichst wenig Speicher gebrauchen. Der Autor kommt auf die Programmiersprache C zu sprechen, der er in seinem zweiten Jahr im College begegnete und die einen starken Fokus auf Performanz habe.
C wurde in den frühen 1970er-Jahren bei Bell Labs erfunden, dem Forschungszweig der Bell Telephone Company, die zu jener Zeit ein Monopol auf Ferngespräche hatte. Komplizierte Software sei nötig gewesen, um die Anrufe weiterzuleiten. Um die Weiterleitung zu unterstützen, sei das Betriebssystem UNIX geschrieben worden; zuerst in Assemblersprache, wie fast jedes Betriebssystem damals. Es habe die Meinung geherrscht, dass höhere Programmiersprachen Code hervorbrachten, der zu langsam und sperrig war, um im Inneren eines Betriebssystems ausgeführt zu werden. Als die Zeit kam, UNIX zu portieren, um es auf einem neuen Computer zum Laufen zu bringen, wurde dennoch die Entscheidung getroffen, es in einer höheren Programmiersprache neu zu schreiben. Es habe keine passende Sprache existiert, was zur Geburt der Programmiersprache C geführt habe.
Es sei hart zu beschreiben, wie richtig (how right) C sich für jemanden wie den Autor angefühlt habe, der daran gewöhnt gewesen sei, gegen einen begrenzten Speicherplatz auf einem IBM PC zu kämpfen. In Pascal würden Code-Abschnitte (blocks of code) mit den Stichworten BEGIN und END dargestellt; in C nehme man dafür die geschweiften Klammern { ... }. Wenn eine Konstruktion in Programmiersprachen als aerodynamisch beschrieben werden könne, dann sei es das gewesen. Das sei eine kleine Sache, die keine Auswirkung auf den Code hätte, den der Compiler produziere, aber dadurch habe sich C elegant und modern angefühlt, während Pascal einen blassen Hauch von Tweedjacke und Ellbogen-Patches behalten habe.
