在构建器方法中处理故障的最佳方法是什么?
例如:
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;
}
Run Code Online (Sandbox Code Playgroud)
如果_build_file_handle无法获得句柄,则构建器将返回undef,这会使类型约束失败.
我可以在file_handle类型约束中使用union ,以便它接受undef一个有效值.但是has_file_handle,即使值很大,谓词也会返回true undef.
有没有办法表明构建器失败了,属性应该保持清除?
你没有想到足够高的水平.好的,构建器失败了.该属性仍未定义.但是你如何处理调用访问器的代码呢?类合同表明调用该方法总是会返回一个IO :: File.但现在它正在返回undef.(合同是IO::File,不是Maybe[IO::File]吗?)
所以在下一行代码中,调用者将会死("不能在_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;
}
Run Code Online (Sandbox Code Playgroud)
现在你知道程序什么时候会死; in new或in write_log.你知道,因为文档说的是这样.
第二种方式使您的代码更清洁; 消费者不需要知道你的类的实现,它只需要知道它可以告诉它写一些日志消息.现在调用者不关心您的实现细节; 它只是告诉班级它真正想要它做什么.
而且,死亡write_log甚至可能是你可以恢复的东西(在一个捕获区块中),而"无法打开这个随机不透明的东西你不应该知道"对于呼叫者来说更难以恢复.
基本上,设计你的代码是理智的,异常是唯一的答案.
(无论如何,我没有得到整体"他们是一个kludge".他们在C++中的工作方式完全相同,在Java和Haskell以及其他所有语言中也是如此.这个词die真的那么可怕吗?)
"最佳"是主观的,但你必须决定哪些在你的代码中更有意义:
如果在文件句柄无法构建时可以继续使用代码(即它是可恢复的条件),则构建器应返回undef并将类型约束设置为'Maybe[IO::File]'.这意味着您还必须在使用它时检查该属性的定义.您还可以检查此属性是否已正确构建BUILD,并选择在该点采取进一步操作(正如他在评论中提到的那样),例如,如果它是undef则调用clear_file_handle(因为构建器将始终为属性赋值,假设它当然不会死).
否则,让构建器失败,或者通过显式抛出异常(您可以选择向上捕获),或者只是返回undef并让类型约束失败.无论哪种方式你的代码都会死; 你可以选择它是如何死的以及堆栈轨迹是多么庞大.:)
PS.您可能还想看看Moose在内部使用的Try :: Tiny,它基本上只是*do eval { blah } or die ...成语的包装器.
*但做得对!并以一种很酷的方式!(我似乎从#moose听到很多耳语!)