Philipp Hasenfratz: OOP Error-/ExceptionHandling

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


  1. A-Za-z ↩︎

  1. 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

    --
    Q: God, root, what's the difference?
    A: God is merciful.
    1. 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

      1. 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

      2. 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

        --
        Wer sich zu überschwänglich freut, wir später Grund zum Weinen haben.
        1. 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!');

          or

          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