如何设计例外


11

我正在努力解决一个非常简单的问题:

我现在正在服务器应用程序上工作,我需要为异常创建一个层次结构(某些异常已经存在,但是需要一个通用框架)。我什至如何开始这样做?

我正在考虑遵循以下策略:

1)出了什么问题?

  • 有人问,这是不允许的。
  • 要求进行某些操作,这是允许的,但由于参数错误而无法使用。
  • 有人问,这是允许的,但是由于内部错误,它不起作用。

2)谁在发起请求?

  • 客户端应用
  • 另一个服务器应用程序

3)消息处理:在处理服务器应用程序时,这全都与接收和发送消息有关。那么,如果发送消息出错了怎么办?

这样,我们可能会得到以下异常类型:

  • ServerNotAllowedException
  • ClientNotAllowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException(如果服务器不知道请求来自何处)
    • ServerInternalException
    • ClientInternalException
  • MessageHandlingException

这是定义异常层次结构的非常通用的方法,但恐怕我可能缺少一些明显的案例。您是否对我不涉及的领域有想法,是否知道此方法的任何缺点,或者是否有更一般的方法来解决此类问题(在后一种情况下,在哪里可以找到它)?

提前致谢


5
您没有提到要使用异常类层次结构实现的目标(这一点也不明显)。有意义的日志记录?使客户能够对不同的例外做出合理的反应?或者是什么?
拉尔夫·克莱伯霍夫

2
首先研究一些故事和用例,然后看看结果如何,这可能会很有用。例如:不允许客户请求X。客户端请求X,但该请求无效。通过他们的工作来思考谁应该处理该异常,他们可以如何处理该异常(提示,重试等等),以及他们需要什么信息才能很好地处理该异常。然后,一旦您知道具体的异常是什么,以及处理程序需要处理哪些信息,就可以将它们形成一个很好的层次结构。
没用的


1
我从未真正理解过要使用这么多不同的异常类型的愿望。通常,对于catch我使用的大多数块,我对异常的使用并不比包含的错误消息多得多。对于一个因读取文件失败而导致读取文件失败的异常情况,我实际上没有什么不同,因此在读取文件过程中我未能分配内存,因此我倾向于捕获std::exception并报告其中包含的错误消息,也许是修饰"Failed to open file: %s", ex.what()在打印之前将其保存到堆栈缓冲区中。

3
此外,我无法预期所有将首先抛出的异常类型。我现在也许可以预见到它们,但是同事可能会在将来引入新的异常,例如,因此对于我而言,直到现在和永远,我都无法知道在运行过程中可能会抛出的所有不同类型的异常。操作。因此,我通常只用一个捕获块就可以超级捕获。我见过一些人catch在单个恢复站点中使用许多不同块的示例,但通常只是忽略异常中的消息并打印出更本地化的消息...

Answers:


5

一般说明

(有点偏见)

我通常不会去详细的异常层次结构。

最重要的是:异常告诉您的调用者您的方法无法完成其工作。而且您的呼叫者必须对此有所注意,因此他不会简单地继续。无论您选择哪种异常类,都可以处理任何异常。

第二方面是日志记录。每当出现问题时,您都希望找到有意义的日志条目。这也不需要不同的异常类,只需要设计良好的文本消息即可(我想您不需要自动机即可读取错误日志...)。

第三方面是呼叫者的反应。您的呼叫者收到异常时可以做什么?在这里,使用不同的异常类是有意义的,因此调用方可以决定是重试同一调用,使用其他解决方案(例如,使用备用源)还是放弃。

也许您想将异常作为通知最终用户该问题的基础。这意味着除了为日志文件的管理文本创建一个用户友好的消息,但不需要不同的异常类(尽管这可能会使文本的生成更加容易...)。

日志记录(和用户错误消息)的一个重要方面是能够通过在某一层捕获异常,添加一些上下文信息(例如方法参数)并重新抛出该异常来使用上下文信息来修改异常。

您的阶层

谁在发起请求?我认为您不需要启动请求的信息。我什至无法想象您是如何知道某些调用堆栈的。

