Perl/Exception::Class

De Astillas.net
Módulo Exception::Class
Versión 1.31
Uso Todo tipo de aplicaciones
Propósito Permite crear una jerarquía de excepciones que pueden ser lanzadas y tratadas en cualquier parte de un programa.


Ejemplos de uso

Utilizando las excepciones en el programa

#/usr/bin/perl

use strict;
use MyExceptions;

my $config_fh = undef;

# Abrimos el archivo de configuración
eval {
    $config_fh = open_file( 'file.conf' );
};

# Si ha habido un error 
if (my $ex = MyExceptions->caught()) {
    # si el archivo no existe 
    if ($ex->isa('MyExceptions::FileNotFound')) {
        # utilizamos la configuración predeterminada
        use_default_config();
    }
    # si no tenemos acceso de lectura 
    elsif ($ex->isa('MyExceptions::FileNotRead')) {
        # utilizamos también la configuración predeterminada pero avisamos de
        # dicha circunstancia
        warn "could not read file.conf";
        use_default_config();
    }
    # no podemos hacer más si es otro tipo de excepción que lanzarla hacia
    # atrás
    else {
        $ex->rethrow();
    }
}
else {
    # leemos el archivo ...
    read_config_file( $config_fh );
}

sub open_file {
    my $file    = shift;
    my $fh      = undef;

    # Si el archivo no existe 
    if (not -e $file) {
        MyExceptions::FileNotFound->throw( filename => $file );
    }
    # no es lo mismo que si no podemos leerlo 
    elsif (not -r $file) {
        MyExceptions::FileNotRead->throw( filename => $file );
    }
    # ni tampoco que no podamos abrirlo (las razones puede diferir)
    if (not $fh = open $file, "r") {
        MyExceptions::FileOpen->throw( filename => $file, errno => $! );
    }

    return $fh;
}

Definiendo excepciones

package MyExceptions;

use strict;

use Exception::Class (
    'MyException',
    'MyException::File' =>  {
        isa         =>  q(MyException),
        description =>  q(Error on file operation),
        fields      =>  qw( filename ),
    },
    'MyException::FileNotFound' => {
        isa         =>  q(MyException::File),
        description =>  q(File not found),
    },
    'MyException::FileNotRead'  =>  {
        isa         =>  q(MyException::File),
        description =>  q(File not readable),
    },
    'MyException::FileOpen' =>  {
        isa         =>  q(MyException::File),
        description =>  q(Fatal error opening file),
        fields      =>  qw( errno ),
    }
);

1;

Las tres últimas clases, las que emplea el ejemplo de arriba, derivan de la clase MyException::File que incluye como campo el nombre del archivo filename por lo que no es necesario definirlo en ellas (aunque tampoco se puede prescindir de él ya que es heredado guste o no). La última clase, además, define un campo más destinado a guardar el código de error producido en la operación de apertura; este código procede del sistema operativo por lo que puede ser importante para determinar por qué ha fallado la operación.

Conceptos

El módulo Exception::Class permite construir y utilizar un mecanismo de gestión de errores dentro de un programa o una biblioteca de código. Para ello hace uso de excepciones en forma de terminaciones en la ejecución del programa que pueden ser capturadas y analizadas; esto evita, entre otras cosas, tener que construir cascadas de retornos en funciones para asegurarnos de que la más mínima condición de error es comunicada hacia atrás hasta la primera de las funciones en la pila.

Definiendo excepciones

Antes de usar el mecanismo de excepciones debemos construirlo. Para ello crearemos una jerarquía de clases indicando cuáles heredan de quienes y qué campos adicionales de información añaden via herencia. También se les puede proporcionar una descripción humana para que los mensajes de error sean más claros.

El hecho de definir las excepciones como clases tiene como ventajas:

  • Las clases definidas lo son a nivel global para la aplicación, con lo que basta con cargar la definición en una parte de la misma para que en todas partes puedan ser utilizadas.
  • Es fácil derivar clases nuevas que añadan o modifiquen información, y que realicen otras tareas más específicas con su naturaleza como limpieza o registro de eventos.

y como desventajas

  • Las clases definidas son a nivel global.
  • La definición puede llegar a ser engorrosa.
  • Requiere planificación previa de la jerarquía para evitar retoques en el código subyacente.

El siguiente código debería ir en su propio archivo MyException.pm para incluirse con facilidad en otros puntos del programa.

package MyException;
use strict;

