例外-“发生了什么”与“做什么”


19

我们使用异常来使代码的使用者以有用的方式处理意外行为。通常,异常是围绕“发生的情况”构建的,例如FileNotFound(我们无法找到您指定的文件)或ZeroDivisionError(我们无法执行1/0操作)。

如果有可能指定消费者的预期行为怎么办?

例如,假设我们有fetch资源,该资源执行HTTP请求并返回检索到的数据。而不是像ServiceTemporaryUnavailable或之类的错误,RateLimitExceeded我们只会提出一个RetryableError建议,即消费者应该重试该请求,而不关心特定的失败。因此,我们基本上是在建议呼叫者采取一项行动-“该做什么”。

我们不经常这样做,因为我们不了解消费者的所有用例。但是,想象一下这是一个特定的组成部分,我们确实知道呼叫者的最佳操作过程-那么我们应该使用“做什么”方法吗?


3
HTTP尚未执行此操作吗?503是一个临时故障回复,所以请求应重试,404是一个根本不存在,所以它是没有意义的重试,301名的意思是“永久移动”,那么你应该重新尝试,但使用不同的地址,等等
克里安2015年

7
在许多情况下,如果我们确实知道“该做什么”,我们可以使计算机自动执行该操作,而用户甚至不必知道发生任何错误。我认为只要我的浏览器收到301,它都会直接转到新地址,而不会询问我。
Ixrec

@Ixrec-也有相同的想法。但是,消费者可能不想等待另一个请求并忽略该商品或完全失败。
Roman Bodnarchuk

1
@RomanBodnarchuk:我不同意。这就像说一个人不需要说中文就可以说中文。HTTP是一种协议,客户端和服务器都应知道并遵循它。协议就是这样工作的。如果只有一方知道并遵守,那您就无法交流。
克里斯·普拉特

1
老实说,这听起来像是您尝试用catch块替换异常。这就是为什么我们移至异常的原因-不再if( ! function() ) handle_something();,但能够在您真正了解调用上下文的地方处理错误-即,如果服务器发生故障,则告诉客户端调用sys admin;如果连接断开,则告诉客户端自动重新加载,但会提醒您如果调用方是另一个微服务。让捕获块处理捕获。
2015年

Answers:


47

但是,想象一下这是一个特定的组件,我们确实知道呼叫者的最佳操作过程。

对于您的至少一个呼叫者来说,这几乎总是失败,这种行为令人难以置信。不要以为你最了解。告诉您的用户正在发生的事情,而不是告诉您您认为他们应该怎么做。在许多情况下,已经知道应该采取什么明智的措施(如果不是,请在用户手册中提出建议)。

例如,即使您问题中给出的例外情况也证明了您的错误假设:a ServiceTemporaryUnavailable等同于“稍后重试”,RateLimitExceeded等同于“哇,放松一下,也许调整您的计时器参数,然后在几分钟内重试”。但是用户可能还想发出某种警报ServiceTemporaryUnavailable(这表明服务器有问题),而不是发出警报RateLimitExceeded(这不是问题)。

给他们选择


4
我同意。服务器应仅正确传达信息。另一方面,文档应清楚地概述在这种情况下的正确做法。
尼尔

1
这方面的一个微小缺陷是,对于某些黑客来说,某些异常可以使他们了解您的代码在做什么,并且他们可以使用它来找出利用它的方法。
法拉普2015年

3
@Pharap如果您的黑客可以访问异常本身而不是错误消息,则您已经迷路了。
corsiKa 2015年

2
我喜欢这个答案,但它缺少我认为的例外要求...如果您知道如何恢复,那将不是例外!例外应该仅在您不能对其采取任何措施的情况下出现:无效的输入,无效的状态,无效的安全性-您无法以编程方式修复这些异常。
corsiKa 2015年

1
同意 如果您坚持指出是否可以重试,则始终可以使相关的具体异常继承自RetryableError
sapi 2015年

18

警告!C ++程序员带着各种不同的想法进入这里,试图回答一个肯定与另一种语言有关的问题,应该如何进行异常处理!

有了这个想法:

例如,假设我们有获取资源,该资源执行HTTP请求并返回检索到的数据。而不是像ServiceTemporaryUnavailable或RateLimitExceededed这样的错误,我们只会引发RetryableError,建议使用者它应该仅重试该请求,而不关心特定的失败。

...我想建议的一件事是,您可能会把报告错误的担忧与采取的应对措施相提并论,以可能降低代码通用性或要求大量“转换点”作为例外的方式。

例如,如果我对涉及加载文件的事务进行建模,则可能由于多种原因而失败。加载文件可能涉及加载用户计算机上不存在的插件。也许文件只是损坏了,我们在解析文件时遇到了错误。

不管发生什么情况,我们都可以说要采取的行动是向用户报告发生的情况,并提示用户要执行的操作(“重试,加载另一个文件,取消”)。

投掷者VS捕手

无论在这种情况下我们遇到哪种错误,都应遵循该行动方针。它没有嵌入到解析错误的一般思想中,也没有嵌入未能加载插件的一般思想中。它嵌入了在加载文件的精确上下文(加载文件和失败的组合)期间遇到此类错误的想法。因此,通常来说,粗略地说,我将其视为catcher's确定响应抛出的异常(例如,向用户提供选项的提示)而采取的措施的责任,而不是thrower's

换句话说,throw例外的站点通常缺少这种上下文信息,尤其是当抛出的功能通常适用时。即使在完全分散的环境中,当他们拥有此信息时,您也最终通过将其嵌入到throw站点中而在恢复行为方面陷入困境。catch通常是那些拥有最多信息的站点,这些站点可用来确定某项行动方案,并为您提供一个中心位置来修改该行动方案是否应针对给定交易进行更改。

