在构建器方法中处理故障的最佳方法是什么?
例如:
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。
有没有办法表明构建器失败了,属性应该保持清除?
答案 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;
}
现在你知道程序什么时候会死;在new
或write_log
中。你知道,因为文档说的是这样。
第二种方式使你的代码更清洁;消费者不需要知道你的类的实现,它只需要知道它可以告诉它写一些日志消息。现在调用者不关心您的实现细节;它只是告诉班级它真正想要它做什么。
并且,在write_log
中死亡甚至可能是你可以从(在一个捕获区块中)恢复的东西,而“无法打开这个你不应该知道的随机不透明的东西”对于呼叫者从中恢复。
基本上,设计你的代码是合理的,异常是唯一的答案。
(无论如何,我不会得到整体“他们是一个kludge”。他们在C ++中的工作方式完全相同,在Java和Haskell以及其他所有语言中的工作方式非常相似。die
这个词是否真的那么可怕或什么?)
答案 1 :(得分:6)
“最佳”是主观的,但你必须决定哪些更有意义:
如果您在文件句柄无法构建时可以继续使用代码(即它是可恢复的条件),则构建器应返回undef并将类型约束设置为'Maybe[IO::File]'
。这意味着您还必须在使用它时检查该属性的定义。您还可以检查此属性是否在BUILD
中正确构建,并选择在此时采取进一步操作(正如他在评论中提到的那样),例如如果它是undef则调用clear_file_handle(因为构建器将始终为属性赋值,假设它当然不会死)。
否则,让构建器失败,或者通过显式抛出异常(您可以选择向上捕获),或者只是返回undef并让类型约束失败。无论哪种方式你的代码都会死;你可以选择它是如何死的以及堆栈轨迹是多么庞大。 :)
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;
};