Dienstag, 17. März 2009

Unbehandelte Ausnahmen als Zusicherungen

Wenn eine Methode nicht tun kann, was sie tun soll, teilt sie dies dem Aufrufer mit einer Ausnahme mit. Der Aufrufer kann diese Ausnahme mittels eines try ... catch ... Blocks behandeln, um auf eine definierte Weise zu reagieren. Nach dem Methodenaufruf geht die normale Ausführung weiter, während nach dem catch-Statement der im Ausnahmefall auszuführende Code folgt. Meist bekommt man in solchen catch-Blöcken allerdings nur wenig Phantasievolles zu sehen: Ein eigenes raise-Statement mit einer eigenen Ausnahme, den Aufruf eines Fehlersammlers, das Absetzen einer Meldung (meist ganz schlecht, da es nur im Dialog richtig funktioniert) oder einfach nur eine Kommentarzeile
try.
...
catch cx_root.
* Todo: Fehlerhandling
endtry.

Bei diesem Beispiel ist es obendrein gefährlich, dass alle Exceptions abgefangen und mit Stillschweigen übergangen werden: Wenn es zu irgendeiner Ausnahme kommt, hat man keinen Hinweis darauf, dass überhaupt irgendetwas schief gegangen ist – und keine Informationen zu der konkreten Ausnahme, die aufgetreten ist. Der Code Inspector dagegen schweigt zu dieser Verirrung, als hätte man durch eine derartige Ausnahmebehandlung die Codequalität gar noch verbessert!

Statt sie abzufangen, kann sich der Aufrufer auch entscheiden, die Ausnahme seinerseits nur weiterzureichen, indem er sie in seiner eigenen Schnittstelle deklariert. Damit ist nach dem Motto "Melden macht frei und belastet den Aufrufer" die Verantwortung einfach an den Aufrufer weitergereicht.

Wenn er weder das eine noch das andere macht, bekommt er eine Warnung von der Syntaxprüfung. Denn zur Laufzeit kann es zum Programmabbruch mit Kurzdump kommen, wenn die Ausnahme auftritt. In Zeiten, in denen Programmquelltexte regelmässig mittels Jobs auf ihre Qualität geprüft werden – in ABAP etwa mit dem Code-Inspector – könnte man sich gezwungen fühlen, allen so erscheinenden Warnungen nachzugehen und schliesslich gewissermassen an jeder Mülltonne eine Ausnahmebehandlung zu programmieren, was den Code aufbläht und unübersichtlicher macht.

Nur um die Syntaxprüfung zum Schweigen zu bringen? Das ist ein schlechter Grund! Manchmal sollte man dazu stehen, dass man gewisse Ausnahmen bewusst nicht abfängt, beispielsweise weil nachfolgende Verarbeitungen sinnlos werden, wenn die Ausnahme auftritt. Ein Programmabbruch wäre dann das erwünschte und korrekte Systemverhalten! Wenn beispielsweise ein für die Applikation zentrales Objekt zu Beginn der Anwendung von der Objektfabrik nicht beschafft werden kann, so wäre es sinnlos weiterzumachen: Der nächste Zugriff auf eine Methode oder auf ein Attribut des nicht vorhandenen Objekts führt zum Kurzdump OBJECTS_OBJREF_NOT_ASSIGNED. Warum also den Abbruch nicht gleich zu Programmbeginn auslösen – in Form eines Kurzdumps UNCAUGHT_EXCEPTION – bevor das Programm tiefer in die Verarbeitung einsteigt? Es erleichtert die Fehleranalyse, wenn ein Programm gleich bei der Ursache des Problems abbricht als irgendwann später, wenn bereits weitere - und im Ausnahmefall oft unerwünschte - Verarbeitungsschritte durchlaufen wurden.