消息处理:这不是一个不同的方面,只是“出了什么问题?”的其他情况。

在注释中,您谈到了创建异常时的“ no logging ”标志。我认为在您创建和引发异常的地方,您可以做出是否记录该异常的可靠决定。

我能想象的唯一情况是,某个更高的层以有时会产生异常的方式使用您的API,然后该层知道它不需要打扰任何管理员,因此它默默地吞下了该异常。但这是代码的味道:预期的异常本身就是一个矛盾,暗示着要更改API。而且应该由高层决定,而不是产生异常的代码。


以我的经验,将错误代码与用户友好的文本结合使用非常有效。管理员可以使用错误代码查找其他信息。
Sjoerd '18年

1
我总体上喜欢这个答案背后的想法。根据我的经验,异常确实不应该变得像野兽那样复杂。异常的主要目的是允许调用代码解决特定问题并获得相关的调试/重试信息,而不会弄乱函数的响应。
greggle138

2

设计错误响应模式时,要记住的主要事情是确保它对调用者有用。无论您使用的是异常还是已定义的错误代码,这都适用,但是我们将仅限于使用异常进行说明。

  • 如果您的语言或框架已经提供了通用异常类,请在适当的地方和合理预期的地方使用它们。不要定义自己的类ArgumentNullExceptionArgumentOutOfRange异常类。呼叫者不会期望抓住这些。

  • 定义MyClientServerAppException基类以包含应用程序上下文中唯一的错误。永远不要抛出基类的实例。错误的响应是“最糟糕的事情”。如果存在“内部错误”,请解释该错误是什么。

  • 在大多数情况下,基类下面的层次结构应该是宽泛的,而不是深层次的。您仅需要在对调用者有用的情况下加深它。例如,如果有5种原因可能导致消息从客户端到服务器失败ServerMessageFault,则可以定义一个异常,然后为该5个错误中的每一个定义一个异常类。这样,调用方可以在需要或想要的情况下捕获超类。尝试将其限制为特定的合理情况。

  • 在实际使用所有异常类之前,请勿尝试定义它们。您将完成大部分操作。当您在编写代码时遇到错误情况时,请决定如何最好地描述该错误。理想情况下,它应该在调用者尝试执行的操作的上下文中表达。

  • 与上一点有关,请记住,仅因为您使用异常来响应错误,但这并不意味着您必须仅将异常用于错误状态。请记住,抛出异常通常很昂贵,并且从一种语言到另一种语言的执行成本可能会有所不同。对于某些语言,成本取决于调用堆栈的深度,因此较高,因此,如果在调用堆栈中存在错误,请检查是否不能使用简单的原始类型(整数错误代码或布尔标志)进行推送该错误将备份到堆栈中,因此可以将其引发到调用者的调用附近。

  • 如果您将日志记录作为错误响应的一部分,则调用者可以轻松地将上下文信息附加到异常对象上,这很容易实现。从记录代码中使用信息的地方开始。决定需要多少信息才能使日志有用(而不是一堵巨大的文字墙)。然后向后工作,以确保可以轻松地为异常类提供该信息。

最后,除非您的应用程序可以绝对妥善地处理内存不足错误,否则请不要尝试处理这些错误或其他灾难性的运行时异常。只需让OS处理它,因为实际上,这就是您能做的。


2
除了倒数第二句(关于例外的代价)之外,答案都是好的。关于异常代价的问题是令人误解的,因为在低级实现异常的所有常用方式中,抛出异常的代价完全与调用堆栈的深度无关。如果您知道错误将由直接调用者处理,但是在抛出异常之前不要从调用堆栈中删除一些函数,则其他错误报告方法可能会更好。
Bart van Ingen Schenau,

@BartvanIngenSchenau:我尝试不将其与特定语言绑定。在某些语言(例如Java)中,调用堆栈的深度会影响实例化的成本。我将进行编辑以反映出它不是那么干。
马克·本宁菲尔德

0

