Nur keine Fehler
Als ich anfing die, Computerprogramme zu schreiben, wurde mir gesagt, daß mit das Wichtigste die Fehlerbehandlung sei. Wie genau eine richtige Fehlerbehandlung aussehen würde, erklärte mir aber niemand. Sollte das Programm in der Lage sein, bei einem Stromausfall das Kraftwerk wieder hochzufahren, bei einem Headcrash die Daten der Datenbank wiederherzustellen? Meine erste Idee war, daß man nur alle Eingaben hinreichend genau kontrollieren müßte, um eine zuverlässige Programmausführung zu erreichen.
Später lernte ich dann, wie man in C-Programmen massenweise Returncodes auswertet oder wie man Exceptionhandler benutzt. Etwas, das immer wieder behauptet wurde, war, daß es wichtig ist die Fehlerbehandlung von Anfang an in das Design einzuplanen, um sie richtig zu machen. Leider kenne ich nur ganz wenige Anwendungen mit einem durchgängigen Fehlerbehandlungskonzept. Die Idee von Java, die Entwickler immer wieder zur Fehlerbehandlung zu drängen, hat die Situation in meinen Augen noch verschlimmert.
Da gibt es über den gesamten Code verteilte Fehlerbehandlung, die im wesentlichen nichts tut, als Exceptions zu fangen und wieder zu werfen bis in irgendeinem Exceptionhändler nur noch eine ToDo Kommentar steht. Auch kenne ich eine Anwendung, die Datenbankfehler so gut kapselt, daß der Benutzer nichts davon merkt, wenn die Datenbank während der Bearbeitung verloren geht. Auswahlfelder werden aus irgendwelchen Caches gefüllt und beim Abspeichern landen die Daten im Nirwana. Technisch ist das eine schöne Lösung, aber wahrscheinlich nicht ganz im Sinne des Anwenders.
Typen von Fehlern
Um hier weiterzukommen, müssen wir zuerst die zwei Arten von Fehlern unterscheiden, wie bei der Programmausführung auftreten können. Zunächst gibt es die völlig unerwarteten Fehler. Hierbei kann es sich um Hardwareprobleme handeln oder Programmiererfehler. Java versucht das durch die Unterscheidung von Error und RuntimeException abzubilden. Leider ist derselbe Fehler in einer Situation erwartet und in einer anderen unerwartet.
Nach dem Auftreten eines unerwarteten Fehlers befindet sich das System in einem unerwarteten Zustand. Jetzt weiterzumachen ist gefährlich. Die Annahmen, die bei der Entwicklung gemacht wurden, gelten nicht mehr mit Sicherheit. Variablen können mit inkonsistenten Werten vorbesetzt sein, externe Systeme in einem Zwischenzustand, bei dem unser System die Fortsetzung nicht mehr kennt. Was bleibt ist, aufzuräumen und den Fehler möglichst detailliert zu melden.
Was also tun? Mein erster Ratschlag ist: möglichst wenig. Dies ist vielleicht ein wenig provokativ, aber eine schlechte Fehlerbehandlung zu bereinigen ist schwerer, als eine einfache zu erweitern. Die einfachste Annahme ist, daß keine Fehler zu erwarten sind. Dann brauche ich mich nur um die unerwarteten zu kümmern. Das bedeutet, daß ich nur außen um meine Anwendung einen Exceptionhandler brauche, der eben aufräumt und den Fehler protokolliert. Was "außen" heißt, kann variieren, je nachdem was die isolierbare Einheit ist. In einer einfachen Anwendung, wird es die gesamte Anwendung sein. Unter JEE kann es ein EJB sein, unter dotnet eine Applicationdomain.
Aufräumen
Wenn der Fehler bei einer Verbindung zu externen Ressourcen passiert, wird man zunächst versuchen, einen stabilen Zustand lokal herzustellen, indem man die Ressource schließt. Dies erfolgt durch try/finally oder in C# durch einen using-Block. Kommt es hierbei zu einem Folgefehler, so ist weitermachen nicht mehr sinnvoll und die Exception sollte bis zu dem äußeren Exceptionhandler durchgelassen werden.
Erweiterung der Fehlerbehandlung
Mit der Zeit wird man die Ursache und die Auswirkung für bestimmte Fehler verstehen lernen. Dann ist man in der Lage, für diese Fehler eine spezielle Behandlung einzubauen. Hierzu muß zuerst festgestellt werden, wo der Fehler abgefangen werden soll. Eine gute Anwendung hat nur wenige Stellen, wo eine Fehlerbehandlung geschieht. Außerdem darf nur genau dieser spezifische Fehler abgefangen werden. Wenn mehrere Fehlerbehandlungen verwendet werden, hat es sich bewährt, eine entsprechende Vererbungshierarchie von Exceptions aufzubauen.
Beispiel: Batchverarbeitung von Dateien
Ein System verarbeitet alle Dateien, die in einem Verzeichnis liegen. Die Dateien bestehen aus Datensätzen, die einzeln verarbeitet werden. Hier gibt es drei Ebenen der Verarbeitung: Datensatz, Datei und Gesamtverarbeitung. Bei jedem Fehler kann also unterschieden werden, ob der einzelne Datensatz fehlerhaft ist, die gesamte Datei oder ob die Verarbeitung wegen der Schwere des Problems eingestellt werden muß. Es werden also fachliche Exception für Datensatzprobleme und für Dateiprobleme gebraucht, wobei die ersteren von den zweiten erben können. Alle anderen Fehler führen zu einem Anwendungsabbruch.
Umwandlung von Exceptions
Häufig werden Exceptions generell abgefangen und in anwendungsspezifische Exception umgesetzt, die dann neu geworfen werden. Aus meiner Sicht macht das nur unter zwei Umständen Sinn. Der erste ist, wenn ich ein fachliches Problem anhand einer technischen Meldung feststelle. Wenn die Datenbank "Duplicate key" meldet, kann da unter Umständen fachlich behandelt werden. Der andere Grund kann sein, daß ich mich von einer herstellerspezifischen API unabhängig machen will. So abstrahiert das Spring-Framework Datenbankexceptions, so daß der Entwickler einer Datenzugriffsschicht sich nicht um das konkrete Produkt kümmern muß.
Ansonsten sollten Exceptions möglichst in dem Kontext behandelt werden, in dem sie auch verstanden werden. Will sagen, Oracle-Fehler sollten nur in der Datenbankschicht behandlet werden. Wenn dies nicht möglich ist, werden sie zur äußeren Fehlerbehandlungsroutine durchgereicht, die Fehler nicht mehr spezifisch behandelt. Die Datenbankzugriffsschicht kann dann unterschiedliche Methoden anbieten, die z. B. bei einem Fehler infolge bereits vorhandenem Schlüssel einmal einen (unerwarteten) Fehler meldet oder statt Insert transparent ein Update versucht.
Fehler frühzeitig erkennen
Es ist immer besser, wenn die Programmlogik ein Problem erkennt als wenn dies in der Runtime passiert. Wenn eine Nullpointer-Exception auftritt, so ist es recht mühselig, die Ursache zu analysieren. Grund kann sein, daß eine Eingabeschnittstelle einen erwarteten Wert nicht geliefert hat. Wenn dies direkt an der Schnittstelle überprüft wird, kann man gleich eine sinnvolle Fehlermeldung liefern. Ich bin noch nach zwanzig Jahren der Meinung, daß ein Programm wesentlich stabiler und auch einfacher wird, wenn man sich bei der Datenvalidierung mehr Mühe macht. Am besten wäre natürlich, wenn jede Routine ihre Vorbedingungen überprüfen würde, wie das die Sprache Eiffel nahelegt.
Keine Kommentare:
Kommentar veröffentlichen