10

处理构建器方法失败的最佳方法是什么?

例如:

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将返回 true,即使值为undef.

有没有办法发出构建器失败的信号,并且属性应该保持清除?

4

3 回答 3

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

现在你知道程序什么时候死了;在new, 或 在write_log. 你知道,因为文档是这样说的。

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

而且,死亡write_log甚至可能是你可以从中恢复的东西(在一个 catch 块中),而“无法打开这个你无论如何都不应该知道的随机不透明的东西”对于调用者来说更难恢复。

基本上,理智地设计你的代码,例外是唯一的答案。

(无论如何,我并没有完全理解“它们是杂种”。它们在 C++ 中的工作方式完全相同,在 Java、Haskell 和其他所有语言中也非常相似。这个词die真的那么可怕吗?)

于 2010-01-30T05:56:22.857 回答
6

“最佳”是主观的,但您必须决定在您的代码中哪个更有意义:

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

  2. 否则,让构建器失败,或者通过显式抛出异常(您可以选择追赶更高级别),或者简单地返回 undef 并让类型约束失败。无论哪种方式,您的代码都会死掉;您只需选择它是如何死亡的以及堆栈跟踪的数量有多大。:)

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

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

于 2010-01-29T20:14:55.013 回答
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;

};
于 2010-01-29T20:54:46.257 回答