Diese Lösung der „Exception Translation“ scheint elegant zu sein,
und wird von vielen Kollegen bevorzugt. Diese zeigt auch die
Diskussion bei heise.de.
Ich glaube nicht daran, dass es „die eine richtige“ Vorgehensweise bei der Softwareentwicklung gibt. Bestimmte Techniken werden allgemein befolgt, oder zumindest allgemein anerkannt und akzeptiert. Bestimmte sind umstritten - und das sicherlich nicht ohne Grund. Für bestimmte Projekte, bestimmte Entwickler oder Teams ist „Exception Translation“ das Richtige.
In folgenden Abschnitten beschreibe ich, warum ich damit meine Probleme habe und
viel lieber die RuntimeExceptions verwende. (Nicht zu verwechseln mit Errors,
ich verwende RuntimeExceptions als normale „erwartete“
Exceptions, nicht als Indikator dafür, dass sich das Programm in einem undefinierten oder
unreparierbaren Zustand befindet oder dass ein Programmierfehler vorliegt - ich verwende
sie mit der gleichen Semantik wie man Exceptions in z.B. C# verwenden würde).
In unserem Beispiel haben wir die Exception CustomerException in der
Domäne Kundenverwaltung vorgestellt. Diese domänenspezifische Exception kann
grundsätzlich zwei Ursachen haben.
Einerseits kann die Ursache tatsächlich in der Domäne Kundenverwaltung liegen.
Ein Beispiel für eine solche Ursache wäre, wenn man z.B. einen Kunden anlegen
würde, der noch nicht 18 ist, und unsere Geschäftsbedingungen würden sowas verhindern.
Die Methode createCustomer würde in diesem Falle eine CustomerException
werfen.
Obwohl aber die Ursache in der Domäne Kundenverwaltung liegt, kann es passieren,
dass der Fehler in einer anderen (technischer) Domäne festgestellt wird. Zum Beispiel
kann eine Verletzung eines Primärschlüssel bedeuten, dass eine Kundennummer bereits
vergeben wurde. In diesem Falle würde die Methode createCustomer
die SQLException abfangen und sie in eine CustomerException
„übersetzen“.
Anderseits aber kann das Problem tatsächlich in einer anderen Domäne liegen. Es kann sein, dass die Datenbank keinen Festplattenplatz mehr hat, oder dass sie einfach überlastet ist, oder dass der Datenbankserver gerade lichterloh brennt...
Wir haben zwar die Schicht, in der die Datenhalung geschieht, gekapselt und abstrahiert, aber Abstraktionen tendieren dazu, Lecks zu haben.
Zu diesem Thema hat vor einiger Zeit Joel Spolsky
einen interessanten
Artikel veröffentlicht.
Wenn der Datenbankserver brennt, kann man es kaum in als eine sinnvolle kundenverwaltungsspezische
Exception ausdrücken. Wir können zwar die SQLException abfangen und
eine nichtssagende CustomerException werfen (wie in unserem Beispiel).
Damit hätten wir aber den Leck in der Abstraktion nicht behoben, wir hätten ihn
nur verschleiert - um letztendlich dem Benutzer eine Fehlermeldung der Art
„Ein unerwarteter Fehler ist aufgetreten. [OK] [Cancel] [Dankeschön]“ zu präsentieren.
Damit beraubten wir den Benutzer der Chance den tatsächlichen Fehler schnell zu identifizieren und ihn eventuell zu beheben und mit dem Feuerlöscher in den Serverraum zu rennen :-)
Wenn ein Fehler auftritt, den wir nicht einer Exception die tatsächlich in unserer Domäne liegt, zuordnen können, sollten wir diese Tatsache nicht verschleiern. Wenn dieser Fehler eine checked Exception ist, können bzw. müssen wir sie zwar in eine andere übersetzen, wir sollten die ursprüngliche Exception nicht verschleiern, sondern sie in unsere neue Exception einbetten.
Dies scheint auch Sun erkannt zu haben, und seit der Version 1.4 des JDK enthalten
die Exceptions die Eigenschaft cause, die im Konstruktor gesetzt werden
kann und genau diesem Zweck dient.
Unser angepasster Quelltext würde also so aussehen:
public class JdbcCustomerProvider implements CustomerProvider {
public Collection<Customer> getCustomers() throws CustomerException {
try {
ResultSet rs = connection.executeQuery(...);
... usw. ...
} catch (SQLException sqle) {
throw new CustomerException("Datenbankproblem!", sqle);
} finally {
// JDBC-Objekte schließen
... usw. ...
}
}
}
Soweit so gut. Wir werfen zwar eine CustomerException, es ist aber
in Wirklichkeit keine. In Wirklichkeit ist es eine veschleierte
SQLException, die wir aber nicht direkt durchlassen dürfen, weil es
unser Kontrakt verbietet.
Wir haben also einen Weg gefunden, unseren Kontrakt zwar formal zu erfüllen,
tatsächlich umgehen wir ihn aber. Nicht gerade ein Zeichen hoher Moral, aber
was bleibt uns anderes übrig? Das System zwingt uns dazu zu mogeln.
Wäre SQLException nicht checked, würden wir sie ganz offen durchlassen,
wenn wir sie nicht in eine waschechte CustomerException umwandeln wüssten :-)
Den Kontrakt, der uns dazu verpflichtet keine SQLException zu werfen,
gibt es aus zwei Gründen: Wir wollen unserem Aufrufer die Mühe, sich mit JDBC zu
befassen, ersparen; und wir wollen die Quelltextabhängigkeiten des direkten Aufrufers
zu JDBC vermeiden. (Schließlich kann es sein, dass er gar nicht mit JDCB zu tun haben wird,
er kann nur den HttpCustomerProvider verwenden).
Die erste noble Absicht können wir aber, wie sich gezeigt hat, leider nicht erfüllen.
Die Abstraktionen haben Lecks und wir werden gezwungen entweder die SQLException
unbehandelt zu verschlingen und sie durch wenigsagende CustomerException
zu ersetzen; oder wir beugen unseren Kontrakt und „schmuggeln“ die SQLException
sowieso zu dem Aufrufer und wahrscheinlich noch weiter, um dem Benutzer den
Stacktrace anzuzeigen.
Die zweite Absicht ist erfüllbar und sie ist auch sehr wichtig. In den Quelltexten
der Kundenverwaltungsschicht sollten tatsächlich keine JDBC-Bezüge stehen, wenn sie
nicht unvermeidbar sind. Diese Absicht liese sich aber mit viel weniger Tipparbeit
erledigen, wenn wir die SQLException unchecked machen könnten.
OK, bei SQLException bleibt uns nichts anderes übrig, aber
wenn wir eigene Exceptions definieren, spricht meiner Meinung nach, kaum
etwas dafür, sie als checked Exceptions zu deklarieren.
Und somit kommen wir zu der fünften Möglichkeit, wie eine Methode mit einer checked Exception umgehen kann:
Um bei unserem Beispiel zu bleiben: Wäre die SQLException unchecked,
würde ich die Verletzung der Primärschlüssel in der Tabelle CUSTOMERS
immer noch in einen CustomerException mit der Meldung „Kundennummer
bereits vergeben.“ umwandeln, denn es würde sich tatsächlich um eine
Fehlermeldung aus der Domäne Kundenverwaltung handeln. (Vorausgesetzt, diese
Funktionalität wäre so spezifiziert. Man sollte ja nicht das Geld des Auftraggebers
verschwenden, und unspezifizierte Funktionalität implementieren.) Andere
SQLExceptions würde ich aber durchlaufen lassen.
Da die SQLExceptions aber nicht unchecked ist, bleibt mir in meinen
Quelltexten nichts anderes übrig, als sie abzufangen und sie in eine RuntimeException
einzubetten.
Dabei kann es sich je nach Bedarf der Anwendung um eine unspezifische SoftenedCheckedException
oder um eine spezifische SoftendedSQLException handeln.
Die checked Exceptions in Java ermöglichen das formelle Deklarieren eines Kontraktes zwischen dem Aufrufer und der Methode, in dem sich die Methode verpflichtet bestimmte Exceptions nicht zu werfen. Der Vorteil für den Aufrufer ist, dass er sich um solche checked Exceptions nicht kümmern muss.
Allerdings wird der Kontrakt in vielen Fällen nur formell eingehalten und die ursprünglichen checked Exceptions werden trotzdem geworfen, allerdings eingebettet in andere checked oder unchecked Exceptions. Die Verpflichtung des Kontraktes wird also häufig umgangen.
Der Aufrufer kann manchmal auf den Vorteil, den er aus einem solchen Kontrakt ziehen könnte, verzichten, weil er die Exception durchaus behandeln könnte, indem er einfach dem Benutzer eine Fehlermeldung anzeigt.
Für mich bedeutet es also, dass die checked Exceptions mir als Entwickler meistens mehr Arbeit als Nutzen bringen und deswegen vermeide ich deren Verwendung.
Da das Thema aber „umstritten“ ist, gehe ich davon aus, dass andere Entwickler andere Erfahrungen bezüglich des Kosten/Nutzen-Verhältnissen bezüglich checked Exceptions haben. Falls jemand diesen Artikel zu Ende gelesen hat, und andere Erfahrungen mit checked Exceptions gemacht hat, werde ich dankbar, wenn er sie mit mir teilt. :-)