Es ist meines Erachtens ein legitimes Vorgehen, sich bei der Programmierung bewusst für das Nichtbehandeln einer Ausnahme zu entscheiden. Dass man von der Syntaxprüfung gewarnt wird, bedeutet nichts weiter als eine Warnung. Es bedeutet nicht, dass das Nichtbehandeln von Ausnahmen den Code schlecht macht. Code wird umgekehrt auch nicht besser durch das stumpfe Einschieben von try... catch ... Blöcken. Eine Ausnahme bewusst nicht zu behandeln, kann die Bedeutung einer Zusicherung haben. Ähnlich wie man mit der Anweisung assert sicherstellt, dass eine bestimmte Bedingung erfüllt sein muss - und das Programm abbricht, wenn sie nicht erfüllt ist - so kann man durch das Nichtabfangen einer Ausnahme sicherstellen, dass eine bestimmte Methode ohne Ausnahme durchlaufen wurde - wenn dies zum Beispiel für die nachfolgende Verarbeitung der Rückgabewerte dieser Methode vorausgesetzt werden soll.

ABAP stellt drei Typen von Ausnahmen zur Verfügung. Der hier beschriebene Fall, dass eine Ausnahme nicht im Code explizit abgefangen werden soll, sondern das Programm beim Auftreten der Ausnahme abbrechen soll, sollte den häufigsten Fall von Ausnahmen darstellen. Er entspricht den dynamisch geprüften Ausnahmen: Das sind Ausnahmen, die von der Klasse CX_DYNAMIC_CHECK erben. Die Syntaxprüfung schweigt zu diesen Ausnahmen: Wird eine Methode ausgerufen, die eine dynamische Ausnahme auslöst, so muss diese weder mit CATCH behandelt noch in der Signatur der aufrufenden Methode deklariert werden (sie kann aber, wenn man das wünscht, behandelt oder deklariert werden). Ein Beispiel ist die Division durch Null: Man will nicht bei jeder Divisionsoperation die Ausnahme CX_SY_ZERODIVIDE abfangen müssen, nur weil es möglich ist, dass der Nenner Null werden kann. Die Programme wären schnell unlesbar, wenn alle möglichen Ausnahmen abgefangen werden müssten. CX_SY_ZERODIVIDE erbt daher von CX_DYNAMIC_CHECK. Man kann die Division durch Null abfangen, muss es aber nicht. In der Regel erwartet man in dieser Situation schlicht, dass das Programm abbricht (weil man beim Programmieren denkt: Dieser Fall kann gar nicht auftreten!), weil z.B. eine Fehlerkorrektur nötig ist: Der Nenner "dürfte eigentlich nie Null sein" - wenn er es doch ist, gab es offenbar an früherer Stelle, als der Nenner bestimmt wurde, einen Programmfehler.

Neben den dynamischen Ausnahmen gibt es noch die deklarationspflichtigen Ausnahmen, die von CX_STATIC_CHECK erben. Ein typisches Beispiel wäre eine Ausnahme ZCX_FILE_OPEN_ERROR beim Öffnen von Dateien. Dies ist zwar nicht der Regelfall, sondern wirklich eine Ausnahme - andererseits ist sie nicht vom Typ einer Zusicherung: Es kann durchaus passieren, dass eine Datei nicht geöffnet werden kann, z.B. weil sie gerade von einem anderen Benutzer bearbeitet wird. Für diesen Fall muss der Aufufer der Methode, die die Datei öffnet, ein Verhalten vorsehen. Daher ist es gut, eine solche Ausnahme für deklarationspflichtig zu erklären.

Ein dritter Typ von Ausnahmen sind diejenigen, die von CX_NO_CHECK erben. Sie dürfen zwar mit einem CATCH-Block behandelt werden, dürfen aber nicht in Methoden deklariert werden, da CX_NO_CHECK bereits implizit in jeder Methode deklariert ist. Von diesem Typ sind Ausnahmen, die einen Cross System Concern betreffen, z.B. einen Mangel an Hauptspeicher. Es wäre sinnlos, eine solche Ausnahme in jeder Methode des Aufrufstacks deklarieren zu müssen, nur damit sie an oberster Stelle behandelt werden kann. Ausnahmen von Typ CX_NO_CHECK werden also automatisch durch alle Stackebenen hindurchgereicht, bis sie schliesslich in einem CATCH-Block behandelt werden (oder das Programm abbricht, wenn es keinen solchen Behandler gibt).

Fast immer empfiehlt sich das Nichtabfangen in Testcode: Die Modultestlaufzeit fängt selbst die Exception cx_sy_no_handler ab, die bei unbehandelten Ausnahmen von der ABAP-Laufzeit ausgelöst wird. Die betreffende Testmethode, in der die unerwartete oder unerwünschte Ausnahme ausgelöst wurde, geht dann auf NOT OK. Dies ist eine elegante Möglichkeit, mit wenig Coding sicherzustellen, dass in bestimmten Contexten eine Ausnahme nicht auftritt.

