Re: Wann verwende ich Exceptions
Geschrieben am 11. Mai 2009 von Cem Derin
Kurz vor dem Redesign meines Blogs habe ich „Ein Herz für Blogs“-Artikel gesagt, dass ich öfter auf die Blogosphäre eingehen möchte. Bisher habe ich das auch ein paar mal getan. Leider ist es immer etwas schwierig, da die meisten Themen entweder in den Artikeln schon sehr gut abgehandelt werden, und ich nicht mehr als einen Verweis hier hin schreiben könnte, oder sie sind für mich uninteressant – sei es wegen zu niedrigem Niveau (was nicht abwertend gemeint ist) oder weil sie Dinge behandeln, zu denen ich nichts sagen kann.
Heute ist das aber anders: Vor ein paar Tagen fragte sich Nils, wie man wohl Exceptions korrekt verwendet. Jetzt finde ich endlich mal genug Zeit, meine Meinung zu dem Thema niederzuschreiben.
Nils versuchte sich mit folgendem Grudsatz aus der Affäre zu ziehen
Excpetions (Ausnahmen) werden immer dann geworfen, wenn etwas unerwartetes passiert.
Die meisten gestandenen Entwickler werden dem Zustimmen. Die meisten Anfänger werden sich fragen, wie man unerwartete Vorfälle berücksichtigen kann. Diese werden dann müde belächelt, Erklärungen bleibt man ihnen dann aber meist dennoch schuldig. Nils immerhin gibt sich Mühe, eine Reihe solcher Ereignisse beispielhaft aufzuzählen. Am Ende weiß er dann aber selbst nicht, was man für eine Regel an die Hand geben könnte. Ich ich könnte wetten, die meisten Entwickler sind etwas ratlos, was das Thema Exceptions angeht.
Versuchen wir also einfach einmal, zu erörtern, wie man Exceptions am besten einsetzen könnte. Vorher will ich aber mit einer ziemlich falschen Behauptung aufräumen: Exceptions stellen keine unerwarteten Ereignisse dar, sondern Ausnahmen (wie der Name ja auch sagt). Somit hat sich der Hirnknoten, wie man unerwartetes Berücksichtigen kann hoffentlich auch aufgelöst.
Wer die Kommentare in Nils Beitrag gelesen hat, dem wird nicht entgangen sein, dass ich dort der Verwendung von Exceptions für strukturelle Prüfungen wie Nils negativ gegenüberstehe. Diese Meinung lege ich an dieser Stelle (vorläufig) ab (das sollte man übrigens öfter mal tun, wenn man einen Objektiven Blick auf ein Thema erhalten will).
Alles ist eine Klasse – Jeder Fehler eine Exception
Ich persönlich finde den Ansatz, dass alles durch eine Klasse repräsentiert wird ziemlich bequem. Beherzigt man diesen Grundsatz konsequent, kann man verdammt flexible Anwendungen bauen. Refactoring wird auf einmal extrem bequem, und wenn man dann noch das Template Pattern benutzt, ist man komplett auf der sicheren Seite. Es wäre nur Konsequent, wenn man hier Fehlerfälle durch Exceptions abbildet. Aber wie macht man so etwas richtig?
Einfach zu sagen „Prüfungen liefern nicht mehr true oder false sondern werfen von nun an eine Exception, wenn etwas nicht zutrifft“ ist leider etwas zu einfach. Nehmen wir das Beispiel einer User-Klasse: Wollen wir wissen, ob der Benutzer online ist, werden wir dafür eine Methode bemühen, die etwas isOnline heißen könnte. Ist der Benutzer nicht online, stellt dies natürlich keine Ausnahme dar. Hier ist ein Boolescher Wert (oder wenn man ganz penibel sein möchte auch die Repräsentation durch eine Klasse) vollkommen in Ordnung.
Nehmen wir aber an, dass wir Änderungen den Stammdaten eines Benutzers vorgenommen haben und diese nun speichern wollen. Dann haben wir für so etwas sicherlich eine Methode die save heißen könnte. Natürlich könnte es vorkommen, dass aus verschiedenen Gründen der Speichervorgang fehl schlägt: Die Datenbank ist plötzlich nicht mehr verfügbar, die angegebenen Daten können nicht eingetragen werden (vielleicht weil gegen Constraints verstoßen wird).
In diesem Fall einen Booleschen Wert als Indikator zu liefern klingt erst einmal plausibel. Der entsprechende Code könnte ungefähr so aussehen
if($user->save()) {
// Der Speichervorgang war erfolgreich
} else {
// Der Speichervorgang war nicht erfolgreich
}
Eine Reihe von Gründen sprechen allerdings gegen ein solches Vorgehen. Legt man beispielsweise Wert auf ein Fluent Interface (das bedeutet, dass eine Methode wenn möglich immer die Instanz des eigenen Objektes zurückgibt), ist dieser Ansatz schon einmal nicht möglich. In diesem Fall bleibt uns nichts anderes übrig, als mit Exceptions zu arbeiten.
Folgender Code könnte verwendet werden
try {
$output = $user->save()->getId();
echo 'Der User hat die ID: '. $output;
} catch(UserException $e) {
echo 'Bitte prüfen Sie Ihre Angaben';
} catch(DbQueryException $e) {
echo 'Bitte prüfen Sie Ihre Angaben';
} catch(DbException $e) {
echo 'Leider ist die Datenbank zur Zeit ausgefallen. Versuchen Sie es zu einem späteren Zeitpunkt noch einmal'.
} catch(Exception) {
echo 'Ein unbekannter Fehler ist aufgetreten. Wir arbeiten 'dran';
}
Das Programm versucht das Benutzerobjekt zu speichern und liest direkt die ID aus, um diese später an den Benutzer auszugeben. Wird in diesem Prozess nun eine Exception geworfen, gibt es eine Reihe von Fängern – je nach dem, welche geworfen wird. Man kann hier also auch eine tolle Fallunterscheidung treffen. Für den Fall, dass eine Komponente einen nicht dokumentierten oder hier evtl. tatsächlich nicht erwarteten Fehler in Form einer Exception auslöst, sollte man diesen Fall auch immer vorsehen und einen Catch-Block für die Basisklasse erstellen.
Unter der Haube
Innerhalb der Userklasse könnte der Code ungefähr so aussehen:
class User {
public function save() {
$this->validate();
$this->writeToDb();
return $this;
}
public function validate() {
if(!$this->isValidUserName($this->data->username)) {
throw new UserException('Username not valid');
}
}
public function writeToDb() {
$this->databaseAdapter->query($this->createInsertQuery());
}
}
Wir sehen, das in der Methode validate durchaus ein Boolescher Wert ausgewertet wird. Die Answendung macht hier insofern Sinn, dass hier eine Prüfung stattfindet. Im Falle der Methode save wird angenommen, dass alle Daten korrekt sind, unabhängig von einer evtl. erneuten Prüfung.
In der Methode writeToDb sehen wir, dass ein Query erzeugt wird, welcher an einen Datenbankadapter weitergereicht wird. Dieser wird im Falle eines Fehler eine Exception werfen. Diese fangen wir absichtlich nicht innerhalb der Modellklasse.
Exception-Codes
Nehmen wir nun einfach einmal an, beim Speichern unseres Benutzerobjektes wurde gegen einen Contraint in der Datenbank verstoßen. Die Datenbank meldet dies, unser Adapter wird eine Exception werfen. Wir wissen nun, dass etwas schief gelaufen ist, wir wissen auch ziemlich genau wo. Aber wir wissen nicht, was genau muckiert wird.
Die erste Überlegung die sicherlich nun viele haben (und die ich leider auch schon in Produktivsystemen angetroffen habe) ist, dass man Informationen in der Message des Exception-Objekts ablegt. Das ist zwar durchaus möglich und das habe ich auch schon gesehen, aber das ist eine eher unsaubere und unschöne Methode.
Wenn ich die Message einer Exception auslese, will ich, dass diese in einem Menschenlesbaren Format in einer definierten Sprache (und explizit nicht in einer Übersetzung) vorliegt. Ich will, dass sie für mich als Entwickler Aussagekräftig ist und nicht so weit verallgemeinert wurde, dass ein Endbenutzer auch noch etwas damit anfangen kann.
Wie bekomme ich nun also Informationen in die Exception verpackt, mit der das Programm arbeiten kann um so stets qualifizierte Fehlermeldungen erzeugen zu können? Ein kleiner Blick in das PHP-Handbuch hätte es verraten: Exceptions bieten die Möglichkeit neben einer Message auch einen Fehlercode zu hinterlegen. Dieser kann Eindeutig gemacht werden und so in catch-Blöcken detaillierte Fehlerausgaben ermöglichen.
Fazit
Während unserer kleinen Übrlegung hat sich folgendes herauskristallisiert: Es ist ziemlich entscheidend, an welcher Stelle Exceptions geworfen und an welcher sie gefangen werden. Konkret kann man resumieren: Geworfen werden Exceptions aus tieferen Programmschichten, gefangen und ausgewertet in höheren. Auf das MVC-Muster angewandt könnte man sich als Faustregel festhalten, dass Exceptions aus Modellen oder tieferen Hilfsklassen geworfen werden und von Controllern oder entsprechenden Hilsklassen gefangen werden.
Somit klärt sich neben dem Wie nun auch das Wo.
Das Problem Debugging
Nun haben wir doch eigentlich einen ziemlich klaren Anwendungsbereich für Exceptions erarbeitet. Trotzdem gibt es ein kleines Problem: Das fangen von Exceptions kann dazu führen, dass auftretende Fehler evtl. nicht mehr korrekt ausgegeben werden. Aus diesem Grund ein kleiner Kniff auf meiner Trickkiste: Exceptions mitloggen.
In einem Projekt solltet ihr für jede Klasse eigene Exceptions anlegen, die sich allerdings auch wieder von einer eigenen Exception Klasse ableiten. Diese könnte dann ungefähr so aussehen:
class MyException extends Exception {
public function __construct($message, $code = null) {
parent::__construct($message, $code);
MyLogger::log('Exception thrown with message "'. $message. '"');
}
}
#001
Moin Cem,
erstmal danke, dass du meinen Artijkel aufgreifst. Das wie, wann und warum von Exceptions in Worte zu fassen ist wirklich nicht so einfach und die Erklärung:
“Excpetions (Ausnahmen) werden immer dann geworfen, wenn etwas unerwartetes passiert.”ist wahrscheinlich wirklich eine der besten, auch wenn dein Beispiel sich nicht dran hält. Ein öffentliches Interface, das “validate” als Methode hat, sollte doch erwarten, dass etwas einmal nicht valide ist, ansonsten würde diese Methode nicht so viel Sinn machen.
Dass man beim Speichern eine Exception wirft finde ich auf jeden Fall richtig, nur den Ansatz mit dem validieren mag ich nicht so. In meinen Klassen kommt an dieser Stelle ein ValidationObject zurück. Auf das Frage ich vor dem Speichern auch ab und wenn es nicht “positiv” war, so werfe ich eine Exception.
Das war’s auch schon
Gruß,
Nils