use Exception::Class (
    'MyException',
    'MyException::System' => {
        isa         =>  'MyException',
        description =>  'Operating system related failure',
        },
    'MyException::User' => {
        isa         =>  'MyException',
        description =>  'User interaction realted failure',
        },
    'MyException::User::Keyboard' => {
        isa         =>  'MyException::User',
        description =>  'Bad input from user',
        },
    'MyException::User::Gestures' => {
        isa         =>  'MyException::User',
        description =>  'User did an offensive gesture ',
        },
    );

1;

Empleando excepciones

Utilizar el mecanismo de excepciones requiere principalmente un cambio de mentalidad. En un componente software nuevo tendremos en cuenta que:

  • Nuestro componente debe efectuar su trabajo hasta que encuentre una situación que no pueda manejar y que le impida continuar. En ese momento debe lanzar una excepción tal que explique a su antecesor qué ha ocurrido.
  • Al utilizar a otros componentes que puedan fallar de la misma forma que nosotros debemos envolver su ejecución mediante el operador eval y verificar que exista o no un error fatal.
  • Si ha ocurrido un error fatal en otros componentes debemos analizarlo y si podemos hacer algo para paliar sus efectos hacerlo. En caso contrario, sin tocar nada más, tendremos que relanzar el error hacia atrás.
#!/usr/bin/perl

use strict;
use MyException;

TRY:
my $result = eval { my_code() };

my $ex;
if ($ex = Exception::Class->caught('MyException')) {
    if (is_recoverable()) {
        next TRY;
    }
    else {
        $ex->rethrow();
    }
}

Y como la forma de capturar excepciones puede ser complicada el módulo proporciona alguna facilidad más en forma de azúcar sintáctico:

TRY:

eval {
    dangerous_code();
    };

# Si el problema es por parte del usuario ...
if (my $ex = MyException::User->caught()) {
    # si es un gesto ofensivo 
    if ($ex->caught('MyException::User::Gestures'))) {
        # damos por terminado el proceso 
        croak "unacceptable user input";
    }
    else {
        # una nueva oportunidad ...
        next TRY;
    }
}
else {
    # es un error de sistema (ó cualquier otra cosa) que no podemos manejar
    $ex->rethrow();
}

Exception::Class dispone de un método heredable llamado caught() que acepta un nombre de clase y devuelve un objeto si la última excepción es de dicha clase o una subclase de ella. Si no se le pasa ningún parámetro retorna el valor de $@.

Recetario

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:

package MyExceptions;

use Exception::Class (
    'MyExceptions' => {
        description => 'Parent class',
        },
    'MyExceptions::FileSystem' => {
        isa         => 'MyExceptions',
        description => 'Fatal error in file system access',
        fields      => [ qw( errno file ) ],
        },
    'MyExceptions::Network' => {
        isa         =>  'MyExceptions',
        description =>  'Fatal error in network access',
        fields      =>  [ qw( host port errno ) ],
        },
);

sub full_message {
    my  $self   =   shift;
    my @ret = ();
    foreach my $m ( $self->__first_line(),
                    $self->__message(),
                    $self->__fields() ) {
        push(@ret, $m) if $m;
    }

    return join("", @ret);
}

sub __first_line {
    my $self = shift;
    my $program_name = $0 || $self->file();
    my $local_time = localtime($self->time());
    my $package = $self->package();
    my $file = $self->file();
    my $line = $self->line();
    my $pid = $self->pid();
    my $uid = sprintf("uid=%u,gid=%u,euid=%u,egid=%u",
                $self->uid(), $self->gid(), $self->euid(),
                $self->egid());
    $package = sprintf("package %s,", $package) if $package;

    return <<EOF;
${program_name}(${pid}): error fatal in ${package}
  file ${file}, line ${line} at ${local_time}
  with ${uid}
EOF
}

sub __message {
    my $self = shift;

    my $ret = $self->message() || ref($self)->description()
            || 'unknown error';

    return sprintf("\n%s\n\n", $ret);
}

sub __fields {
    my $self = shift;
    my @ret = ( );

    foreach my $f (ref($self)->Fields()) {
        push(@ret, sprintf("    %s = %s\n", $f, $self->{$f}));
    }

    return @ret ? ("  additional info:\n", @ret) : ();
}

En realidad el método que debemos sobrecargar es full_message; el resto son añadidos para hacer la salida más informativa.

Aunque lo anterior puede ser un tanto excesivo para incluir repetidamente en cada nuevo proyecto en cualquier caso daría como resultado un mensaje similar a éste:

El mensaje que produciría una excepción de este tipo es similar al siguiente:

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

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.