// der php hacker

// archiv

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. '"');
	}
}
Geschrieben in Allgemein 13 Kommentare

#001
11. Mai 2009
Nils

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


#002
11. Mai 2009
knalli

In deinem letzten Beispiel solltest du noch den Parent-Konstruktor aufrufen ;)


#003
11. Mai 2009
Cem Derin

@Nils: Die Validierung ist auch nur ein Beispiel. In einer realen Anwendung würde man da entsprechend über Validator-Klassen arbeiten. Wichtig zu erwähnen wäre jedoch, dass “validate” eigentlich nicht im Interface steht. Um genau zu sein, wäre save in dem Fall die einzige Methode, die im Interface definiert würde.

@Knalli: Öhm, stimmt. Man sollte vor dem ersten Kaffee keine Ergänzungen machen ;-)


#004
11. Mai 2009

Es freut mich sehr, dass das Thema noch einmal aufgegriffen wurde. So ähnlich hatte ich es mir auch bei Nils vorgestellt. Leider ist er dann doch nicht so sehr darauf eingegangen.
Ihr seid schon ein gutes Team. ;)

Kleiner Tippfehler bei “Exception-Codes”: “Die Datenbank meldet diesm unser”.
Und im Fazit: “Geworfen werdne Exceptions aus”.


#005
11. Mai 2009
Nils

@cem: Ok, dann sollte sie aber auch private sein ;)


#006
11. Mai 2009
knalli

Vielleicht ist der Begriff “unerwartete Fehler” etwas zu abstrakt in der Verwendung (das ist kein Problem von dir, sondern ist halt so). Gemeint ist meist eher ein Fehlerzustand (vgl. Zustandsmodelle, fehlerhafter oder falscher Zustand) und der “Nichtzustand” ist sehr wohl bekannt.

Es ist also kein Problem weil der Programmierer nicht mit unerwarteten “Fehlern” im Sinne von “Bugs” konfrontiert wird, sondern von Ausnahmen, für die er sich nicht (mehr) zuständig fühlt. Überdies hinaus ist eine feine Abstimmung, welcher Fehler beim Speichern aufgetreten ist, wie du bereits erwähnt hast, wesentlich aussagekräftiger. Und wem das egal ist (gibt solche Situationen), der fängt halt nur eine Exception (bzw. in deinem Falle ggf. “MyException”) ab.

Meiner Meinung nach sollte man jedoch es vermeiden, seine API mit Exceptions-Throws zu überladen.. denn in einem wohlgeformten (hm, gibt es sowas? :D ) Programm muss man alle Exceptions abfangen.. das kann irgendwann leicht unübersichtlich und kontraproduktiv sein. Das greift auch Nils Einwand (aus seinem Blog) auf: Mit einer vernünftigen IDE (ehm, ich weiß jetzt nicht, ob das aktuell mit PHP und Eclipse o.a. auch geht) wird geprüft, ob alle Exceptions an Ort und Stelle abgefangen werden; wenn nicht, wird man dazu gezwungen. Vor NPEs ist man ohne bewusste Programmierung nie sicher.

Also nur dort, wo es auch Sinn hat. Wahlweise kann man für zentral wichtige Operatoren auch zweigleisig fahren, wie es bspw. bei Hibernate der Fall ist. Das Laden von Objekten kann einmal ohne, einmal mit Exceptions durchgeführt werden. Einmal kann null zurückkommen, das andere mal kommt immer ein Objekt zurück.


#007
11. Mai 2009
Nils

Wie gehst du eigentlich vor, wenn zwei Validierungsfehler auftreten, die du vll. im Userformular dann anzeigen willst?


#008
11. Mai 2009
Cem Derin

Ich sagte ja bereits, dass die Validierung lediglich ein Beispiel darstellt und exemplarisch für Template methods steht. In der Tat war das unglücklich gewählt. Um deine Frage zu beantworten: Ich Validiere mit Validator-Klassen die ggf. Fehler in ein Result-Objekt schieben.


#009
11. Mai 2009
Nils

Bei mir haben auch so viele gemotzt. Ich darf das also ;) Aber ich weiss jetzt auch wieder, warum ich mich an das Thema nicht so richtig rangetraut habe.

#010
29. Mai 2009

[...] versucht, den Nutzen von Exceptions zu erläutern. Der “PHP Hacker” hat eine gute Antwort dazu geschrieben und das Thema noch einmal von vorne erklärt. Unter beiden Artikeln finden sich [...]


#011
29. Mai 2009

Kann man also sagen, dass Exceptions geworfen werden, sobald man keinen Rückgabewert mehr braucht, bzw. sobald ein Fehler auftritt den man (ausser mit einem catch-Block) nicht weiter behandeln oder umgehen braucht?

Sehr interessante Thematik – Exceptions. Selbst arbeite ich nicht damit, weil ich leider – auch bis jetzt – nicht verstehe, wie man sie effizient und zielführend einsetzt.

Worin liegt der Vorteil, der Verwendung von Exceptions im Vergleich zur Verwendung von einfachen boolschen Rückgabewerten oder einem eigenen Fehler-Logging?

Gibt es zu diesem Thema noch andere Artikel?

Dankeschön! =o)

#012
01. Jul 2009

[...] Eine Exception muss erst einmal keinen Fehler darstellen. Dazu einfach mal ein Artikel von mir: Re: Wann verwende ich Exceptions | Der PHP Hacker — Noch ein PHP Blog [...]

#013
18. Okt 2010

[...] [...]

// kommentieren

// senden
theme von mir, software von wordpress, grid von 960 grid system. funktioniert in allen browsern, aber der safari bekommt das mit der schrift am schönsten hin.