DVK*_*DVK 10 error-handling perl exception-handling perl-module
问题:
什么被认为是"最佳实践" - 以及为什么 - 在构造函数中处理错误?
"最佳实践"可以引自施瓦茨,或50%的CPAN模块使用它等等; 但我对任何人都有充分理由的意见感到高兴,即使它解释了为什么常见的最佳实践并不是真正的最佳方法.
至于我自己对该主题的看法(通过Perl中的软件开发多年来了解),我在perl模块中看到了三种主要的错误处理方法(在我看来从最好到最差列出):
构造一个对象,设置一个无效的标志(通常是" is_valid"方法).通常通过类的错误处理与设置错误消息相结合.
优点:
允许标准(与其他方法调用相比)错误处理,因为它允许$obj->errors()在错误的构造函数之后使用类型调用,就像在任何其他方法调用之后一样.
允许传递其他信息(例如> 1错误,警告等...)
允许轻量级的"重做"/"fixme"功能,换句话说,如果构造的对象非常繁重,许多复杂的属性100%总是正常,并且它无效的唯一原因是因为某人输入了不正确的日期,你可以简单地做" $obj->setDate()"而不是再次重新执行整个构造函数的开销.这种模式并不总是需要,但在正确的设计中非常有用.
缺点:没有我知道的.
返回" undef".
缺点:无法实现第一个解决方案的任何优点(全局变量之外的每个对象错误消息和重型对象的轻量级"fixme"功能).
死在构造函数内部.在一些非常狭窄的边缘情况之外,我个人认为这是一个可怕的选择,有太多理由列出这个问题的边缘.
更新:为了清楚,我认为(非常有价值和一个伟大的设计)解决方案有一个非常简单的构造函数,它根本不会失败,而且是一个繁重的初始化方法,其中所有错误检查都只是其中任何一个的子集出于此问题的目的,情况#1(如果初始化程序设置错误标志)或情况#3(如果初始化程序死亡).显然,选择这样的设计,你会自动拒绝选项#2.
这取决于您希望构造函数的行为方式.
其余的回应都反映在我的个人观察中,但与Perl的大多数事情一样,最佳实践真的归结为"这是一种方法,你可以根据自己的需要采取或离开." 您描述的偏好完全有效且一致,没有人应该告诉您.
如果构造失败,我实际上更愿意死,因为我们设置它以便在对象构造期间可能发生的唯一类型的错误确实是大的,明显的错误,应该停止执行.
另一方面,如果您不希望这种情况发生,我认为我更喜欢2超过1,因为检查一个未定义的对象就像检查一些标志变量一样容易.这不是C,所以我们没有强类型约束告诉我们我们的构造函数必须返回这种类型的对象.因此,返回undef并检查确定成功或失败是一个很好的选择.
构造失败的"开销"是某些边缘情况下的考虑因素(在产生开销之前你不能快速失败),所以对于那些你可能更喜欢方法1.所以再次,它取决于你为对象定义的语义施工.例如,我更喜欢在构造之外进行重量级初始化.至于标准化,我认为检查构造函数是否返回已定义的对象与检查标志变量一样好.
编辑:为了响应您对初始化程序拒绝情况#2的编辑,我不明白为什么初始化程序不能简单地返回指示成功或失败的值而不是设置标志变量.实际上,您可能希望同时使用两者,具体取决于您对发生的错误的详细程度.但是初始化程序在成功和undef失败时返回true是完全有效的.
我更喜欢:
croak 当出现问题时,提供信息性的信息.另外,undef如果类的用户可能不关心为什么发生故障,只有当他们获得了有效的对象时,返回(而不是呱呱叫)是好的.
我鄙视容易忘记的is_valid方法或添加额外的检查,以确保在没有很好地定义对象的内部状态时不调用方法.
我从非常主观的角度说这些,而没有对最佳实践做出任何陈述.
我建议反对#1只是因为它会导致更多的错误处理代码无法写入.例如,如果你只是返回false,那么这很好.
my $obj = Class->new or die "Construction failed...";
Run Code Online (Sandbox Code Playgroud)
但是如果你返回一个无效的对象......
my $obj = Class->new;
die "Construction failed @{[ $obj->error_message ]}" if $obj->is_valid;
Run Code Online (Sandbox Code Playgroud)
并且随着错误处理代码的数量增加,其写入概率降低.而且它不是线性的.通过增加错误处理系统的复杂性,您实际上减少了在实际使用中会捕获的错误数量.
您还必须小心,当调用任何方法时(除了is_valid和error_message)导致更多代码和错误机会时,您的无效对象会消失.
但我同意能够获得有关失败的信息是有价值的,这使得返回错误(只是return没有return undef)低劣.传统上,这是通过调用DBI中的类方法或全局变量来完成的.
我的$ dbh = DBI-> connect($ data_source,$ username,$ password)或者死$ DBI :: errstr;
但是它会受到影响A)你仍然需要编写错误处理代码和B)它只对最后一次操作有效.
一般来说,最好的办法就是抛出异常croak.现在,在正常情况下,用户不会编写特殊代码,错误发生在问题点,并且默认情况下会得到一条好的错误消息.
my $obj = Class->new;
Run Code Online (Sandbox Code Playgroud)
Perl关于将库中代码中的异常抛弃为不礼貌的传统建议已经过时.Perl程序员(最终)接受异常.而不是一次又一次地编写错误处理代码,严重且经常忘记,例外DWIM.如果你不相信只是开始使用autodie(观看pjf关于它的视频),你永远不会回去.
异常将霍夫曼编码与实际使用对齐.期望构造函数正常工作并且如果不存在则需要错误的常见情况现在是最少的代码.想要处理该错误的不常见情况需要编写特殊代码.特殊代码非常小.
my $obj = eval { Class->new } or do { something else };
Run Code Online (Sandbox Code Playgroud)
如果你发现自己把每一个电话eval都包裹起来,那么你做错了.之所以称为例外,因为它们是例外的.如果您在上面的评论中想要为用户的利益进行优雅的错误处理,那么请利用错误在堆栈中冒泡的事实.例如,如果要提供一个不错的用户错误页面并记录错误,可以执行以下操作:
eval {
run_the_main_web_code();
} or do {
log_the_error($@);
print_the_pretty_error_page;
};
Run Code Online (Sandbox Code Playgroud)
你只需要在一个地方,在你的调用堆栈顶部,而不是分散在任何地方.您可以以较小的增量利用此功能,例如......
my $users = eval { Users->search({ name => $name }) } or do {
...handle an error while finding a user...
};
Run Code Online (Sandbox Code Playgroud)
发生了两件事.1)Users->search总是返回一个真值,在本例中是一个数组引用.这使得简单的my $obj = eval { Class->method } or do工作.这是可选的.但更重要的是2)你只需要进行特殊的错误处理Users->search.内部Users->search调用的所有方法以及它们调用的所有方法......它们只是抛出异常.而且他们都被抓到了一点并处理了同样的事情.在关心它的点处理异常会使得更简洁,紧凑和灵活的错误处理代码.
您可以croak通过使用字符串重载对象而不仅仅是字符串将更多信息打包到异常中.
my $obj = eval { Class->new }
or die "Construction failed: $@ and there were @{[ $@->num_frobnitz ]} frobnitzes";
Run Code Online (Sandbox Code Playgroud)
例外:
诸如Try :: Tiny之类的模块解决了围绕eval用作异常处理程序的大多数悬而未决的问题.
至于你的用例,你可能有一个非常昂贵的对象,并希望尝试继续它部分构建......闻起来像YAGNI给我.你真的需要它吗?或者你有一个膨胀的对象设计太早做了太多的工作.如果确实需要,可以在异常对象中放置继续构造所需的信息.