12

问题

什么被认为是“最佳实践”——以及为什么——在构造函数中处理错误?

“最佳实践”可以是 Schwartz 的引述,或者 50% 的 CPAN 模块都使用它,等等……;但我对任何人的合理意见感到满意,即使它解释了为什么常见的最佳实践并不是真正的最佳方法。

就我自己对该主题的看法(多年以来受 Perl 软件开发的启发),我已经看到了在 perl 模块中处理错误的三种主要方法(我认为从最好到最差列出):

  1. 构造一个对象,设置一个无效标志(通常是“ is_valid”方法)。通常与通过类的错误处理设置错误消息相结合。

    优点

    • 允许标准(与其他方法调用相比)错误处理,因为它允许$obj->errors()在错误的构造函数之后使用类型调用,就像在任何其他方法调用之后一样。

    • 允许传递附加信息(例如 >1 错误、警告等...)

    • 允许轻量级的“redo”/“fixme”功能,换句话说,如果构造的对象非常重,具有许多复杂的属性,100% 总是可以的,并且它无效的唯一原因是因为有人输入了一个日期不正确,您可以简单地执行“ $obj->setDate()”,而不是再次重新执行整个构造函数的开销。这种模式并不总是需要,但在正确的设计中非常有用。

    缺点:据我所知没有。

  2. 返回“ undef”。

    缺点:无法实现第一个解决方案的任何优点(全局变量之外的每个对象错误消息和重对象的轻量级“fixme”功能)。

  3. 在构造函数中死掉。除了一些非常狭窄的边缘情况之外,我个人认为这是一个糟糕的选择,原因太多,无法在这个问题的边缘列出。

  4. 更新:为了清楚起见,我认为(否则非常值得和一个伟大的设计)解决方案具有非常简单且根本不会失败的构造函数和一个繁重的初始化方法,其中所有错误检查都只是其中之一的一个子集出于本问题的目的,案例#1(如果初始化程序设置错误标志)或案例#3(如果初始化程序死亡)。显然,选择这样的设计,你会自动拒绝选项 #2。

4

4 回答 4

8

这取决于您希望构造函数的行为方式。

此回复的其余部分属于我个人的观察,但与 Perl 的大多数事情一样,最佳实践真正归结为“这是一种方法,您可以根据需要采取或离开。” 你所描述的偏好是完全有效和一致的,没有人应该告诉你。

实际上,如果构造失败,我宁愿死掉,因为我们设置它以便在对象构造期间可能发生的唯一类型的错误确实是大的,应该停止执行的明显错误。

另一方面,如果您不希望这种情况发生,我认为我更喜欢 2 而不是 1,因为检查未定义对象与检查某些标志变量一样容易。这不是 C,所以我们没有强类型约束告诉我们构造函数必须返回这种类型的对象。因此undef,返回并检查以确定成功或失败是一个不错的选择。

构造失败的“​​开销”是在某些边缘情况下的一个考虑因素(在产生开销之前你不能快速失败),所以对于那些你可能更喜欢方法 1 的人。同样,这取决于你为对象定义的语义建造。例如,我更喜欢在构造之外进行重量级初始化。至于标准化,我认为检查构造函数是否返回定义的对象与检查标志变量一样好。

编辑:作为对您对初始化程序拒绝案例 #2 的编辑的回应,我不明白为什么初始化程序不能简单地返回一个指示成功或失败的值,而不是设置一个标志变量。实际上,您可能希望同时使用这两种方法,具体取决于您想要了解所发生错误的详细信息。但是初始化程序在成功和undef失败时返回 true 是完全有效的。

于 2009-09-30T13:55:19.470 回答
5

我更喜欢:

  1. 在构造函数中做尽可能少的初始化。
  2. croak当出现问题时,会提供信息丰富的消息。
  3. 使用适当的初始化方法来提供每个对象的错误消息等

此外,返回undef(而不是呱呱叫)是好的,以防类的用户可能不关心失败的确切原因,只要他们得到一个有效的对象。

我鄙视容易忘记的is_valid方法或添加额外的检查以确保在对象的内部状态未明确定义时不会调用方法。

我从一个非常主观的角度说这些,而没有对最佳实践做出任何陈述。

于 2009-09-30T13:41:51.413 回答
5

我会推荐反对#1,因为它会导致更多不会编写的错误处理代码。例如,如果您只是返回 false,那么这可以正常工作。

my $obj = Class->new or die "Construction failed...";

但是如果你返回一个无效的对象......

my $obj = Class->new;
die "Construction failed @{[ $obj->error_message ]}" if $obj->is_valid;

并且随着错误处理代码数量的增加,它被编写的可能性会降低。而且它不是线性的。通过增加错误处理系统的复杂性,实际上可以减少它在实际使用中捕获的错误数量。

您还必须小心,当调用任何方法(除了is_validand error_message)时,您的无效对象会死亡,从而导致更多代码和错误机会。

但我同意能够获取有关失败的信息是有价值的,这使得返回 false (只是returnnot return undef)劣势。传统上,这是通过调用类方法或 DBI 中的全局变量来完成的。

