Die Fertigstellung wird wohl einige Jahre dauern ...
Datum der letzten Änderung: 20. Januar 2004 (Beitrag kopiert, sonst nichts)
Im Nachgang einer Diskussion über Exceptions hat GregorRayman seine Ansichten zum Thema im lesenswerten Beitrag GregorRayman/Exceptions kurz zusammenzufassen versucht.
Einige der in seinem Beitrag dargelegten Punkte kollidieren mit meinen eigenen Ansichten. Diese Punkte möchte ich im folgenden nennen. Dazu habe ich Gregors Beitrag hier in Kopie aufgenommen (Stand 20. Januar 2004) und in hervorgehobener Schrift notiert (wird von Browsern üblicherweise kursiv angezeigt).
Meine Anmerkungen notiere ich eingerückt und in nicht-hervorgehobener Schrift.
In der Diskussion EinGutesExceptionBeispiel ist es klar geworden, dass sich unsere Meinungen über die Exceptions nicht zusammenbringen lassen. Mich stört es gar nicht, denn ich bin nicht auf diesem Wiki und Meinungsgleichheit zu erreichen, oder um andere Leute von meiner Meinung zu überzeugen. Eine Einigkeit ist auch nicht nötig, denn schließlich arbeiten wir nicht an einem Software-Projekt zusammen.
Im Gegenteil. Ich schätze die Einsicht in anders lautende Meinungen anderer Leute, denn sie ermöglichen mir die Dinge aus unterschiedlichen Blickwinkeln zu betrachten und so mehr Wissen über das Thema zu erlangen, meine Ansichten zu revidieren und sie zu präzisieren.
Der Folgende Artikel ist also nur die Darstellung meiner Meinung über die Exceptions. Er enthält Erkenntnisse, die ich in der Diskussion mit VolkerGlave gewonnen habe. Für mich sind seine Erfahrungen und Ansichten wertvoll und ich freue mich, dass ich sie kennen lernen konnte. Hier also ein kurzer Einblick in meine Ansichten über die Exceptions. Sie sind nur my humble opinion :-)
Einsatz von Exceptions |
Mit einer Exception kann eine Methode signalisieren, dass sie ihre Aufgabe nicht erfüllen kann. Die Ursachen, warum die Methode ihre Aufgabe nicht erfüllen kann, können verschieden sein. Eine der Ursachen kann sein, dass eine der Methoden, die unsere Methode benutzt ihre Teilaufgabe nicht erfüllen können. Eine Andere, dass die vorhandenen Daten die Erfüllung der Aufgabe grundsätzlich nicht ermöglichen. Auch auf erkannte Programmierfehler kann man mit Exceptions reagieren, dazu mehr später.
Exceptions sollten nicht verwendet werden, um ein gewöhnlich erwartetes Ergebnis zu kommunizieren. Was ein normal erwartetes Ergebnis ist, und wann die Aufgabe einer Methode nicht erfüllt werden kann, ist oft Definitionssache. Ich möchte es an einem Beispiel verdeutlichen:
Nehmen wir an, dass unsere Methode aus einen Dictionary zu einem Schlüssel einen Wert zurückgeben soll. Was soll die Methode machen, wenn es zu dem übergebenen Schlüssel keinen Eintrag in dem Dictionary gibt? Soll sie NULL zurückgeben oder soll sie eine Exception werfen? Beide Vorgehensweisen haben ihre Berechtigung und die Entscheidung, welche man nehmen soll hängt davon ab, wie man das Dictionary betrachtet.
Sieht man es eher als ein assoziatives Array, von dem man vorher gespeicherte Daten auslesen will; ist also die Aufgabe der Methode zu dem Schlüssel, von dem erwartet wird, dass er einen Eintrag in dem Dictionary besitzt, den Wert zurückzugeben; dann würde ich bei einen nicht vorhandenem Schlüssel, eine Exception werfen. Denn die Methode (die ich in C++ operator[] nennen würde) kann bei nicht vorhandenem Schlüssel ihre Aufgabe nicht erfüllen.
Betrachtet man das Dictionary eher als eine Datenbank, in der bestimmte Einträge sein können aber nicht müssen; lautet also die Aufgabe der Methode eher ?Schau nach, ob wir einen Eintrag zu diesem Schlüssel haben, und wenn ja, gib mir den Wert zurück?, dann würde ich die Methode eher findEntry nennen, und bei nicht vorhandenem Schlüssel eine NULL als korrektes Ergebnis definieren, welches bedeutet ?Es gibt keinen Eintrag für diesen Schlüssel?.
Es gibt hier einen subtilen Unterschied zwischen den zwei Vorgehensweisen: Wenn die Methode findEntry NULL zurückgibt, bedeutet es, dass der Schlüssel definitiv keinen Eintrag im Dictionary hat. Wenn aber die Methode operator[] eine KeyNotFound-Exception wirft, bedeutet es bloß, dass das Nichtvorhandensein des Eintrages die Ursache des Scheitern sein könnte. Ich würde aber nicht versprechen, dass diese Exception ausschließlich nur dann geworfen wird, wenn der Schlüssel nicht vorhanden ist.
Es wäre also gegen meinen Geschmack, die Exception zu erwarten um zu überprüfen, ob der Eintrag existiert. Würde ich die Information brauchen, ob ein Eintrag in dem assoziativem Array mit einem Schlüssel existiert, würde ich lieber eine Methode hasKey neben operator[] zur Verfügung stellen, als KeyNotFound-Exception von operator[] zu fangen.
Für welche der zwei Varianten man sich entscheidet hängt davon ab, in welchem Kontext das Dictionary verwendet wird. Nur anhand der Kontextes kann man entscheiden, ob der Aufruf dict[key] oder dict->findEntry(key) der geeignete ist.
Ähnliche Situation haben wir bei der Division durch 0. Man kann bei reellen Zahlen definieren, dass das Ergebnis einer Division durch 0 die positive oder negative Unendlichkeit, oder bei 0/0 eine ?Nichtzahl? ist. Die positive und negative Unendlichkeit und die ?Nichtzahl? sind hier kein Indikator dafür, dass die Division gescheitert ist ? es sind nur speziell definierte Werte, die den Wertebereich der reellen Zahlen pragmatisch erweitern.
Bei ganzen Zahlen hingegen würde eine solche Entscheidung wahrscheinlich keinen Sinn haben, also würde ich bei ganzzahliger Division durch 0 eine Exception werfen um anzuzeigen, dass die Division gescheitert ist.
Eine Bemerkung noch: Dass eine Methode scheitert, beutet in diesem Kontext nicht automatisch, dass das Programm einen Fehler hat oder sich in inkonsistentem Zustand befindet, sondern nur dass die primäre Aufgabe der Methode nicht erfüllt werden konnte.
Exceptions und Fehlercodes |
Exceptions sind natürlich nicht die einzige Möglichkeit das Scheitern einer Methode anzuzeigen. Eine andere häufig verwendete Vorgehensweise ist es, einen speziell definierten Wert als Ergebnis zu benutzen, oder einen speziell für diesen Zweck definierten Ausgabeparameter zu benutzen in dem ein Fehlercode zurückgegeben wird.
Ich benutze lieber Exceptions, wenn es darum geht, anzuzeigen, dass die Methode ihre Aufgabe nicht erfüllen konnte; oder ich Formuliere die Aufgabe der Methode so, dass der ?Fehlercode? den normalen Wertebereich der Methode erweitert und so die Aufgabe der Methode nicht als unerfüllt gilt.
Ich sehe einen entscheidenden Vorteil bei der Verwendung von Exceptions:
Um das Scheitern einer Untermethode weiter an den Aufrufer zu kommunizieren, brauche ich nichts zu tun. Wenn ich die, von der Untermethode geworfene, Exception nicht fange, wird sie automatisch an den Aufrufer weitergeleitet. Bei Fehlercodes dagegen, muss jeder Aufruf einer Untermethode explizit überprüft werden.
Das heißt, dass die Methode ?in der Mitte? der Aufrufkette, die weder den Grund des Scheiterns feststellt, noch darauf irgendwie reagieren kann (außer selbst zu scheitern) die Fehlerbehandlung überhaupt nicht implementieren muss und trotzdem wird der Grund des Scheiterns an die behandelnde Stelle signalisiert.
Weitere Unterschiede ergeben sich eher aus der üblichen Implementierung der Exception und der Fehlercodes, es sind allerdings keine grundsätzliche Unterschiede.
Wenn die verwendete Programmiersprache keine Exceptions zuläßt, kann man sie natürlich nicht verwenden. Es gibt z. B. viele C?Bibliotheken, die mit Fehlercodes arbeiten. Wenn man diese Bibliotheken in C++ verwendet, ist man gezwungen, entweder mit den Fehlercodes zu leben oder eine Fassade zu der Bibliothek zu bauen.
Ein anderer Grund der für Fehlercodes spricht ist die Tatsache, dass die Exceptions einer Methode keine Möglichkeit geben, neben dem Erfolg noch zusätzliche Informationen an deren Aufrufer zu liefern. Mit Exceptions kann man nur das Scheitern einer Methode melden, nicht aber Warnungen oder Hinweise. Hier stehen sich aber nicht die Fehlecodes und die Exceptions als Alternativen gegenüber. Man kann hier die Fehlercodes (die ich hier lieber Statuscodes oder halt Zusatzinformationen nennen möchte) neben der Exceptions verwenden.
Der wichtigste Grund aber, der für die Fehlercodes spricht, ist die Komplexität, die die Exeptions in die Struktur eines Programmes bringen. Diesem Punkt möchte ich einen eigenen Abschnitt widmen.
Exceptions und die Verständlichkeit eines Programmes |
Den Exceptions wird vorgeworfen, dass sie den Ablauf einer Methode verschleiern, weil bestimmte Ausführungspfade (die, in welchen eine Exception geworfen wird) nicht explizit sichtbar sind und die Ausgangspunkte der Methode nicht immer erkennbar sind.
Dieser Vorwurf stimmt.
Bringen uns Exceptions Vorteile, die diesen Nachteil überwiegen? Die Antwort für mich lautet: JA!
Eine einfache Methode
|
hat ohne Exceptions nur einen explizit sichtbaren Ausführungspfad und nur einen Ausgangspunkt. Es werden die Methoden a(); b() und c() nacheinander aufgerufen, und dann wird der Wert 1 zurückgegeben. Wenn jede der Untermethoden eine Exception werfen kann, haben wir plötzlich vier Ausführungspfade und vier Ausgangspunkte.
Die oben skizzierte Methode enthält keine Fehlerbehandlung. Nur wenn wir davon ausgehen, dass die Methoden a(), b() und c() nicht scheitern können, braucht unsere einfache Methode keine Fehlerbehandlung. In dem Fall wird aber in a(), b() oder c() auch keine Exception geworfen und schon löst sich der Vorwurf der zusätzlichen Ausführungspfade und Ausganspunkte auf, denn es gibt keine.
Sollten wir aber eine Fehlerbehandlung brauchen, haben wir gleich ob mit oder ohne Exceptions immer umgefähr die selbe Anzahl der Ausführungspfade und der Ausgangspunkte. Die Exceptions bringen uns also grundsätzlich keine zusätzlichen Ausführungspfade und Ausgangspunkte, sie erlauben uns nur, diese nicht explizit formulieren zu müssen ? und sie zwingen uns an diese Ausführungspfade und Ausgangspunkte zu denken. Denn mit Exceptions sind sie immer da. Was man mit Exceptions explizit formulieren muss, ist das beabsichtigte Ignorieren des Scheiterns einer aufgerufenen Methode. Das ist ohne Exceptions das Standardverhalten ? es ist aber immer besser, das bewusste Ignorieren des Scheiterns einer Methode, deutlich zu kennzeichnen, damit klar ist, dass die Fehlerbehandlung nicht aus Versehen weggelassen worden ist.
Ist es ein Nachteil? Ist es ein Vorteil? Hier liegt die Entscheidung im Auge des Betrachters.
Mit Exceptions ist der Weg, der zum Erfolg einer Methode führt, also die Implementierung der Umsetzung der eigentlichen Aufgabe der Methode, viel deutlicher. Die Ausführungspfade des Scheiterns sind implizit und automatisch da. Anderseits, weil sie da sind, muss man immer mit diesen zusätzlichen Ausführungspfaden rechnen.
Ohne Exceptions ist dies ein korrekter C++ Code:
|
wenn a(pX) eine Exception wirft, haben wir ein Speicherleck.
Die Deutlichkeit, mit der der Erfolgspfad der Methode sichtbar wird, wird also mit der Notwendigkeit bezahlt, die finally-Blöcke (in z. B. Java, C#, Python usw.) oder speziell präparierte Klassen, die im Destruktor aufräumen (in z. B. C++) usw. zu schreiben.
Fazit: Die Exceptions verstecken die Ausführungspfade der Fehlerbehandlung in den Methoden, die weder den Ausnahmefall signalisieren noch die Fehlersituation behandeln. In einem Programm mit Exceptions, muss man sich vor Augen halten, dass es diese Pfade gibt und entsprechend die Struktur des Programms anpassen. Dafür wird der Erfolgspfad einer Methode deutlicher.
Es bietet sich eine Analogie zu virtuellen Methoden in einer objekt-orientierten Sprache an: Aus dem Quelltext einer Methode, die virtuelle Methoden aufruft, kann man nicht erkennen, welche konkrete Implementierung der virtuellen Methode aufgerufen wird. Diese Information ist nicht offensichtlich, sie ist versteckt. Trotzdem, nein, gerade deswegen, erhöht der Einsatz von virtuellen Methoden die Übersichtlichkeit des Quelltextes. Mit den Exceptions ist es ähnlich: gerade weil sie bestimmte Ausführungspfade verstecken, erhöhen sie die Übersichtlichkeit des Quelltextes, weil sie die wichtigen Abläufe verdeutlichen.
Programmierfehler, Assertions, Exceptions und Abbruch |
Bisher habe ich über Fälle geschrieben, in denen eine korrekt implementierte Methode in einem korrekt implementierten und konfigurierten Programm erkennt, dass sie ihre Primäraufgabe nicht erfüllen kann. Vollkommen korrekte Programme sind äußerst selten, und somit extrem teuer. Wir müssen also mit der Tatsache leben, dass Programme die wir schreiben Fehler haben werden.
Was soll ein Programm machen, wenn es erkennt, dass es fehlerhaft programmiert oder falsch konfiguriert ist?
Die Antwort hängt von vielen Faktoren ab: Die wichtigste Faustregel, an die man sich halten kann, formulierten die Pragmatiker Andy Hunt und Dave Thomas im Buch ?The Pragmatic Programmer? ( ISBN 0-201-61622-X): Dead Programs Tell No Lies.
Wenn ein Programm feststellt, dass es sich in einem inkonsistenten Zustand befindet, ist es besser das Programm zu beenden, als es weiterlaufen zu lassen und so z. B. die Datenbank durcheinander zu bringen, oder dem Patienten die falsche Dosis Strahlung zu geben oder andere noch schlimmere Katastrophen zu verursachen.
Nur wie bricht man Programme am besten ab? Und was ist überhaupt ?ein Programm??
Die Absicht, die hinter einem Programmabbruch steht, ist nicht die Arbeit des Programms so lange zu verhindern, bis der Fehler behoben ist; es ist nicht die Selbstkasteiung der Entwickler so lange von den Anwendern genervt zu werden, bis sie die vollkommene Software abliefern. Die Absicht ist: das Programm nicht in einem undefinierten Zustand laufen zu lassen, und Folgeschäden zu vermeiden ? das Ziel ist also das Programm wieder in einen definierten Zustand zu bringen. Und Neustart ist eine ziemlich sichere Methode, wie man in einen definierten Zustand zurückfinden kann.
Ist es aber die einzig mögliche Maßnahme?
Um diese Frage beantworten zu können, muss man sich zuerst überlegen, was ?ein Programm? überhaupt ist. Schließlich nennen wir Prozeduren und Funktionen (und somit auch Methoden) auch ?Unterprogramme?. Sind die installierten Anwendungen Unterprogramme des Betriebsystems? Gehören die Plugins und Skripte für ein Office-Programm zu diesem Programm? Soll ein Fehler in einem JSP-Skript den Webserver beenden?
Diese Fragen lassen sich nicht pauschal beantworten, sie sind aber auch nicht so wichtig. Die wichtige Frage lautet: Welchen Teil der auf dem Rechner laufender Software muss man beenden und neu starten, um in einen definierten Zustand zurückzukehren? Wie isoliert sind die Teile der Software, so dass ein Fehler in einem Teil den Zustand eines anderen Teiles nicht beeinträchtig?
Von der Antwort auf diese Fragen hängt dann ab, welcher Teil der Software beendet wird. Wird einfach ein 501 Statuscode in der HTTP-Antwort zurückgeschickt? Wird eine Anwendung beendet, oder nur ein Plugin? Oder wird einfach nur ein Objekt gelöscht und ein neues angelegt? Ergreift den Kernel die Panik oder wird der Bildschirm blau? Dies alles sind in entsprechenden Situationen die richtigen Antworten.
Betrachten wir jetzt den einfachen Fall, dass wir eine Anwendung schreiben, die wir im Programmierfehlerfall beenden wollen, und wollen dass deren Neustart sie in einen definierten Zustand bringt. Wenn uns die Exceptions zur Verfügung stehen, haben wir zwei Möglichkeiten. Entweder werfen wir eine ?unbehandelte? Exception, oder wird stoppen das Programm auf die brachiale Art mit abort, exit oder dergleichen.
Sofortiger Abort hat einen Vorteil ? das Programm wird sofort angehalten und kann keinen Schaden mehr anrichten, eine Exception dagegen kann doch irgendwo abgefangen werden ? vielleicht an einer Stelle, auf der ein Kollege mit catch(...), wo der Fehler verschluckt wird, in guter Absicht einen Pflasterstein auf den Weg zur Hölle gelegt hat.
Der sofortige Abbruch gleicht einem ehrenwerten Samurai, der sich seiner Unwürdigkeit bewusst wurde, und so entschloss er sich für ein Seppuku. Kein gut gemeinter catch(...) kann ihn dazu bringen, in einem undefinierten Zustand weiterzumachen und so möglicherweise seinem Meister noch mehr Schaden zuzufügen.
Allerdings hat der Abbruch auch Nachteile:
Bei einem sofortigen Abbruch kann die Anwendung ihr Zeug nicht aufräumen. Den belegten Speicherplatz gibt das Betriebsystem frei, es löscht auch die Locks an geöffneten Dateien. Wer löscht aber die temporären Dateien? Wer benachrichtigt den Webserver, dass die Session beendet ist? Eine geworfene Exception, erlaubt es der Anwendung in den finally-Blöcken und des Destruktoren einen geordneten Rückzug, in dem sie vor ihrer Wiedergeburt den Frieden mit der Welt schließen kann.
Ein weiterer Nachteil des sofortigen Abbruchs ist die Modularität des Programms. Eine Prozedur braucht nicht dass Wissen über den Umfang des Programms, in dem sie verwendet wird. Und wenn sie dieses Wissen nicht braucht, soll sie es auch nicht haben. Eine Prozedur, in der also ein Programmierfehler festgestellt wurde, soll nicht wissen, dass unsere Absicht in so einem Fall ist, die Anwendung zu beenden. Vielleicht wird sie irgendwann in einer Anwendung verwendet, die ihre Teile besser isoliert, und nur die kaputten Teile neu starten muss. Vielleicht schafft es diese Anwendung im catch(...) wirklich was Vernünftiges zu machen. Wenn die Prozedur aber den sofortigen Abbruch auslöst, macht sie so eine Anwendung unmöglich.
Deswegen ziehe ich, wenn ich einen Programmier- oder Konfigurationsfehler feststelle, immer eine Exception einem harten Abbruch vor. Ein catch(...) ohne throw, das nicht garantiert die Anwendung in einen definierten Zustand zurückbringt halte ich für ein sehr strenges CodeSmell.
[Hier endet die Besprechung.]
Diskussion |