2010-01-29 6 views
10

¿Cuál es la mejor manera de manejar una falla en un método de construcción?¿Qué debo hacer si falla un método de compilación de Moose?

Por ejemplo:

package MyObj; 
use Moose; 
use IO::File; 

has => 'file_name' (is => 'ro', isa => 'Str',  required =>1 ); 
has => 'file_handle' (is => 'ro', isa => 'IO::File', lazy_build => 1); 

sub _build_file_handle { 
    my $self = shift; 
    my $fh = IO::File->new($self->file_name, '<'); 

    return $fh; 
} 

Si el _build_file_handle falla para conseguir una manija, el constructor volverá undef, que no la restricción de tipo.

Podría usar una unión en la restricción de tipo file_handle, de modo que acepte un undef como valor válido. Pero entonces, el predicado has_file_handle devolverá verdadero, incluso cuando el valor sea undef.

¿Hay alguna manera de indicar que el constructor ha fallado y que el atributo debe permanecer despejado?

+4

¿Por qué no lanzar una excepción? – friedo

+0

@friedo, explique cómo manejaría las excepciones en este caso. Mi preocupación es que si lanzo una excepción fatal en el constructor, eso significa que cada vez que accedo 'file_handle', tendría que estar listo para atrapar una excepción. – daotoad

+1

Si no tiene una excepción, tendrá que verificar lo que devuelve y hacer algo diferente si falla; es probable que la comprobación de una excepción sea un código menor. – Mark

Respuesta

6

"mejor" es subjetiva, pero usted tendrá que decidir qué tiene más sentido en el código:

  1. si puede continuar en el código cuando el gestor de archivo no se genera (es decir, se una condición recuperable), el constructor debe devolver undef y establecer la restricción de tipo en 'Maybe[IO::File]'. Eso significa que también deberá verificar la definición de ese atributo siempre que lo use. También podría verificar si este atributo se construyó correctamente en BUILD, y optar por tomar medidas adicionales en ese punto (como se hizo alusión a friedo en su comentario), p. llamando a clear_file_handle si es undef (ya que un constructor siempre asignará un valor al atributo, asumiendo que no muera, por supuesto).

  2. De lo contrario, deje que el generador falle, ya sea lanzando una excepción explícitamente (que puede optar por alcanzar más arriba), o simplemente devolviendo undef y dejando que la restricción de tipo falle. De cualquier forma, tu código morirá; solo tienes que elegir cómo se muere y cuán voluminoso es el rastro de la pila. :)

PS. También puede consultar Try::Tiny, que Moose usa internamente, y es básicamente un contenedor para * la expresión do eval { blah } or die ....