我的 $dbh = DBI->connect($data_source, $username, $password) 或死 $DBI::errstr;

但它的缺点是 A)您仍然必须编写错误处理代码和 B)它仅对最后一个操作有效。

一般来说,最好的办法是用croak. 现在在正常情况下,用户不编写特殊代码,错误发生在问题点,并且默认情况下他们会收到一个很好的错误消息。

my $obj = Class->new;

Perl 反对在库代码中抛出异常的传统建议已经过时了。Perl 程序员(终于)接受了异常。而不是一遍又一遍地编写错误处理代码,严重且经常忘记异常 DWIM。如果您不相信就开始使用autodie观看 pjf 的视频),您将永远不会回头。

例外情况使霍夫曼编码与实际使用保持一致。期望构造函数正常工作并且如果不正常则希望出现错误的常见情况现在是最少的代码。想要处理该错误的罕见情况需要编写特殊代码。而且特殊代码很小。

my $obj = eval { Class->new } or do { something else };

如果您发现自己将每个电话都包装在 an 中,eval那么您做错了。异常被称为是因为它们是异常的。如果,如您在上面的评论中,您希望为了用户的利益而进行优雅的错误处理,那么请利用错误在堆栈中冒泡的事实。例如,如果您想提供一个漂亮的用户错误页面并记录错误,您可以这样做:

eval {
    run_the_main_web_code();
} or do {
    log_the_error($@);
    print_the_pretty_error_page;
};

你只需要它在一个地方,在你的调用堆栈的顶部,而不是分散在任何地方。您可以以较小的增量利用这一点,例如...

my $users = eval { Users->search({ name => $name }) } or do {
    ...handle an error while finding a user...
};

有两件事正在发生。1)Users->search总是返回一个真值,在这种情况下是一个数组 ref。这使得简单的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";

例外:

  • 做正确的事,不经过调用者的任何思考
  • 最常见的情况需要最少的代码
  • Provide the most flexibility and information about the failure to the caller

Modules such as Try::Tiny fix most of the hanging issues surrounding using eval as an exception handler.

As for your use case where you might have a very expensive object and want to try and continue with it partially build... smells like YAGNI to me. Do you really need it? Or you have a bloated object design which is doing too much work too early. IF you do need it, you can put the information necessary to continue the construction in the exception object.

于 2010-05-18T18:46:13.760 回答
2

首先是浮夸的一般观察:

  1. 构造函数的工作应该是:给定有效的构造参数,返回一个有效的对象。
  2. 不构造有效对象的构造函数无法执行其工作,因此是异常生成的完美候选者。
  3. 确保构造的对象有效是构造函数工作的一部分。分发一个已知的坏对象并依靠客户端检查该对象是否有效是一种确保无效对象因非明显原因而在偏远地方爆炸的可靠方法。
  4. 在构造函数调用之前检查所有正确的参数是否到位客户端的工作。
  5. 异常提供了一种细粒度的方式来传播发生的特定错误,而无需手头有损坏的对象。
  6. return undef;总是不好[1]
  7. bIlujDI' yIchegh()Qo'; 嘿嘿()!

现在到实际问题,我将其解释为“你是什么,darch,考虑最佳实践以及为什么”。首先,我会注意到在失败时返回 false 值有很长的 Perl 历史(例如,大多数核心都是这样工作的),并且很多模块都遵循这个约定。然而,事实证明这种约定会产生劣质的客户端代码,并且更新的模块正在远离它。 [2]

[对此的支持论点和代码示例证明是促使创建autodie的异常的更一般情况,因此我将抵制在此提出这种情况的诱惑。反而:]

必须检查是否成功创建实际上比在适当的异常处理级别检查异常更繁重。其他解决方案要求直接客户端完成比获取对象更多的工作,而当构造函数因抛出异常而失败时,则不需要这些工作。 [3] 异常比undef传回损坏的对象以记录错误并在调用堆栈中的各个级别注释它们具有更大的表现力,并且同样具有表现力。

如果您在异常中将其传回,您甚至可以获得部分构造的对象。根据我对构造函数与其客户的合同应该是什么的看法,我认为这是一种不好的做法,但这种行为是受支持的。尴尬。

所以:不能创建有效对象的构造函数应该尽早抛出异常。构造函数可以抛出的异常应该记录在其接口的部分中。只有可以有意义地作用于异常的调用级别才应该寻找它;很多时候,“如果这个构造失败,什么都不做”的行为是完全正确的。

[1]:我的意思是,我不知道任何return;不是严格优于的用例。如果有人打电话给我,我可能不得不提出一个问题。所以请不要。;)
[2]:根据我对过去两年读过的模块接口的极其不科学的回忆,受选择和确认偏差的影响。
[3]:请注意,与其他建议的解决方案一样,抛出异常仍然需要错误处理。这并不意味着将每个实例都包含在 an 中,eval除非您真的想对每个构造进行复杂的错误处理(如果您认为这样做,那么您可能错了)。这意味着将能够有意义地对异常采取行动的调用包装在eval.

于 2010-05-14T01:20:47.630 回答