Als Medien noch grundsätzlich auf Papier ausgeliefert wurden, war der Inhalt, also die Gedanken und Informationen, die transportiert wurden, an die äußere Form gebunden. Man konnte weder die Schrift vergrößern noch eine unterschiedliche Schriftart wählen oder - angenehm für manche Sehbehinderten - den Kontrast reduzieren. Daran änderte sich auch durch Radio und Fernsehen wenig.
Der Computer ist hingegen dazu in der Lage, Inhalte von ihrem Erscheinungsbild getrennt aufzubewahren. Jeder, der die Schriftart eines Dokuments in einer Textverarbeitung verändert, hat das schon ausprobiert. Tatsächlich sind Textverarbeitungsprogramme in dieser Hinsicht jedoch ein kleiner Rückschritt. ln der Bronzezeit der IT wurden Texte in Systemen wie TeX geschrieben und erst dann zu einem Dokument mit einem bestimmten Aussehen kompiliert. Man konnte die Präsentationsvorlage separat ändern und eine neue Version kompilieren.
An die Stelle dieser Satzsysteme ist heute in vielen Bereichen das Web getreten, in dem Dokumente nicht mehr kompiliert, sondern im Browser mit visuellen Attributen wie Farbe, Schriftart, Schriftgröße und Rahmen versehen werden. Auch bei HTML sind Bereiche durch Markup gegliedert: Der Autor schreibt Anweisungen direkt in den Text, die bei der Ausgabe jedoch entfallen, weil sie nicht für den menschlichen Leser bestimmt sind. Im Falle von HTML sind das die Tags, die in <> eingeschlossen werden.1
Eine Trennung zwischen Dokumentstruktur und Inhalten auf der einen Seite und Präsentationsanweisungen auf der anderen Seite hat folgende Vorteile:
• Der Inhalt kann leicht auf ganz unterschiedlichen Geräten zugänglich gemacht werden - das ist mit dem Aufkommen von leistungsfähigen Browsern auf Smartphones mit ihren kleinen Displays ausgesprochen wichtig geworden.
• Dokumente können von Suchmaschinen und anderen Programmen viel leichter verarbeitet werden, weil die sich für die Darstellung nicht interessieren.
Das Prinzip der Trennung von Funktion und Aussehen hat sich nicht nur im Bereich von Dokumenten bewährt, es ist auch in der Softwareentwicklung relevant. Will man ein Programm entwickeln, das nicht nur ein paar Daten umformatiert, sondern als Webapplikation oder sogar als lokal installierbare Applikation laufen soll, dann muss man sich Gedanken über die Benutzerschnittstelle machen und darüber, wie man sie mit dem Backend des Programms verzahnt.
Auch wenn man nicht einfach ein Stylesheet zum Programmcode hinzufügen kann, um eine grafische Benutzeroberfläche zu erzeugen, kann man Frontend und Backend trennen. Das Frontend erzeugt die Benutzeroberfläche, das Backend (auch Businesslogik genannt) ist der Teil, der Daten hält, verarbeitet und in Datenbanken schreibt, URLs aufruft oder Benutzereingaben validiert.
Webapplikationen werden häufig nach folgendem Modell geschrieben:
• Der Seiver, auf dem die Applikation später laufen wird, ist auch der Entwicklungsserver. Der Programmierer verbindet sich mithilfe eines FTP-Programms mit dem Server und lädt Programmdateien herunter und wieder hoch (oder der Texteditor hat FTP eingebaut und erlaubt es, scheinbar auf dem Server zu editieren, lädt aber im Hintergrund alles hoch und herunter).
• Änderungen werden lokal gemacht, nach dem Abspeichern wird die geänderte Datei auf den Server geladen und dort getestet.
• Läuft etwas nicht wie geplant, dann wird wieder lokal geändert und hochgeladen.
Dieses Modell ist zwar angenehm einfach, hat aber den Nachteil, dass die »Roundtrip-Zeit« (speichern, testen, ausbessern, neu speichern) immer ein paar Sekunden beträgt. >>Ein paar Sekunden« klingt nicht viel, aber vor allem als ungeübter Programmierer braucht man häufig mehrere Anläufe, bis der Code ungefähr das macht, was er soll. Da sind aus den wenigen Sekunden schon einige Minuten geworden, in denen man stattdes-sen etwas weniger Stumpfsinniges hätte tun können, zum Beispiel das Altpapier rausbringen. Die paar Sekunden können auch ausreichen, dass man aus dem geistigen »Flow« gerissen wird - dem Zustand der Konzentration, in dem man die Vorstellung davon, was man implementieren will, zusammen mit dem Punkt, an dem man steht, im Kopf hat. Ist man erst einmal raus, kann es ziemlich lange dauern, wieder in diesen Zustand zu kommen, und so können aus wenigen Sekunden schnell Viertelstunden werden.
Eine Abhilfe kann sein, das Programm nicht auf einen entfernten Server hochzuladen, sondern auf dem Arbeitsrechner einen Webserver einzurichten und nur lokal zu entwickeln. Heutzutage erlaubt es jedes Betriebssystem, einen lokalen Webserver mit PHPUnterstützung einzurichten, und falls man das lieber nicht möchte, kann man problemlos in VirtualBox oder einem ähnlichen Programm eine virtuelle Maschine starten, die einen Webserver, die Programmiersprachen der Wahl und SFTP-Zugang mitbringt. Arbeitet man mit einer derartigen Konfiguration, ist die Roundtrip-Zeit deutlich kürzer.
Hinzu kommt noch, dass die Verbindung zum entfernten Server auch einfach abreißen kann. Wer schon einmal im Zug per UMTS mal eben einen kleinen Fehler ausbügeln wollte, kennt das unangenehme Gefühl, wenn der Fortschrittsbalken einfach hängen bleibt. Sofort stellen sich interessante Fragen wie »führt ein halber Upload auch zu einer halben Datei auf dem Server?«. Und wenn irgendetwas richtig schiefgeht, jagt einem der Gedanke »Ich habe doch ein Backup - oder?« Schauer über den Rücken. Leider meist keine wohligen Schauer, weil man bei dieser Entwicklungsmethode in aller Regel eben kein Backup hat.
Bei der Entwicklung mit einem lokalen Entwicklungs- und einem Produktivserver muss man etwas mehr Aufwand in Konfigurationsmöglichkeiten stecken, denn das, was man lokal entwickelt, soll ja irgendwann auf den Produktivserver umziehen - und da sind üblicherweise einige Umgebungsvariablen und Pfade doch anders. Das muss nicht unbedingt ein Nachteil sein, sondern kann sich zum Vorteil entwickeln, weil man auf diese Weise von Anfang an darauf achtet, derartige serverspezifische Angaben in Config-Dateien auszulagern. Das ist grundsätzlich eine gute Idee, und es kann sich später auszahlen, weil man ja vielleicht eines Tages den Hoster wechseln wollen könnte.
Nebenbei hat die Trennung zwischen Entwicklungs- und Produktivserver den unschätzbaren Vorteil, dass man angstfreier entwickeln kann. Hat man unerklärliche Fehler in die Applikation eingebaut, dann kann man notfalls alles löschen und aus dem Versionskon-trollsystem wieder einspielen. Hat man keines, dann von einem Backup. Oder im schlimmsten Notfall die Version vom Produktivserver. ln dieser Zeit läuft die Applikation auf dem Produktivserver wie ein schnurrendes Kätzchen weiter.
Softwareentwicklung besteht zu einem Gutteil daraus, aus größeren Datenbeständen kleine Herden zu bilden. Hat man es - wie die Programmierer von Bildbearbeitungssoftware - mit unstrukturierten Daten zu tun (in diesem Beispiel mit einem Haufen Pixel), dann muss man jeden einzelnen Datensatz betrachten, wenn man z. B. alle roten Pixel grün machen möchte. Das dauert lange und ist nicht schön (es sei denn, man ist schon ein weniger schlechter Programmierer, der bequeme Tricks kennt).
Häufiger hat man es mit stark strukturierten Daten zu tun, die schon ganz unterschiedliche Namen, Frisuren und Hemden tragen - also vergleichbare Eigenschaften besitzen, anhand derer man sie gruppieren kann. Wenn man zum Beispiel eine Adressverwaltung schreiben will, dann kann man davon ausgehen, dass die Einträge allesamt einen Namen besitzen. Man kann weiterhin davon ausgehen, dass die Suche im Datenbestand nach dem Namen einer der häufigeren Anwendungsfälle sein wird. Während im vorigen Beispiel eine Suche nach »gib mir alle roten Pixel, die links neben einem blauen liegen« selten ist, laden strukturierte Daten zu Suchen wie »gib mir alle Maiers aus Berlin« geradezu ein. Solche flexiblen Suchen sind allerdings gar nicht so einfach zu programmieren, weil die Kriterien und ihre Zahl schwanken können. Während es noch ziemlich einfach ist, eine Suche zu programmieren, die in einem Adressdatenbestand alle Datensätze heraussucht, deren last_name = "Maier" und deren city = "Berlin” ist, wird es schon komplexer, wenn last_name auch noch "Meier" und "Mayer" umfassen soll. Als Nächstes kommt dann das Controlling und will aus diesem Bestand noch die haben, deren payment_status = "prepaid" ist. Oder die länger als ein Jahr Mitglieder sind. Spätestens dann beginnt die Angelegenheit auszuarten.
Aus dieser Not wurden beispielsweise Datenbank-Abfragesprachen wie SQL erschaffen. Mit ihrer Hilfe kann man in großen Mengen reich strukturierter Daten ziemlich flexibel suchen. Das Geheimnis für den Erfolg solcher Sprachen sind sogenannte Selektoren.
Selektoren stellen ein Vokabular dar, das beschreibt, welche Elemente aus einem größeren Datenbestand man gerne für die Weiterverarbeitung hätte - die Software ist dann dafür verantwortlich, mithilfe des Selektors die Daten zu sieben.
Wann immer man Gelegenheit dazu hat, mit Selektoren Daten zu filtern, sollte man es tun. Diese Art der angewandten Mengenlehre macht Programme wesentlich kürzer (häufig auch schneller) und reduziert die Zahl der Fehler ganz erheblich.
Ein Beispiel: Will man dem Nutzer ein Formular präsentieren, dann sollten Fehler beim Ausfüllen zu einer sichtbaren Fehlermeldung führen. So könnte man zum Beispiel einen Bereich rot hinterlegen, um auf den Fehler deutlich hinzuweisen.
Hier sehen Sie das HTML:
<div>
<div class="error">Fehler!</div>
<div class="ok">OK!</div>
</div>
Den <div>-Tag mit der Klasse »error« soll rot hinterlegt werden.
Um die Eingabeelemente im Formular zu finden, müsste man sich ohne Selektorensprache erst einmal alle <div>-Tags heraussuchen und sie dann einzeln durchgehen, um den richtigen Bereich zu finden. Das würde in JavaScript ungefähr so aussehen:
var nodelist = document.getElementsByTagName('div'); for (var i = 0; i < nodelist.length; i++) { var node = nodelist[i]; var cName = node.className;
if ((cName) && (cName.indexOf( 'error') != -1)) { node.setAttribute ('style', 'background: red');
Um das abzukürzen, kann man eine JavaScript-Bibliothek namens jQuery einbinden, die die Verwendung von Selektoren erlaubt.2 Hat man sie im <head> eingebunden, genügt folgende Anweisung in JavaScript:
$('div.error').css('background’, 'red');
Das funktioniert so:
$(’div.error') sucht im aktuellen HTML-Dokument alle <div>-Tags mit der Klasse error - es wird also ein Selektor angewendet, der mehrstufig arbeitet. Zunächst sucht er sich alle <div>-Tags, davon finden sich in diesem Beispiel drei. Dann grenzt er diese Menge auf solche ein, die ein class-Attribut besitzen, das sind zwei. Von diesen wählt er die aus, die im class-Attribut error stehen haben. Und schließlich wird im Ergebnis dieser Filteroperation der CSS-Eigenschaft background der Wert red zugewiesen.
Ein Beispiel in SQL:
select street from users where town = 'Berlin';
Auch hier wird mehrstufig gefiltert. Zunächst weiß die Datenbank, dass wir uns für Einträge aus der users-Tabelle interessieren. Einträge in anderen Tabellen sind für die Suche also irrelevant. Dann sucht die Datenbank diejenigen Einträge heraus, in denen in der Spalte town der String Berlin eingetragen ist. Von diesen Einträgen wird nur der Wert der Spalte street zurückgegeben.
Selektoren sind Teil deklarativer Programmierung. Mit ihrer Hilfe gibt man vor, welcher Art die Objekte sind, mit denen man arbeiten will, man muss sie aber nicht selbst heraussuchen. Deklarativ zu programmieren, kann den Code erheblich verkürzen und hilft dabei, die Fehleranfälligkeit zu reduzieren - andererseits ist solcher Code gelegentlich schwerer zu lesen und zu debuggen, weil man in komplexe, mehrstufige Selektoren keine print-Anweisungen einfügen kann, die einem die Ergebnisse einer Zwischenstufe ausgeben. Verwendet man Selektoren, dann bekommt man als Ergebnis immer eine Menge zurück. Selbst bei einer eindeutigen Suche nach Elementen, die durch eine Unique-ID glasklar gekennzeichnet sind, bekommt man eine Menge mit einem Element zurück.
Größere Programme setzen sich typischerweise aus Teilen zusammen, die von verschiedenen Autoren stammen, beispielsweise aus Funktions- und/oder Klassenbibliotheken und extra für das Projekt geschriebenem Code. Daher besteht die Gefahr, dass zwei der
Autoren denselben Namen für eine global sichtbare Funktion oder Variable venvenden. Natürlich kann das auch passieren, wenn Sie eigenen Code wiederverwenden möchten, etwa die String-Funktionen aus Projekt A und die praktischen Listenfunktionen aus Projekt B. Wenn Sie nun feststellen, dass beide Module eine Funktion definieren, die sort() heißt und noch dazu in beiden Projekten etwas anderes tut, dann haben Sie erst einmal ein Problem. Im besten Fall macht der Compiler oder Interpreter Sie umgehend darauf aufmerksam. Dieses Problem nennt man »name clash«, das Aufeinanderprallen zweier Funktionen oder globaler Variablen mit demselben Namen.
Die traditionelle Lösung besteht in einem in Großbuchstaben geschriebenen Präfix vor jedem global sichtbaren Namen. Dieses Präfix, das häufig aus nur zwei Zeichen besteht, liefert einen Hinweis auf das Herkunftsmodul oder -projekt. Aus sort() und sort() wird also PROJECTA_sort() und PROJECTB_sort() oder, kompakter, PA_sort() und PB_sort().
Die allgemeine Lösung sind Namespaces, also das explizite Definieren eines benannten Namensraums. Der große Vorteil besteht darin, dass Sie keine hässlichen Großbuchstaben in Ihren Code einsprenkeln müssen. Stattdessen können Sie einmal am Anfang der Datei festlegen, welche Namensräume Sie importieren wollen. Der Compiler prüft dann, ob Sie wirklich den Code aus dem angegebenen Namensraum verwenden.
Beispielsweise könnte ein Content-Management-System HTML-Seiten sowohl für Artikel als auch für Blogposts generieren, wobei die Anforderungen für die unterschiedlichen Typen von Seiten unterschiedlich sein könnten. In Ruby würde man daher zwei Module definieren:
module Article class Page # ... end end
module Blog class Page # ... end end
Der Code wird in article.rb und blog.rb gespeichert. Um eine neue Page zu erzeugen, schreibt man dann
require "blog"
blogpost = Blog: :Page::new
beziehungsweise
require "article"
article = Article::Page::new
Page wird also durch ihre Zugehörigkeit zu den Modulen Article und Blag eindeutig gekennzeichnet.
Der Verzicht auf Namespaces durch wenig erfahrene Programmierer, in unserem ersten Beispiel also die Autoren der beiden sort()-Funktionen, wird auch als »global namespace pollution« bezeichnet, als Verschmutzung des globalen Namensraums. Helfen Sie mit, den globalen Namespace sauber zu halten, denn wir haben nur den einen - und er ist von unseren Kindern nur geliehen!
Objektorientierte Sprachen bieten eine wesentlich elegantere Möglichkeit: Da jede Klasse einen eigenen Namensraum bildet, Funktionen und Member-Variablen also nur innerhalb einer Klasse eindeutig sein müssen, tritt hier das Problem seltener auf, nämlich nur bei Klassennamen. Das Problem ist mit Klassen als Namensräumen also nicht grundsätzlich gelöst, sondern eher verschoben und entschärft. In manchen objektorientierten Sprachen ist jede Klasse noch in eine Pakethierarchie eingebettet (zum Beispiel »java.utii«), wodurch ein noch größerer Namensraum geschaffen wird.
Variablen haben einen Geltungsbereich, den sogenannten Scope. Nur wenn sie >>in scope«, also sichtbar sind, kann man ihnen Werte zuweisen oder ihre Werte auslesen. Außerhalb ihres Gültigkeitsbereichs kennt die Programmiersprache sie nicht (die Variable ist »nicht sichtbar«). Gültigkeitsbereiche haben in den meisten Sprachen folgende Eigenschaften:
• Sie sind hierarchisch. Es gibt übergeordnete Gültigkeitsbereiche (»global«, das heißt überall im Programm sichtbar) und kleinere (»lokal«, nur in einer Funktion, jedoch nicht außerhalb).
• Variablen mit einem globalen Gültigkeitsbereich sind in den lokalen Gültigkeitsbereichen sichtbar, umgekehrt jedoch nicht.
• Lokale Gültigkeitsbereiche haben Vorrang vor globalen. Angenommen, man verwendet eine Variable loop für eine globale Schleife, hat aber auch in den Funktionen, die von dieser Schleife aufgerufen werden, lokale Variablen mit Namen loop. Wenn man in einer solchen Funktion jetzt aus loop liest, wird man was zurückbekommen, den Wert für die globale Schleife oder die lokale? Die Antwort ist: Die lokale Variable geht vor, man kann also in der Unterfunktion den Wert der globalen Variable nicht sehen, weil sie von der lokalen »überschattet« wird. Wenn man der lokalen Variable in der Funktion einen Wen zuweist, dann wirkt sich das auf die globale nicht aus. Der Programmcode außerhalb der Unterfunktion hingegen sieht die globale Variable, nicht aber die lokale, weil er sich in einem übergeordneten Gültigkeitsbereich bewegt.
• Variablen in nebeneinanderliegenden lokalen Gültigkeitsbereichen derselben Hierarchie stören einander nicht. Verwendet man also in Funktion A und Funktion B jeweils eine Variable des Namens loop, dann sieht man in Funktion A nur die eigene, nicht aber die von Funktion B.
Auch wenn es zunächst wie eine lästige Einschränkung scheint, dass man nicht alle Variablen von überall her lesen kann, haben Scopes große Vorteile: Man sieht nur das, was man in einer Funktion wirklich benötigt, und kann anderen Funktionen nicht dazwischentunken, indem man ihre Variablen unbeabsichtigt überschreibt.
Es gilt allgemein als gute Praxis, möglichst wenig globale Variablen zu benutzen, denn es kann ungemein schwer sein, nachzuvollziehen, wer wo genau Variablenwerte ändert. Je mehr man lokal arbeitet, desto leichter sind Probleme einzugrenzen. Funktionen sind lokale Codebereiche, die Parameter übergeben bekommen und nur mit diesen Parametern und ihren lokalen Variablen arbeiten sollten. Schreibt man in Funktionen in globale Variablen, dann durchbricht man diese schöne Strukturierung (siehe auch Kapitel 14).
Dieser Überlegung folgend, wurde in manchen Sprachen (zum Beispiel Java) der globale Geltungsbereich komplett abgeschafft. Wenn das in der Sprache Ihrer Wahl nicht der Fall ist (wie zum Beispiel bei JavaScript, PHP oder Perl), sollten Sie dennoch möglichst keine Variablen im globalen Scope anlegen.
Assertions werden benutzt, um sich vor Fehlern durch falsche Eingangsdaten zu schützen, indem man den tatsächlichen Wert, den man erhalten hat, mit einem erwarteten Wert vergleicht.
ln Funktionen, die sich auf bestimmte Werte für ihre Eingabeparameter verlassen, sollten Sie diese am Beginn der Funktion prüfen. Derartige Prüfungen werden »Sanity-Checks« genannt. Die folgende Schleife soll die ersten n Kunden aus einem Array zurückgeben, die gewünschte Zahl steht in numPersons:
function getFirstNCustomers (allCustomers, numPersons) { var foundCustomers = new Array(); int loop = 0;
while(foundCustomers.length < numPersons) { var customer = allCustomers[loop]; if (customer != null) {
foundCustomers.add (customer);
loop = loop + 1;
return foundCustomers;
Das funktioniert sehr gut, solange es in allCustomers mindestens so viele Kunden gibt, wie numPersons verlangt. Wenn wir also ein Kunden-Array definieren
var customers = new Array();
customers.add(new Customer("name" => "Hans Meiser", "id" => 1)); customers.add(new Customer("name" => "Bernadette Eisen", "id" => 2)); customers.add(new Customer("name" => "Tinchen von Lurk", "id" => 3));
und dann die Funktion so aufrufen, dass wir aus diesem Array die ersten zwei Einträge zurückbekommen wollen, erhalten wir
>getFirstNCustomers (customers, 2)
>Customer("name" => "Hans Meiser", "id" => 1)
>Customer("name" => "Bernadette Eisen", "id" => 2)
Das Problem der Funktion wird deutlich, wenn wir statt zwei mit den gleichen Eingangsdaten vier Kunden bekommen wollen. In diesem Fall wird aus der while-Schleife (in vielen Sprachen) eine Endlosschleife, weil loop irgendwann die Länge des customers-Arrays überschreitet. Jeder weitere gelesene Kunde in der Zeile var customer = allCusto-mers[loop]; ist dann null, das foundCustomers-Array kann keine weiteren Einträge erhalten und die Abbruchbedingung der Schleife wird nie erreicht.
Normalerweise würde man in diesem Fall nur so viele Kunden zurückliefern, wie es gibt, aber es kann Fälle geben, in denen ein so rigides Verhalten erwünscht ist, weil ein zu großer Wert in numPersons ein Anzeichen für einen Fehler im Rest des Programms ist. In der Sprache C wäre eine ähnlich naive Version beispielsweise eine Sicherheitslücke, die es erlauben würde, Speicher auszulesen, den das Programm möglicher'eise nicht lesen sollte. Um absichtliche Exploits (also das Ausnutzen von Sicherheitsproblemen) oder Programmierfehler zu vermeiden, sollten in unserem Fall zwei Annahmen immer erfüllt werden:
Die verlangte Zahl von Einträgen kann nicht größer als die Länge des Eingangsarrays allCustomers sein.
Das Eingangsarray darf keine Kundeneinträge enthalten, die null sind.
Macht man diese Angabe maschinenlesbar, kann das Programm zur Laufzeit automatisch darauf hinweisen, dass die angegebenen Bedingungen verletzt wurden. Für die Prüfung derartiger sogenannter Invarianten (also Vorbedingungen, die sich nie ändern) gibt es in vielen Programmiersprachen assert(). Assertions statten die Funktion mit Klauen und Zähnen aus, um sich gegen Fehler und Manipulationen zu verteidigen. Die genaue Bezeichnung und Syntax ist von Sprache zu Sprache unterschiedlich, aber das Prinzip ist immer ähnlich:
assert (allCustomers.length >= numPersons, "Error: requested number exceeds Array length" );
assert() nimmt meist zwei Argumente entgegen, zunächst die Bedingung, die geprüft werden soll, und dann eine Fehlermeldung, die ausgegeben wird, wenn die Bedingung verletzt wird.
Eine mögliche robuste Version der Funktion würde daher so lauten:
function getFirstNCustomers (allCustomers, numPersons) { assert (allCustomers.length >= numPersons); var foundCustomers = new Array(); int loop = o;
while(foundCustomers.length < numPersons) { var customer = allCustomers[loop]; assert (customer != null)
foundCustomers.add (customer); loop = loop + 1;
return foundCustomers;
Die Funktion ist jetzt sehr penibel und bricht das Programm mit einer Assertion-Excep-tion ab, wenn Sie mehr Kunden lesen wollen, als Sie überhaupt kennen, oder wenn ein Eintrag im customers-Array null ist.
Zwar ist es hilfreich, Invarianten in Codekommentaren zu der Funktion zu notieren, aber Kommentare können zur Laufzeit nicht automatisch geprüft werden und können veralten. Ändern sich die Vorbedingungen Ihrer Funktion und Sie verwenden Assertions, dann zwingt die resultierende Assertion-Verletzung Sie, noch einmal über die Funktion und ihre Invarianten nachzudenken.
Schreiben Sie immer Assertions, ...
• ... wenn Sie sicher sind, dass ein bestimmter Fall niemals eintreten kann. Passiert es doch, dann werden Sie prompt über einen Denkfehler informiert.
• ... wenn ein Fehler so schwerwiegend ist, dass das Programm nicht sinnvoll weiterarbeiten kann.
• ... wenn Sie mit Daten umgehen müssen, die einem bestimmten Schema gehorchen sollten. Prüfen Sie rigoros, ob die Daten sich an die Spielregeln halten, dann schützen Sie sich vor Datenverfälschungen. Gerade in Situationen, wo Sie große Datenmengen verarbeiten müssen, haben Sie kaum eine Chance, Datenfehler per Hand zu finden.
• ... wenn Sie versucht sind, in einem Codekommentar zu schreiben >>... darf auf gar keinen Fall passieren«. Irgendwann wird es passieren, der Codekommentar wird es nicht verhindern.
• ... am Beginn einer Funktion, um zu erzwingen, dass Variablen nur bestimmte Wertebereiche annehmen können.
• .. .am Ende einer Funktion, um zu prüfen, ob das Ergebnis sinnvoll ist, also ebenfalls in einem bestimmten Wertebereich liegt.
Allerdings sind Assertions ein scharfes Schwert. Wenn sie fehlschlagen, wird das Programm beendet, was nur in echten Notfällen passieren sollte. Wenn Ihr Programm also beispielsweise die Ergebnisse einer Berechnung in eine Datei schreiben will, diese aber schreibgeschützt ist und das Schreiben daher fehlschlägt, dann ist es sinnvoll, das Programm zu beenden. Die Alternative wäre, dass keine Daten geschrieben werden und der Anwender davon nichts merkt. assert ( file.canWrite()) ist in diesem Fall also eine gute Praktik.
Weniger sinnvoll ist es hingegen, wenn Ihr Benutzerinterface Assertions verwendet, um Eingabefehlem durch den Benutzer vorzubeugen. Hat der sich in einer Maske vertippt oder eine Angabe vergessen, dann sollte das Programm ihn freundlich darauf aufmerksam machen und nicht mit einer hässlichen Fehlermeldung sterben.
Assertions sind dann nicht notwendig, wenn Sie wissen, dass ein Fehler sowieso eine Exception oder einen Programmabbruch hervorrufen wird, beispielsweise wenn Sie eine Datenbankverbindung öffnen wollen, das aber fehlschlägt. Sie können zwar per assert(dbConnection.isValidQ) den Zustand prüfen, aber wenn die Verbindung nicht geöffnet werden konnte, wird entweder direkt beim versuchten Öffnen eine Exception resultieren (Ihr Programm kommt dann gar nicht erst zu Ihrer Assertion-Prüfung) oder spätestens bei der ersten Datenbankoperation, die Ihr Programm über die Verbindung versucht.
Assertions sind weiterhin ein wichtiger Baustein in Kapitel 16, wo sie überden Erfolg von Unit-Tests entscheiden.
Transaktionen und Rollbacks sollen vor Datenverfälschung schützen, wenn mehrere Datensätze geändert werden.
Wenn Sie in einer Kundendatenbank bei einem Eintrag sowohl den Namen als auch die Adresse ändern wollen, können Sie das ganz bequem mit einem einzigen UPDATE-Statement erledigen. Diese Anweisung ist »atomar«, sie wird entweder ganz ausgeführt oder gar nicht, Sie müssen sich keine Gedanken darüber machen, ob vielleicht nur der Name geändert wurde.
Sind hingegen mehrere Anweisungen im Spiel, die nacheinander ablaufen müssen, aber inhaltlich zusammengehören, kann es ohne Transaktionen dazu kommen, dass nur die ersten Schritte ausgeführt werden, der Rest jedoch nicht. Ein Beispiel: Man hat zwei Konten und möchte Geld von A nach B transferieren. Wenn der Praktikant über die Stromleitung stolpert, nachdem das Geld von Konto A abgezogen wurde, aber bevor es auf Konto B ankommt, ist das Geld weg.
Daher werden solche mehrstufigen Vorgänge mit Transaktionen abgesichert: Nach außen verhält sich die Transaktion atomar, sie wird entweder komplett durchgeführt oder es kommt zu einem Rollback. Das bedeutet, dass alle bisherigen Änderungen wieder zurückgenommen werden, um den Vorgang gänzlich ungeschehen zu machen. ln unserem Beispiel würde das Geld bei einer fehlerhaften Transaktion also auf Konto A zurücküberwiesen. Im Gegensatz zu einfachen SQL-Anweisungen müssen Beginn und Ende einer Transaktion bei Datenbankoperationen angekündigt werden, damit die Datenbank eine Chance hat, zusammengehörige Anweisungen zusammen rückgängig zu machen.
Außerhalb von Datenbanken begegnen Ihnen Transaktionen heute hauptsächlich im Dateisystem: Moderne Dateisysteme sind ziemlich robust dagegen, dass beispielsweise der Strom plötzlich weg ist. Selbst wenn ein Programm gerade in eine Datei schreibt, wenn der Strom ausfällt, wird das Dateisystem nicht beschädigt, denn die Kette »Datei anlegen, öffnen, schreiben, schließen« wird in einer Transaktion aufgezeichnet (im sogenannten Journal). Fällt der Strom aus, dann sieht das Betriebssystem, dass eine Dateioperation nicht beendet werden konnte, und löscht das Dateifragment aus dem Inhaltsverzeichnis. Die geschriebenen Daten sind zwar weg, aber Sie müssen nicht mehr wie früher die Festplatteninhalte reparieren oder neu installieren.
Die Idee hinter Transaktionen, nämlich ein Konzept zu haben, um Schäden durch Störungen zu minimieren, kann auch in der Softwareentwicklung nützlich sein. Wenn Ihr Programm beispielsweise Daten von einem Server herunterlädt und dann verarbeitet, kann es sinnvoll sein, erst die Daten komplett herunterzuladen und dann mit der Verarbeitung zu beginnen. Schlägt der Download fehl, können Sie die Reste löschen und noch einmal anfangen. Haben Sie die Daten schon teilweise verarbeitet, kann es schwierig sein, sie wieder rückstandsfrei zu entfernen.
Hashes oder digitale Fingerabdrücke sind Algorithmen, die fast beliebig lange Dateien zu einer quasieindeutigen Zahl einer bestimmte Länge eindampfen. Mathematisch ist es zwar unmöglich, allem - von einem Buchstaben bis zum Inhalt eines ganzen Rechenzentrums - einen unverwechselbaren Wert zuzuweisen, aber das ist auch nicht das Anliegen von Hashing-Algorithmen. Sie wollen nur digitale Fingerabdrücke erzeugen, die für praktische Anwendungen unverwechselbar sind.
Eines ihrer Einsatzgebiete für Programmierer sind schnelle Vergleiche zwischen sehr großen Digitaldateien. Wenn Sie beispielsweise Tausende oder sogar Millionen von Bildern haben und ein neues Bild dazukommt, möchten Sie eventuell wissen, ob Sie dieses Bild schon irgendwo in der Sammlung haben. Zwar könnten Sie das neue Bild einfach mit allen vorhandenen vergleichen, aber ein Vergleich mit allen Dateienwürde ziemlich lange dauern. Wenn Sie jedoch von jedem Bild seinen Hashwert berechnen, müssen Sie pro Bild nur 20 Byte für den Hashwert vorsehen, beispielsweise in einer Datenbank. Bekommen Sie ein neues Bild herein, dann berechnen Sie seinen Hashwert und vergleichen ihn mit den bereits gespeicherten. Sind zwei Hashes gleich, sind mit an Sicherheit grenzender Wahrscheinlichkeit auch die beiden Dateien gleich. Daher werden Hashwerte auch als digitale Fingerabdrücke bezeichnet.
Gängige Hashing-Algorithmen gehorchen ein paar Regeln:
• Gleiche Eingangsdateien führen für einen bestimmten Hashing-Algorithmus zu gleichen Fingerabdrücken.
• Unterschiedliche Eingangsdateien führen mit einer extrem hohen Wahrscheinlichkeit zu unterschiedlichen Fingerabdrücken. Es kann sogenannte Hash-Kollisionen geben, bei denen zwei unterschiedliche Eingangsdateien zu gleichen Hashes führen, aber die Hashlänge und die Algorithmen sind so konstruiert, dass das nach menschlichen Gesichtspunkten nahezu unmöglich ist.
• Die Länge des Fingerabdrucks ist zwar je nach Algorithmus unterschiedlich, aber für einen bestimmten Algorithmus immer gleich, egal, wie lang die Eingangsdatei war.
• Sie sind keine Kompressionsalgorithmen, daher kann man aus dem Fingerabdruck die Ursprungsdatei nicht rekonstruieren. Während es leicht ist, den Hashwert einer bestimmten Eingabe zu berechnen, ist es praktisch nicht möglich, die Eingabedatei zu einem Hashwert zu berechnen.
• Eine kleine Veränderung der Eingangsdatei erzeugt einen ganz anderen Fingerabdruck, eine große Änderung auch. Sie können also Ähnlichkeiten von Hashwerten nicht als Maß für die Ähnlichkeit der Eingangsdateien nehmen.
ln der Kryptografie werden Hashes als digitale Nachweise für Echtheit eingesetzt, indem zu einem Text ein Hash gebildet, verschlüsselt und das Ergebnis gespeichert wird: Verändert man am Originaltext irgendetwas, dann passt der (entschlüsselte) Hash nicht mehr zum Text. Der Empfänger kann also überprüfen, ob der Text manipuliert wurde, indem er seinen digitalen Fingerabdruck berechnet und mit dem Hash des Originaltextes vergleicht. Die Verschlüsselung des Unterschrifts-Hashs sorgt dafür, dass dieser nicht verändert werden kann. Man kann solche digital signierten Texte per Internet übertragen oder in einem Archiv ablegen und Verfälschungen auch Jahre später noch nachweisen. Zwar könnte man auch den gesamten Text verschlüsseln und damit fälschungssicher machen, das wäre für längere Texte jedoch etwas unpraktisch, weil sich ihre Länge verdoppeln würde.
Weitere Einsatzgebiete:
• Als Checksumme, um Übertragungsfehler zu erkennen.
• Als Unique ID einer bestimmten Version einer Datei. Die Versionskontrollsysteme Mercurial und git verwenden den Hashing-Algorithmus SHA-1, um Dateiversionen zu unterscheiden. In der Bioinformatik werden genetische Sequenzen gelegentlich durch ihren Hashwert identifiziert.
• Erzeugung von Zufallszahlen. Man beginnt mit einer beliebigen Zahl und bildet den Hash von ihr, dann den Hash dieses Hashs und so weiter. Da Hashfunktionen aus einer kleinen Abweichung komplett unterschiedliche Zahlen berechnen, ist die Abfolge solcher verketteten Hashberechnungen zufällig verteilt.
• Speicherung von Passwörtern. Statt des Passworts wird nur der Hash gespeichert. Will sich ein Benutzer einloggen, dann wird von dem von ihm eingegebenen Passwort der Hashwert gebildet und mit dem gespeicherten Hash verglichen. Wenn die Hashes gleich sind, war das Passwort richtig. Beachten Sie aber hierbei die Anmerkungen in Kapitel 25.
• Digitale Kryptowährungen wie bitcoin setzen umfassend darauf, dass die Berechnung eines Hashs sehr schnell geht, es jedoch praktisch unmöglich ist, zu einem gegebenen Hash einen passenden Eingangswert zu finden. Sie nutzen Hashes, um Konten und Transaktionen abzusichern.
Als Entwickler können Ihnen Hash-Algorithmen immer dann nützen, wenn der Inhalt einer Datei oder eines längeren Textstücks die zentrale Information ist, um sie bzw. es von anderen zu unterscheiden. Für eine Blogsoftware oder Bildverwaltung können Sie Ihre Inhalte hashen und die entstehenden Fingerabdrücke als Unique IDs verwenden -egal ob als Dateinamen oder in einer Datenbank. Im Gegensatz zu Timestamps sind Has-hes langzeitstabil: Wenn Sie Dateien nach ihrem Änderungsdatum organisieren, wissen Sie nie hundertprozentig genau, was mit diesen Änderungsdaten passiert, wenn Sie auf einen anderen Server umziehen oder ein Backup einspielen müssen.
Hashes sind auch nützlich, um unterschiedlich lange Informationshäppchen zu vereinheitlichen. Falls Sie jemals einen Webcrawler schreiben wollen, der die Inhalte einer Page unter der URL als Unique ID ablegt, dann werden Sie schnell merken, dass URLs unpraktisch lang sein können - tatsächlich ist die maximale Länge nicht durch einen Standard vorgeben. Hashen Sie die URL jedoch mit SHA-1, dann wissen Sie sicher, dass sie danach verwechslungssichere 20 Byte in der Hand halten. Zwar werden Sie die unge-hashte URL auch irgendwo aufbewahren wollen, aber als Unique ID kann eine Zahl mit definierter Länge angenehmer sein.
Programme, die Datenbankinhalte manipulieren, werden auch als CRUD-Software bezeichnet, nach den vier grundlegenden Operationen, die man auf die Inhalte von Datensammlungen anwenden kann:
• Create,
• Read,
• Update und
• Delete.
Natürlich kann man mit gelesenen Daten noch viel mehr machen, zum Beispiel sie mit anderen Daten vergleichen oder irgendwohin hochladen, aber das ist dann bereits außerhalb des Datenbankrahmens. Diese vier Grundoperationen sind keine willkürliche Auswahl, sondern genau das, was man mit Daten tun kann.
Weil es sich um die grundlegenden vier Operationen handelt, finden sie sich mit anderem Namen auch außerhalb von Datenbanken wieder. Alle Dateien auf Ihrem Computer gehorchen dem gleichen Grundsatz: Sie können sie anlegen, öffnen und daraus lesen, in sie hinein speichern und sie irgendwann löschen.
Auch wenn diese Operationen beim ersten (und zugegebenermaßen auch zehnten) Lesen nicht wie eine umwerfende geistige Leistung erscheinen, ist es gelegentlich nützlich, sich bei der Softwareentwicklung auf sie zu besinnen. Wenn Sie in Ihrem Programm temporäre Dateien hin und her verschieben, ist das nichts anderes als ein Update einer Metainformation, nämlich des Dateipfades. Wenn Sie die temporäre Datei öffnen, die
Inhalte auslesen und in eine andere Datei schreiben, dann machen Sie ein Read auf die temporäre Datei, ein Create auf die Zieldatei und zum Schluss ein Update auf die Zieldatei, bei dem sie den Inhalt in diese Datei schreiben.
Das funktioniert auch in großen verteilten Systemen wie zum Beispiel dem Web: In Con-tent-Management-Systemen können Sie neue Beiträge anlegen, editieren und gegebenenfalls löschen. Und dass Sie im Web HTML-Seiten lesen können, versteht sich von selbst.
Weil diese vier Operationen so fundamental sind, haben die Entwickler des HypertextÜbertragungsprotokolls HTTP, das die Grundlage des Web darstellt, sie in diesem Protokoll implementiert. HTTP funktioniert im Groben so, dass der Client (also zum Beispiel Ihr Webbrowser) an den Server eine Anfrage stellt, die aus einem Zielobjekt und einem Verb besteht. Das Zielobjekt wird durch die URL festgelegt, das Verb ist eines der in HTTP definierten: POST, PUT, PATCH, GET, DELETE.
Man kann CRUD ungefähr folgendermaßen auf die HTTP-Verben abbilden:
• Create: POST
• Read: GET
• Update: PUT und PATCH
• Delete: DELETE
In den 90er und 2000er Jahren wurde viel Schindluder mit diesen HTTP-Verben betrieben. Die Entwicklung des Web beschleunigte sich derart, dass mittelmäßige Hacks zu weitverbreiteter Software wurden. Dabei fiel häufig auch die unterschiedliche Bedeutung der HTTP-Verben unter den Tisch: Große Frameworks wie Java Servlets verwendeten POST, wo GET richtig gewesen wäre.
In den 2010er Jahren fand teilweise eine Rückbesinnung auf die Grundlagen des Web statt. Diese REST- (Representational State Transfer-)Bewegung betonte unter anderem, dass für das Abrufen und Manipulieren von Daten auf Webservern die HTTP-Verben verwendet werden sollten, anstatt die URL für die Kennzeichnung sowohl der Aktion als auch ihres Ziels zu missbrauchen.
Diese Trennung von logischer Gliederung des Textes und visuellen Anweisungen ist nicht unbedingt zwingend. Es gab verschiedene Ansätze, unter anderem in früheren Versionen von Mac OS, die visuellen Informationen in binären Dateien außerhalb des Textes zu halten. Letztlich konnten sie sich nicht durchsetzen.
Wenn Sie sich um ältere Browserversionen keine Gedanken machen müssen, geht es auch ohne jQueiy mit getElementsByClassName().