* Pero hecho bien! y de una manera genial! (Me parece escuchar un montón de susurrándome al oído de #moose ..)

+0

He visto Try :: Tiny. He dudado en saltar sobre cualquier módulo de manejo de excepciones, ya que me han quemado gravemente al usar constructores de clases y manejadores de excepciones "maravillosos nuevos". Me tomó mucho tiempo darle una oportunidad a Moose. – daotoad

+2

No entiendo; Try :: Tiny es realmente trivial y no hace casi nada más allá de lo necesario para evitar el comportamiento que es casi un error en Perl. Me cuesta trabajo comprometerme con algo tan amplio como Moose, pero puedes leer y comprender la fuente de Try :: Tiny en unos 5 minutos. ¿Qué hay para quemarte? – hdp

+1

Ether, gracias. Terminé tomándome el tiempo para comprobar Try :: Tiny, y pasó una prueba bastante completa de olfateo. Cuando el código está estructurado correctamente, no es demasiado difícil minimizar el número de bloques 'try {}'. Hasta el momento, estoy bastante contento con Try :: Tiny. – daotoad

2

¿Hay una manera de señalar que el constructor ha fallado, y el atributo debe permanecer despejado?

No. Esto no tendría sentido, el constructor se disparará si se cleaered el atributo, si se borra dentro del constructor sería simplemente disparar cuando hizo la siguiente llamada a la misma, y ​​permanecer en el aclarado estado. Un desperdicio de mucho trabajo, solo para set-something-if-it-works-and-if-not-continue.

La sugerencia type-union es buena, pero luego tiene que escribir un código que pueda funcionar con dos casos radicalmente diferentes: un identificador de archivo y un manejador de archivo inexistente. Esto parece una mala idea.

Si el manejador de archivo no es esencial para la tarea, probablemente no se comparte entre el mismo ámbito de cosas con acceso al objeto. Si este es el caso, entonces el objeto solo puede proporcionar un método que genere un identificador de archivo del objeto.Lo hago en código de producción. No se empeñe en hacer de todo un atributo perezoso, algunas cosas son funciones de atributo y no siempre tiene sentido adjuntarlas al objeto.

sub get_fh {                 
    my $self = shift;               

    my $abs_loc = $self->abs_loc;            

    if (!(-e $abs_loc) || -e -z $abs_loc) {         
    $self->error({ msg => "Critical doesn't exist or is totally empty" }); 
    die "Will not run this, see above error\n";        
    }                   

    my $st = File::stat::stat($abs_loc);          
    $self->report_datetime(DateTime->from_epoch(epoch => $st->mtime));  

    my $fh = IO::File->new($abs_loc, 'r')         
    || die "Can not open $abs_loc : $!\n"         
    ;                   

    $fh;                  

}                   

Un enfoque totalmente diferente es subclase IO::File, con los metadatos sobre el archivo que desea conservar. Algunas veces esto es efectivo, y una gran solución:

package DM::IO::File::InsideOut; 
use feature ':5.10'; 
use strict; 
use warnings; 

use base 'IO::File'; 

my %data; 

sub previouslyCreated { 
    $data{+shift}->{existed_when_opened} 
} 

sub originalLoc { 
    $data{+shift}->{original_location} 
} 

sub new { 
    my ($class, @args) = @_; 

    my $exists = -e $args[0] ? 1 : 0; 

    my $self = $class->SUPER::new(@args); 

    $data{$self} = { 
    existed_when_opened => $exists 
    , original_location => $args[0] 
    }; 

    $self; 

}; 
9

No está pensando en un nivel lo suficientemente alto. OK, el constructor falla. El atributo permanece indefinido. Pero, ¿qué haces con el código que llama al usuario? El contrato de la clase indicaba que llamar al método siempre devolvería un IO :: File. Pero ahora está volviendo undef. (El contrato fue IO::File, no Maybe[IO::File], ¿verdad?)

Así que en la siguiente línea de código, la persona que llama se va a morir ("No se puede llamar al método 'readline' en un valor indefinido en the_caller.pl línea 42 . "), porque espera que su clase siga el contrato que definió. El fracaso no era algo que tu clase debía hacer, pero ahora lo hizo. ¿Cómo puede la persona que llama hacer algo para corregir este problema?

Si puede manejar undef, la persona que llama no necesita un identificador de archivo para empezar ... entonces, ¿por qué le pidió uno a su objeto?

Con esto en mente, la única solución sensata es morir. No puede cumplir el contrato que acordó, y die es la única forma en que puede salir de esa situación. Así que haz eso; la muerte es un hecho de la vida.

Ahora, si no está preparado para morir cuando se ejecuta el constructor, deberá cambiar cuando se ejecute el código que puede fallar. Puede hacerlo en tiempo de construcción del objeto, ya sea haciéndolo no perezoso, o reviviendo explícitamente el atributo en BUILD (BUILD { $self->file_name }).

Una mejor opción es no exponer el identificador de archivo con el mundo exterior en absoluto, y en lugar de hacer algo como:

# dies when it can't write to the log file 
method write_log { 
    use autodie ':file'; # you want "say" to die when the disk runs out of space, right? 
    my $fh = $self->file_handle; 
    say {$fh} $_ for $self->log_messages; 
} 

Ahora se sabe cuando el programa se va a morir; en new, o en write_log. Ya sabes, porque los documentos lo dicen.

La segunda forma hace que su código sea mucho más limpio; el consumidor no necesita saber sobre la implementación de su clase, solo necesita saber que puede indicarle que escriba algunos mensajes de registro. Ahora la persona que llama no está preocupada con los detalles de su implementación; simplemente le dice a la clase lo que realmente quería que hiciera.

Y, morir en write_log incluso puede ser algo de lo que pueda recuperarse (en un bloque catch), mientras que "no pudo abrir esta opacidad aleatoria que de todos modos no debería saber" es mucho más difícil recuperar de.

Básicamente, diseñe su código de manera sensata, y las excepciones son la única respuesta.

(No entiendo el todo "son un poco pesado" de todos modos. Funcionan exactamente de la misma manera en C++ y de manera similar en Java y Haskell y en todos los demás idiomas. ¿Es la palabra die realmente tan espeluznante o algo así?)

+0

Usted y otros discuten persuasivamente sobre excepciones. Esperaba evitar usarlos debido al ** hecho ** de que están rotos. Lanzar excepciones es fácil y funciona bien. Atraparlos es horrible. El bloque 'eval' está roto. Ya he escrito suficientes repeticiones evangélicas para escribir en la Capilla Sixtina. – daotoad

+3

Utilice Try :: Tiny, que se ha sugerido varias veces en varios subprocesos. Perl no va a cambiar porque tienes miedo de las bibliotecas en Stack Overflow. Entonces, o usa una biblioteca, escribe el texto repetitivo, arregla el perl por sí mismo o vete. – jrockway

+0

Después de haberme quemado probando varias técnicas al revés (recuerda esa moda) y Error.pm y otros módulos try/catch, me volví loco y no le di a Try :: Tiny la oportunidad que merecía. Sus argumentos en favor de las excepciones y de otros aquí me llevaron a la conclusión de que tenían el diseño correcto. Por eso te agradezco. – daotoad

Cuestiones relacionadas