Nehmen wir einmal nur zu Demonstrationszwecken an, wir hätten überflüssigerweise das Rad neu erfunden und eine Quadratwurzelfunktion neu definiert. Diese löse beispielsweise die Ausnahme zcx_parameter_error aus, wenn das übergebene Argument eine negative Zahl ist. Dann hätten wir vielleicht eine Testmethode

method test_4.
data: lv_result type f.
lv_result = go_ref->sqrt( 4 ).
assert_equals_f( act = lv_result
exp = 2 ).
endmethod.


Mit dieser Methode haben wir nicht nur sichergestellt, dass eine beispielhafte Quadratwurzel richtig berechnet wird, sondern auch - ohne dass man das im Coding sieht - dass die Methode nicht etwa die Ausnahme zcx_parameter_error auslöst. Würde sie das tun, würde die Methode test_4() in der Testübersicht im Status "Nicht OK" erscheinen. Dass die Ausnahme ausgelöst würde, wäre am Diagnosetext für die Methode test_4() ersichtlich. Wie haben wir diese zusätzliche verborgene Zusicherung programmiert? Einfach durch Weglassen der Ausnahmebehandlung! Weniger Code - grössere Lesbarkeit und in diesem Fall sogar noch zusätzliche Funktionalität!

Ein anderer Test könnte sicherstellen, dass die Ausnahme für ungültige Argumente auch wirklich ausgelöst wird:
method test_minus.
data: lv_result type f.
try.
lv_result = go_ref->sqrt( -1 ).
fail( 'Ausnahme zcx_parameter_error nicht ausgelöst' ).
catch zcx_parameter_error.
* OK
endtry.
endmethod.

Diese Testmethode löst keine Failure aus, wenn das Wurzelziehen von negativen Zahlen wirklich mit der erwarteten Ausnahme zcx_parameter_error geahndet wurde. Denn dann wird die Verarbeitung im (leeren) catch-Block fortgesezt. Wenn aber sqrt() keine Ausnahme ausgelöst hat, wird die Testmethode mit einem fail() beendet.

2 Kommentare :

Timo John hat gesagt…

Hallo Rüdiger,
ich ergreife jetzt einfach mal das Du, so als Kollege im Abapumfeld...

Gerade war ich auf der Suche nach Informationen über zum Thema Assertions in ABAP. Im Moment lese ich gerade einen interessanten Artikel über Assertions und checkpoint statements: http://www.sapnow.cn/upload/7871465e846771cf9.PDF

Allerdings blicke ich noch nicht so richtig wann ich am besten Assertions und wann doch lieber Exceptions in meinem Coding einbaue. ggf. können wir uns da ja mal austauschen...

Schöne Grüße Timo John.

Rüdiger Plantiko hat gesagt…

Hallo Timo,

Assertions sollen verwendet werden, um sicherzustellen, dass das Programm nicht in einen inkonsistenten Zustand gerät. Mit Exceptions kann man das nicht sicherstellen, denn der Aufrufer Deines Codes könnte die Exception ja abfangen und einfach weitermachen. Exceptions sind also ein Stück weit "regulärer" als Assertions. Assertions sind ein etwas flexiblerer Ersatz für die X-Messages, bei denen man das Programm mit einem Kurzdump davor bewahrt, inkonsistente Daten weiterzuverarbeiten und damit noch schlimmere Inkonsistenzen zu produzieren. Assertions sind für Programmstellen gedacht, bei denen man sich beim Entwickeln denkt "Hier darf er nie hinkommen".

Etwas anderes ist es zu sagen: "Das darf unter keinen Umständen in diesem Feld stehen": Wenn man zu Beginn einer Methode einen Importparameter gegen seine Domänenfestwerte verprobt, sollte man eine Exception (ich habe hierfür ZCX_PARAMETER_ERROR kreiert) auslösen, denn aufgrund des frühen Zeitpunkts der Verprogung - noch vor der Verarbeitung - ist ja meist noch kein Schaden angerichtet.

Gruss,
Rüdiger