当您开始尝试引发异常时,不再报告错误所在,而是尝试确定要做什么,这可能会降低代码的通用性和灵活性。解析错误不应总是导致这种提示,它会因引发此类异常(引发异常的事务)的上下文而异。

盲人投掷者

通常,异常处理的许多设计通常围绕盲目抛出的思想进行。它不知道异常将如何被捕获,或者在哪里。对于使用手动错误传播的更旧形式的错误恢复,也是如此。遇到错误的网站不包含用户的操作方针,它们仅嵌入最少的信息以报告遇到的错误类型。

责任倒置和麦田守望者

在更仔细地考虑这一点时,我试图想象这种代码库可能成为一种诱惑。我的想象力(可能是错误的)是,您的团队仍在扮演“消费者”的角色,并且也实现了大多数调用代码。也许您有很多不同的事务(很多try块),所有事务都可能遇到相同的错误集,并且从设计角度来看,所有事务都应导致统一的恢复操作过程。

考虑到Lightness Races in Orbit's良好答案的明智建议(我认为这实际上是来自于面向图书馆的高级思维方式),您可能仍然倾向于抛出“做什么”异常,而仅在事务恢复站点附近。

在这里可能会找到一个中间的,通用的交易处理站点,该站点实际上集中了“该做什么”的关注点,但仍在捕获的范围内。

在此处输入图片说明

仅当您可以设计所有这些外部事务都使用的某种通用函数(例如:输入另一个函数以调用的函数或具有可重写行为的抽象事务基类来建模此做复杂捕获的中间事务站点)时,这才适用)。

然而,这可能是负责响应各种可能的错误而集中用户的操作过程,并且仍然在捕获而不是抛出的范围内。简单的示例(Python式的伪代码,我丝毫也不是经验丰富的Python开发人员,因此可能会有更惯用的方式进行此操作):

def general_catcher(task):
    try:
       task()
    except SomeError1:
       # do some uniformly-designed recovery stuff here
    except SomeError2:
       # do some other uniformly-designed recovery stuff here
    ...

[希望有一个比general_catcher] 更好的名字。在此示例中,您可以传入一个函数,该函数包含要执行的任务,但仍会因您感兴趣的所有异常类型而从通用/统一捕获行为中受益,并继续扩展或修改所有“做什么”部分您喜欢从这个中心位置出发,但仍处于catch通常鼓励这样做的环境中。最重要的是,我们可以使用“该做什么”(保留“盲人投掷者”的概念)来防止投掷场所对自己的关注。

如果您发现这里的这些建议均无济于事,并且无论如何都会抛出“做什么”异常的强烈诱惑,主要是要意识到,这至少是非常反习惯的做法,并且有可能阻止普遍的思维方式。


2
+1。我以前从未听说过“盲人投掷者”的想法,但是它确实适合我对异常处理的看法:陈述错误发生的原因,不要考虑应该如何处理。当您负责整个堆栈时,很难(但很重要!)将职责明确地分开,被调用方负责通知问题,而调用方负责处理问题。被呼叫者仅知道要执行的操作,而不是为什么。处理错误应在“为什么”的上下文中完成,因此:在调用方中。
Sjoerd Job Postmus

1
(另外:我不认为您的答复是C ++特有的,但通常适用于异常处理)
Sjoerd Job Postmus

1
@SjoerdJobPostmus是的,谢谢!“盲人投掷者”只是我在这里想出的一个愚蠢的类比-我不是很聪明或不擅长消化复杂的技术概念,所以我经常想找些很少的图像和类比来试图解释和增进我自己的理解东西的。也许有一天,我可以尝试写一些充满卡通图画的编程书。:-D

1
嘿,那是一个很好的小图像。一个戴着眼罩并扔出棒球状异常的卡通人物,不确定是谁抓到它们(甚至根本不会抓到它们),但履行了作为盲人投掷者的职责。
Blacklight Shining

1
@DrunkCoder:请不要破坏您的帖子。Internet上已经有足够多令人讨厌的东西。如果您有充分的理由要删除,请标记您的帖子以引起主持人注意并提出您的理由。
罗伯特·哈维

2

我认为在大多数情况下,最好将参数传递给函数以告诉它如何处理这些情况。

例如,考虑一个函数:

Response fetchUrl(URL url, RetryPolicy retryPolicy);

我可以传递RetryPolicy.noRetries()或RetryPolicy.retries(3)等等。如果发生可重试失败,它将咨询该策略以决定是否应重试。


不过,这与将异常抛出回调用站点并没有多大关系。你在谈论的东西有些不同,这是很好的,但问题没有真正部分..
轻竞赛与莫妮卡

相反,@ LightnessRacesinOrbit。我将其作为将异常返回到呼叫站点的想法的替代方法。在OP的示例中,fetchUrl将引发RetryableException,我是说您应该告诉fetchUrl什么时候应该重试。
温斯顿·

2
@WinstonEwert:尽管我同意LightnessRacesinOrbit的观点,但我也确实看到了你的观点,但可以将其视为表示相同控件的另一种方式。但是,请务必考虑您可能要通过new RetryPolicy().onRateLimitExceeded(STOP).onServiceTemporaryUnavailable(RETRY, 3)或其他方法,因为RateLimitExceeded可能需要与进行不同的处理ServiceTemporaryUnavailable。在写完之后,我的想法是:最好抛出一个异常,因为它提供了更灵活的控制。
Sjoerd Job Postmus

@SjoerdJobPostmus,我认为这取决于。速率限制和重试逻辑很可能是在您的库中产生的,在这种情况下,我认为我的方法很有意义。如果将它留给您的呼叫者更有意义,则一定要抛出该错误。
温斯顿·埃韦特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.