OOP Error-/ExceptionHandling
Philipp Hasenfratz
- perl
Halihallo alle
Bei der Modellierung eines Error-/ExceptionHandlings habe ich einige
Vorschläge bezüglich der Umsetzung, die mir allesammt nicht so ganz
gefallen. Ich hoffe, ihr habe hier einige Vorschläge zur Verbesserung
oder ein anderes Verfahren, das ihr einsetzt:
Die von Perl mitgelieferten Funktionen croak/die sind zwar schön für
die Konsole; taugen aber in einer grossen Applikation nicht sehr
viel, da die Fehler abgefangen und verarbeitet werden sollen.
Java bietet hierfür try-catch-finally-Konstrukte an, die vom
Grundgedanken her ganz OK sind. Wie setzt man etwas derartiges am
besten in Perl um?
Nun, das von Perl mitgelieferte Pendant wäre eine eval-die
Konstruktion im Stile:
sub t { die('Error processing foo.'); }
eval { t() };
if ($@) {
# handle Exception 'Error processing foo.'
}
Dies ist zwar schon schön und gut, ist jedoch für grosse
Applikationen etwas starr, "funktional" und zudem langsam.
Zudem wäre ich auch froh, wenn die Fehlermeldungen irgendwo
gespeichert werden, da diese z.B. in einer Fehlerseite dann
ausgegeben werden sollen.
1. Vorschlag:
Ein etwas "objektorientierterer" Ansatz wäre ein Instanzenattribut
errstr und errcode, um die Beschreibung bzw. den Fehlercode in der
Instanz zu speichern. Dazu einige vererbte Methoden getErrorDesc(),
isError(), getErrorCode().
Nachteile bei diesem Vorgehen sind folgende:
- Fehler innerhalb des Constructors new sind nicht abzufangen, da
noch gar keine Instanz angelegt ist.
- Fehler müssen umbedingt abgefangen werden, ansonsten gehen sie
irgendwann verloren.
2. Vorschlag:
Um die Nachteile vom 1. Vorschlag auszumerzen kämen einige statische
Klassenvariablen zum Einsatz. Die Methoden getErrorDesc(), isError(),
getErrorCode(), getNextError() etc. wären Klassenmethoden und würden
auf die Klassenvariablen zugreifen. Somit wäre auch ein Fehler im
Construktor abzufangen und die Fehler würden während des ganzen
Programmes erhalten bleiben. Problem hier: Man kann zwischen
Fehlern einzelner Objektinstanzen nicht mehr unterscheiden.
3. Vorschlag:
Jede Klasse erbt von der Klasse ErrorSuccessful, welche eine Methode
isError() definiert, welche stets den Wert undef (falsch)
zurückliefert. Falls jedoch in einer Methode ein Fehler auftritt,
wird statt des normalen Rückgabewertes eine neue Instanz der Klasse
Error zurückgegeben, welche nun die Methoden und Variablen getError
(), getErrorCode definiert.
Somit wäre folgendes denkbar:
package Person;
use base qw(ErrorSuccessful);
sub new {
my ($class,$name) = @_;
if (!$name =~ /[1]+$/) {
return new Error('ERR_NO_VALID_NAME');
} else {
return $class->SUPER::new(name=>$name);
}
}
package main;
$person1 = new Person('Hasenfratz');
$person2 = new Person('98745jdfs.,ö.');
if ($person1->isError()) {
# wird nicht aufgerufen, da "Hasenfratz" syntaktisch korrekt.
}
if ($person2->isError()) {
die($person2->getErrorDesc());
# wird ausgegeben, da invalide characters im Namen.
# getErrorDesc definiert, da die Klasse nicht Person ist, sondern
# Error
}
---------------------
Probleme bei diesem 3. Vorschlag:
- Jede Klasse erbt von ErrorSuccessful was schlicht unglücklich
ist.
- Wenn man den Fehler nicht abfragt (eg. wenn er egal ist), läuft
man gefahr eine Methode aus Person zu verwenden, welche in der
Klasse Error natürlich nicht definiert ist.
- Der Rückgabewert müsste der Einfachheitshalber stehts ein Objekt
(also eine blessed reference) sein.
4. Vorschlag:
"Back to the roots": eval-die-Konstrukt:
sub test() {
die( Error->new(
'ERR_WRONG_CHARACTER', 'Falsches Zeichen in Name.'
)->toString() ); }
eval { test() };
if ($@) {
my ($err_code, $err_msg, $err_line, ...) = Error->parseError($@);
# do something with $err_code, $err_msg, ...
}
Erklärung:
Man könnte eine Klasse Error implementieren, welche aufgrund der
Argumente des Constructors per toString() eine 'die'-Fehlermeldung
produziert. Diese kann über eval {} abgefangen werden und per
parseError() Methode wieder in die einzelnen Komponenten aufgelöst
werden. Dies käme einem try-catch-finally-Konstrukt schon sehr nahe.
Nachteil: Die Fehlermeldungen werden nirgens gespeichert und müssten
manuell weitergegeben werden. Zudem sind die eval {} Konstrukte
langsam, da Perl immer den Parser anwerfen muss.
Ein weiterer Nachteil ist die (beinahe) Unmöglichkeit mehrere
Fehlermeldungen zurückzugeben. Nehmen wir z.B. ein HTML-Formular,
welches Daten eines Benutzers enthält. Es könnte der Name falsch
sein, die Adresse, der Wohnort, die E-Mail, ... Mir wäre eben auch
die Lösung lieb, dass mehrere Fehlermeldungen zusammen verwaltet
werden und diese in einer Fehlerseite dann allesammt aufgelistet
werden. Bei den anderen Vorschlägen könnte man hierfür eine Art
ErrorList implementieren, worin man über getNextError() an die
nächste Error-Instanz käme.
-----------------
So, was meint ihr dazu? - Habt ihr noch eine andere gute Idee? - Wie
löst ihr dies in grösseren Applikationen, die "empfindlich auf Fehler
reagieren und nicht einfach gleich mit "Fehler: ..." antworten"?
Ich hoffe, ich konnte mein Anliegen halbwegs beschreiben, sodass man
es versteht...
Viele Grüsse
Philipp
A-Za-z ↩︎
Hallo Philipp,
noch nichts von SIG__DIE__ gehört? :) Ich persönlich arbeite viel so:
$SIG{__DIE__} = &fatal;
open DAT,"blahr" or die "open blahr: $!\n";
close DAT;
sub fatal {
my $err = shift;
# tu was
exit;
}
Du kannst auch gern eine Objekts-Instanz oder so übergeben:
$SIG{__DIE__} = &fatal;
open DAT,"blahr" or die new ErrorClass("errortext");
close DAT;
sub fatal {
my $err = shift;
print $err->toString;
exit;
}
package ErrorClass;
sub new {
my ($class,$err) = @_;
return bless {err => $err},ref($class)||$class;
}
sub toString {
return shift->{err};
}
Geht eigentlich recht angenehm und braucht nicht diese ekeligen
eval-Konstrukte. Du kannst $SIG{__DIE__} sogar lokal überschreiben:
{
local $SIG{__DIE__} = 'IGNORE';
# ...
}
Ich hoffe, das hilft. Vorsicht aber: auch in eval()-Blöcken wird
der Callback von die() ausgeführt! Als ggf. überschreiben per local.
Grüße,
CK
Halihallo Christian
noch nichts von SIG__DIE__ gehört? :) Ich persönlich arbeite viel so:
Die Idee ist wirklich sehr gut, nur bei der Umsetzung in einem OOP-
Kontext habe ich einige kleine Bedenken (die Vorteile überwiegen
jedoch).
Geht eigentlich recht angenehm und braucht nicht diese ekeligen
eval-Konstrukte. Du kannst $SIG{__DIE__} sogar lokal überschreiben:
Deine Lösung umgesetzt, würde in etwa folgendes Konstrukt als Äquivalent für try-catch-finally vorsehen:
sub doSomething($) {
my ($self) = @_;
local $SIG{__DIE__} = sub { $self->classErrorHandler(@_); }
# do whatever throws probabely an exception...
}
sub classErrorHandler($$) {
my ($self,$error_message) = @_;
# do something with $error_message
}
oder aber:
sub doSomething($) {
my ($self) = @_;
local $SIG{__DIE__} = sub {
my ($err) = @_;
print($err->getErrorDesc() . ' at line: '.$err->getLine());
exit(255);
}
# do whatever throws probabely an exception... eg:
die( new Error('ERR_NO_VALID_CHARACTER', 'ä ist ungültig') );
}
Randbemerkung: Das ErrorHandling operiert nicht ausschliesslich im
package main, sondern auf Objects und Klassen. Deshalb machen
allgemeine (globale, staatische Funktionsaufrufe) Konstrukte wie $SIG
{__DIE__} = &fatal; wenig Sinn; aber das Umsetzen in OOP kannst du
ja ruhig mir überlassen :-)
Hm. Die zweite Version wirft nicht wesentlich mehr Overhead für das
ErrorHandling auf. Was mich daran etwas stört ist
a) das ErrorHandling wird *vor* dem eigentlichen Fehlerverursacher
definiert. Dies halte ich für Verwirrend und unschön. Der
Programmfluss wird "verschleiert" und verschlechtert die
Lesbarkeit des Codes.
b) Signale dieser Art haben in OOP absolut nichts zu suchen; jedoch
ist dein Vorschlag einem eval-Konstrukt dennoch vorzuziehen.
c) in der ersten Version braucht man eine separate Klassenmethode
für das ErrorHandling, aber verschiedene Methoden brauchen u.U.
auch verschiedene Verarbeitungsvorschriften für die Exceptions.
Natürlich könnte man jeweils den Methodennamen als Parameter
übergeben, oder caller() auswerten, jedoch ist dies viel zu viel
Overhead.
Oder man schreibt für jede Methode eine eigene ErrorHandler-
Methode, was jedoch viel zu viel Overhead bedeutet.
=> die erste Version ist untragbar.
Was ich an dieser Lösung sehr schön finde ist:
a) Es ist ziemlich einfach und universell.
b) Es generiert nicht allzuviel Overhead (Tipparbeit)
c) der Signalhandler wird trotz umschreiben des Signals automatisch
wiederhergestellt, der Programmfluss ist also "stabil". Thanks
to local. Alles andere wäre böse[tm].
d) kein böses eval
e) Das local $SIG{__DIE__} Verfahren ist selbst bei verschachtelten
Aufrufen noch korrekt, da der Kontext der Fehlerbehandlung im
Methoden-Scope stattfindet. Alles andere wäre böse[tm].
Ich hoffe, das hilft. Vorsicht aber: auch in eval()-Blöcken wird
der Callback von die() ausgeführt! Als ggf. überschreiben per local.
Wieso? - Ich zähle sogar darauf, dass er ausgeführt wird, ansonsten
sehe ich die Fehlermeldung nicht...
-------------
Vielen Dank Christian, das war wirklich ein sehr interessanter und
guter Vorschlag! - Obgleich unschön... Aber das hängt nicht an uns,
sondern an Perl, leider...
Viele Grüsse
Philipp
Halihallo Christian
Hm. Die zweite Version wirft nicht wesentlich mehr Overhead für das
ErrorHandling auf. Was mich daran etwas stört ist
a) das ErrorHandling wird *vor* dem eigentlichen Fehlerverursacher
definiert. Dies halte ich für Verwirrend und unschön. Der
Programmfluss wird "verschleiert" und verschlechtert die
Lesbarkeit des Codes.
d) Eigentlich würde ich sehr gerne auf derartige Perl-spezifische
Konstruktionen verzichten, da sie nicht "universell" und 1:1 in
andere Programmiersprachen umgesetzt werden können. Im Sinne
einer guten Konzeptionierung und leicht portierbaren Lösung hätte
ich eine andere Lösung bevorzugt.
Leider haben alle - mir in den Sinn kommenden - Lösungen den
selben Nachteil: Aufgrund der verwendeten (Primär-)Sprache Perl
bzw. dem "kleinsten gemeinsamen Nenner" vieler Sprachen gibt es
keine allgemein mögliche Lösung mit wenig (Tipp-)Aufwand und guter
Modellierung.
Die Portierung steht für mich zwar überhaupt nicht im Vordergrund,
aber dennoch hätte ich mir eine Lösung gewünscht, welche sich
an "gängiger" Funktionalität und Sprachumfang anlehnt. Aber wie
gesagt: Ich halte dies für fast nicht erfüllbar, man muss Kompromisse
eingehen und Dein Vorschlag halte ich für ein sehr guter Kompromiss.
Viele Grüsse
Philipp
Hallo Philipp,
[... Probleme mit der Lösung ...]
Ich würde zwar einfach eine generelle sub fatal() schreiben, die
abhängig von der Exception-Klasse ihr Verhalten ändert. Wenn dir
das nicht zusagt, kannst du ja stattdessen
http://search.cpan.org/~mouns/PException-2.4/PException.pm
benutzen, vielleicht kommt das deinen Bedürfnissen ja eher entgegen.
Grüße,
CK
Halihallo Christian
http://search.cpan.org/~mouns/PException-2.4/PException.pm
benutzen, vielleicht kommt das deinen Bedürfnissen ja eher entgegen.
Auch ein guter Vorschlag, Danke!
Ich habe mich für folgende Lösung entschieden:
dem 'die' wird eine neue Error-Instanz übergeben. Diese Error
Instanz erbt von einer Basisklasse Exception, welche im stringified-
Kontext die Fehlerbeschreibung ausgibt. Falls also 'die' nicht über
eval oder __DIE__ Signal abgefangen wird, wird einfach der Fehler
ausgegeben. Falls er abgefangen wird, kann man über ein Exception-
Handling auf die Exception-Instanz zugreifen (welche z.B. in
Anlehung an Java auch ein StackTrace implementiert).
So habe ich
a) einen ganz normalen 'die', falls nicht abgefangen.
Über '""' overloading erreiche ich eine ganz normale, stringified
Fehlermeldung, die von CORE::die ausgegeben wird, falls abgefangen
kann auf die Instanz zugegriffen werden.
b) eine einfache Möglichkeit Fehler über __DIE__ Signal auszuwerten
und darauf zu reagieren
c) kein Laden von PException oder was auch immer in jeder Klasse.
Folgendes kleines Beispiel zur demonstation meiner momentanen Lösung:
------------------------------------
#!/usr/bin/perl
package Exception;
use overload '""' => &to_string;
use strict;
use warnings;
sub new {
my ($class,$code,$stringified) = @_;
bless {code=>$code,str=>$stringified}, $class;
}
sub to_string($) {
my ($self) = @_;
return "$self->{code} - $self->{str}\n";
}
package main;
use strict;
use warnings;
use base qw(Exception);
die new Exception('ERR_DATABASE_CONNECTION_REFUSED', 'Verbindungsaufbau zur Datenbank fehlgeschlagen!');
local $SIG{__DIE__} = sub {
my ($exception) = @_;
# do something with object $exception
print $exception->{code};
exit(255);
};
die new Exception('ERR_INVALID_CHARACTER', 'Ungültiges Zeichen!');
----------------------------------
Viele Grüsse
Philipp