好吧,我建议您首先Exception为所有可能由应用程序引发的检查异常创建基类。如果您的应用程序被调用DuckType,则创建一个DuckTypeException基类。

这使您可以捕获基DuckTypeException类的任何异常以进行处理。从这里开始,您的异常应使用描述性名称进行分支,以更好地突出问题的类型。例如,“ DatabaseConnectionException”。

让我清楚一点,对于您可能希望在程序中妥善处理的情况,应该对所有这些异常进行检查。换句话说,您无法连接到数据库,因此将DatabaseConnectionException引发a,然后您可以捕获它以等待一段时间后重试。

不会看到针对非常意外的问题(例如无效的SQL查询或空指针异常)的检查异常,并且我鼓励您让这些异常超越大多数catch子句(或根据需要捕获并重新抛出),直到到达主异常为止。控制器本身,然后可以RuntimeException纯粹出于记录目的而捕获。

我个人的喜好是不要将未经检查的RuntimeException异常作为另一个异常抛出,因为根据未经检查的异常的性质,您不会期望它,因此在另一个异常下重新抛出它就是隐藏信息。但是,如果这是您的偏爱,您仍然可以抓住a RuntimeException并抛出a DuckTypeInternalException,这与DuckTypeException派生自其的原因不同RuntimeException,因此未被选中。

如果愿意,可以出于组织目的(例如与DatabaseException数据库相关的任何事物)对异常进行分类,但是我仍鼓励此类子异常从基本异常派生DuckTypeException并抽象化,并因此使用显式描述性名称来派生。

通常,随着上移调用方的调用堆栈以处理异常,尝试捕获应该越来越通用,并且在主控制器中,您将不处理DatabaseConnectionException而是简单地处理DuckTypeException所有检查到的异常。


2
请注意,该问题被标记为“ C ++”。
马丁·巴

0

尝试简化。

帮助您思考另一种策略的第一件事是:捕获许多异常与使用Java中的检查异常非常相似(对不起,我不是C ++开发人员)。由于许多原因,这不是很好,所以我总是尝试不使用它们,并且您的层次结构异常策略使我记住了很多。

因此,我建议您采用另一种灵活的策略:使用未经检查的异常和代码错误。

例如,请参见以下Java代码:

public class SystemErrorCode implements ErrorCode {

    INVALID_NAME(101),
    ORDER_NOT_FOUND(102),
    PARAMETER_NOT_FOUND(103),
    VALUE_TOO_SHORT(104);

    private final int number;

    private ErrorCode(int number) {
        this.number = number;
    }

    @Override
    public int getNumber() {
        return number;
    }
}

还有您独特的例外:

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

在此链接上找到了该策略,您可以在此处找到Java实现,在此处可以查看有关它的更多详细信息,因为上面的代码已简化。

由于需要在“客户端”和“另一个服务器”应用程序之间分离不同的异常,因此可以有多个错误代码类来实现接口ErrorCode。


2
请注意,该问题被标记为“ C ++”。
Sjoerd '18年

我进行编辑以适应这个想法。
Dherik '18年

0

异常是不受限制的,必须谨慎使用。对他们来说最好的策略是限制他们。调用函数必须处理其调用的函数或程序死亡的所有异常。只有调用函数具有用于处理异常的正确上下文。让功能更进一步的调用树来处理它们是不受限制的。

异常不是错误。当环境阻止程序完成代码的一个分支并指示要遵循的另一个分支时,就会发生这种情况。

异常必须在被调用函数的上下文中。例如:一个求解二次方程的函数。它必须处理两个例外:division_by_zero和square_root_of_negative_number。但是这些异常对于调用此二次方程求解器的函数没有意义。它们的发生是由于用于求解方程式的方法,然后简单地将其重新抛出将暴露内部并破坏封装。相反,divide_by_zero应该重新设置为not_quadratic和square_root_of_negative_number和no_real_roots。

不需要层次结构的例外。一个函数抛出的异常(标识它们)的枚举就足够了,因为调用函数必须处理它们。允许他们处理调用树是上下文无关的(不受限制的)goto。

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.