Exception::Class
El módulo Exception::Class permite implementar un control de errores basado en excepciones dentro de un programa ó biblioteca.
Los principios básicos son:
- Se declara una jerarquía de clases que permiten discernir qué clase de
error fatal ocurrió, ó a qué grupo pertenece (mediante el operador isa).
Esto presenta dos ventajas:
- No es necesario importar funciones ni variables en cada uno de los módulos, puesto que las clases creadas lo son globalmente para la aplicación.
- Al tener orientación a objetos se pueden derivar en clases nuevas, añadiendo ó quitando todo tipo de información, así como cambiando drásticamente la gestión de excepciones; tal vez unas graban algo en algún registro mientras que otras efectúan limpiezas en el sistema, las posibilidades son muchas.
- Se envuelve mediante eval todas las llamadas a código que es posible que provoque una excepción ó que muera ante una condición dada.
- Se comprueba inmediatamente después si ha habido una excepción, de qué tipo es y si podemos tratarla nosotros ó debemos lanzarla hacia atrás.
1 use Exception::Class ( 2 'MyException', 3 'MyException::System' => { 4 isa => 'MyException', 5 description => 'Operating system related failure', 6 }, 7 'MyException::User' => { 8 isa => 'MyException', 9 description => 'User interaction realted failure', 10 }, 11 'MyException::User::Keyboard' => { 12 isa => 'MyException::User', 13 description => 'Bad input from user', 14 }, 15 'MyException::User::Gestures' => { 16 isa => 'MyException::User', 17 description => 'User did an offensive gesture ', 18 }, 19 ); 20 21 TRY: 22 my $result = eval { my_code() }; 23 24 my $ex; 25 if ($ex = Exception::Class->caught('MyException')) { 26 if (is_recoverable()) { 27 next TRY; 28 } 29 else { 30 $ex->rethrow(); 31 } 32 }
Y por si el método de capturar excepciones parece complicado,
Exception::Class
proporciona algo de azucar sintáctico para ello:
1 TRY: 2 3 eval { 4 dangerous_code(); 5 }; 6 7 # Si el problema es por parte del usuario ... 8 if (my $ex = MyException::User>caught()) { 9 # si es un gesto ofensivo 10 if ($ex->caught('MyException::User::Gestures'))) { 11 # damos por terminado el proceso 12 croak "unacceptable user input"; 13 } 14 else { 15 # una nueva oportunidad ... 16 next TRY; 17 } 18 } 19 else { 20 # es un error de sistema (ó cualquier otra cosa) que no podemos manejar 21 $ex->rethrow(); 22 }
Exception::Class
dispone de un método heredable llamado caught()
que
acepta un nombre de clase y devuelve un objeto if la última excepción es de
dicha clase ó una subclase de ella. Si no se le pasa ningún parámetro retorna
el valor de $@
.
Personalizando mensajes
Una de las ventajas de utilizar este tipo de gestión de errores está en la posibilidad de personalizar muchos de sus aspectos. En concreto el mensaje creado tras una excepción no capturada puede modificarse totalmente si definimos algunos métodos en el paquete que declara las clases:
1 package MyExceptions; 2 3 use Exception::Class ( 4 'MyExceptions' => { 5 description => 'Parent class', 6 }, 7 'MyExceptions::FileSystem' => { 8 isa => 'MyExceptions', 9 description => 'Fatal error in file system access', 10 fields => [ qw( errno file ) ], 11 }, 12 'MyExceptions::Network' => { 13 isa => 'MyExceptions', 14 description => 'Fatal error in network access', 15 fields => [ qw( host port errno ) ], 16 }, 17 ); 18 19 sub full_message { 20 my $self = shift; 21 my @ret = (); 22 foreach my $m ( $self->__first_line(), 23 $self->__message(), 24 $self->__fields() ) { 25 push(@ret, $m) if $m; 26 } 27 28 return join("", @ret); 29 } 30 31 sub __first_line { 32 my $self = shift; 33 my $program_name = $0 || $self->file(); 34 my $local_time = localtime($self->time()); 35 my $package = $self->package(); 36 my $file = $self->file(); 37 my $line = $self->line(); 38 my $pid = $self->pid(); 39 my $uid = sprintf("uid=%u,gid=%u,euid=%u,egid=%u", 40 $self->uid(), $self->gid(), $self->euid(), 41 $self->egid()); 42 $package = sprintf("package %s,", $package) if $package; 43 44 return <<EOF; 45 ${program_name}(${pid}): error fatal in ${package} 46 file ${file}, line ${line} at ${local_time} 47 with ${uid} 48 EOF 49 } 50 51 sub __message { 52 my $self = shift; 53 54 my $ret = $self->message() || ref($self)->description() 55 || 'unknown error'; 56 57 return sprintf("\n%s\n\n", $ret); 58 } 59 60 sub __fields { 61 my $self = shift; 62 my @ret = ( ); 63 64 foreach my $f (ref($self)->Fields()) { 65 push(@ret, sprintf(" %s = %s\n", $f, $self->{$f})); 66 } 67 68 return @ret ? (" additional info:\n", @ret) : (); 69 }
En realidad el método a sobrecargar es
full_message
mientras que el resto son añadidos para hacer la salida más informativa.
Lo anterior es quizás un tanto excesivo para incluir repetidamente en cada nuevo proyecto; en cualquier caso daría como resultado un mensaje similar a éste:
test.pl(3456): error fatal in package main, file test.pl,
line 3 at lun abr 3 20:40:24 CEST 2006
with uid=1000,gid=1000,euid=1000,egid=1000
could not open password file
additional info:
errno = 1 operation not allowed
He dividido las funciones que extraen información porque me da más juego para obtenerla de varias fuentes, pero creo que es un ejemplo de lo que podría llegar a hacerse.
Nota: conviene advertir que para sobrecargar los métodos la primera clase
que se declara en la línea use Exception::Class
no debe tener un atributo
isa
específico. Exception::Class
se encargará de convertirla en la clase
madre de todas las demás, y la herencia de Perl del resto para que la
sobrecarga funcione.