如果Moose构建器方法失败,我该怎么办?

时间:2010-01-29 19:32:03

标签: perl moose

在构建器方法中处理故障的最佳方法是什么?

例如:

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;
}

如果_build_file_handle无法获得句柄,则构建器将返回undef,这将失败类型约束。

我可以在file_handle类型约束中使用联合,以便它接受undef作为有效值。但是,即使值has_file_handle,谓词undef也会返回true。

有没有办法表明构建器失败了,属性应该保持清除?

3 个答案:

答案 0 :(得分:9)

你没有想到足够高的水平。好的,构建器失败了。该属性仍未定义。但是你如何处理调用访问器的代码呢?类合同表明调用该方法总是会返回一个IO :: File。但现在它正在返回undef。 (合同是IO::File,而不是Maybe[IO::File],对吗?)

所以在下一行代码中,调用者将会死亡(“无法在the_caller.pl第42行的未定义值上调用方法'readline'),因为它希望你的类遵循合同它定义的。失败不是你的班级应该做的事情,但现在确实如此。调用者如何做任何事来纠正这个问题?

如果它可以处理undef,那么调用者实际上并不需要文件句柄......所以为什么它要求你的对象呢?

考虑到这一点,唯一合理的解决方案就是死亡。您无法满足您同意的合同,而die是您摆脱这种情况的唯一方式。所以就这样做;死亡是生活中的事实。

现在,如果您在构建器运行时不准备死亡,则需要在可能失败的代码运行时进行更改。您可以在对象构造时执行此操作,方法是将其设置为非延迟,或者通过在BUILD(BUILD { $self->file_name })中显式激活该属性。

更好的选择是根本不将文件句柄暴露给外部世界,而是执行以下操作:

# 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;
}

现在你知道程序什么时候会死;在newwrite_log中。你知道,因为文档说的是这样。

第二种方式使你的代码更清洁;消费者不需要知道你的类的实现,它只需要知道它可以告诉它写一些日志消息。现在调用者不关心您的实现细节;它只是告诉班级它真正想要它做什么。

并且,在write_log中死亡甚至可能是你可以从(在一个捕获区块中)恢复的东西,而“无法打开这个你不应该知道的随机不透明的东西”对于呼叫者从中恢复。

基本上,设计你的代码是合理的,异常是唯一的答案。

(无论如何,我不会得到整体“他们是一个kludge”。他们在C ++中的工作方式完全相同,在Java和Haskell以及其他所有语言中的工作方式非常相似。die这个词是否真的那么可怕或什么?)

答案 1 :(得分:6)

“最佳”是主观的,但你必须决定哪些更有意义:

  1. 如果您在文件句柄无法构建时可以继续使用代码(即它是可恢复的条件),则构建器应返回undef并将类型约束设置为'Maybe[IO::File]'。这意味着您还必须在使用它时检查该属性的定义。您还可以检查此属性是否在BUILD中正确构建,并选择在此时采取进一步操作(正如他在评论中提到的那样),例如如果它是undef则调用clear_file_handle(因为构建器将始终为属性赋值,假设它当然不会死)。

  2. 否则,让构建器失败,或者通过显式抛出异常(您可以选择向上捕获),或者只是返回undef并让类型约束失败。无论哪种方式你的代码都会死;你可以选择它是如何死的以及堆栈轨迹是多么庞大。 :)

  3. PS。您可能还想查看Moose在内部使用的Try::Tiny,并且基本上只是 * do eval { blah } or die ...成语的包装。

    * 但做得对!并以一种很酷的方式! (我似乎听到很多来自#moose的耳语......)

答案 2 :(得分:2)

  

有没有办法表明构建器失败了,属性应该保持清除?

没有。这没有意义,如果属性被清除,构建器将触发,如果它在构建器中被清除,它将在您进行下一次调用时触发,并保持在清除状态。浪费了很多工作,只是为了设置一些东西,如果它工作,如果不继续

type-union建议是一个很好的建议,但是你必须编写可以在两种截然不同的情况下运行的代码:文件句柄和不存在的文件句柄。这似乎是一个糟糕的主意。

如果文件句柄对任务不重要,那么它可能不会在访问该对象的相同范围内共享。如果是这种情况,则对象可以只提供从对象生成文件句柄的方法。我在生产代码中这样做。不要把所有东西都变成懒惰属性,有些东西是属性的函数,并且将它们附加到对象上并不总是有意义的。

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;                                                                      

}                                                                           

一种完全不同的方法是将IO::File子类化,包含有关要保留的文件的元数据。有时这是有效的,也是一个很好的解决方案:

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;

};