捕获和重新引发异常的最佳实践是什么?

被捕获的异常是应该直接重新抛出,还是应该围绕新的异常进行包装?

也就是说,我应该这样做:

try {
$connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
throw $e;
}

或者这样:

try {
$connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
throw new Exception("Exception Message", 1, $e);
}

如果你的答案是 直接扔,请建议使用 异常链,我不能理解现实世界中我们使用异常链的情况。

81009 次浏览

你通常是这么想的。

类可能引发许多类型的不匹配异常。因此,您可以为该类或类的类型创建一个异常类,然后抛出。

因此,使用该类的代码只需捕获一种类型的异常。

恕我直言,捕获一个异常来重新抛出它是 没用。在这种情况下,只要不捕获它,并让以前调用的方法处理它 (也就是调用堆栈中的“上”方法)

如果重新抛出异常,将捕获的异常链接到要抛出的新异常无疑是一个很好的实践,因为它将保留捕获的异常所包含的信息。然而,重新引发它只有在 添加一些信息或处理某事处理捕获的异常时才有用,可能是一些上下文、值、日志记录、释放资源等等。

添加一些信息的一种方法是扩展 Exception类,使其具有诸如 NullParameterExceptionDatabaseException等异常。而且,这使得开发人员只能捕获他能够处理的一些异常。例如,只能捕获 DatabaseException并尝试解决导致 Exception的原因,比如重新连接到数据库。

这都是为了保持抽象性。所以我建议用异常链直接投掷。至于为什么,让我来解释一下 漏洞百出的抽象概念的概念

假设你在做一个模型。该模型应该从应用程序的其余部分中抽象出所有的数据持久性和验证。那么,当您得到一个数据库错误时会发生什么呢?如果重新抛出 DatabaseQueryException,就会泄漏抽象。为了理解其中的原因,请思考一下抽象概念。您并不关心 怎么做模型存储数据,只是关心它存储的数据。同样地,您也不必关心模型的底层系统中究竟出了什么问题,只需要知道出了什么问题,以及大概出了什么问题。

因此,通过重新引发 DatabaseQueryException,您正在泄漏抽象,并且需要调用代码来理解模型下发生的事情的语义。相反,创建一个通用的 ModelStorageException,并将捕获的 DatabaseQueryException封装在其中。这样,您的调用代码仍然可以尝试从语义上处理错误,但是模型的底层技术并不重要,因为您只暴露来自该抽象层的错误。更好的是,由于您包装了异常,如果它一直冒泡并需要被记录,您可以跟踪到抛出的根异常(遍历链) ,这样您仍然拥有所需的所有调试信息!

不要简单地捕获并重新抛出相同的异常,除非需要进行一些后期处理。但像 } catch (Exception $e) { throw $e; }这样的块是没有意义的。但是您可以重新包装异常,以获得一些重要的抽象收益。

您不应该捕获异常 除非你想做些有意义的事

“有意义的事情”可能是其中之一:

处理异常

最明显的有意义的操作是处理异常,例如通过显示错误消息并中止操作:

try {
$connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
echo "Error while connecting to database!";
die;
}

日志记录或部分清理

有时候,您不知道如何在特定的上下文中正确地处理异常; 也许您缺乏关于“大图”的信息,但是您确实希望尽可能接近故障发生的地方记录故障。在这种情况下,您可能需要捕获、记录和重新抛出:

try {
$connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
logException($e); // does something
throw $e;
}

一个相关的场景是,您处于正确的位置,可以对失败的操作执行一些清理,但是不能决定如何在顶层处理失败。在早期的 PHP 版本中,这将被实现为

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
$connect->insertSomeRecord();
}
catch (Exception $e) {
$connect->disconnect(); // we don't want to keep the connection open anymore
throw $e; // but we also don't know how to respond to the failure
}

PHP 5.5引入了 finally关键字,因此对于清理场景,现在有另一种方法来解决这个问题。如果清理代码无论发生什么都需要运行(例如,在出错和成功时) ,那么现在可以在透明地允许任何抛出的异常传播的同时执行这一操作:

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
$connect->insertSomeRecord();
}
finally {
$connect->disconnect(); // no matter what
}

错误抽象(带异常链)

第三种情况是,您希望在更大的保护伞下对许多可能的故障进行逻辑分组。逻辑分组的一个例子:

class ComponentInitException extends Exception {
// public constructors etc as in Exception
}


class Component {
public function __construct() {
try {
$connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
}
}
}

在这种情况下,您不希望 Component的用户知道它是使用数据库连接实现的(也许您希望保持选项打开并在将来使用基于文件的存储)。因此,Component的规范会说“在初始化失败的情况下,将抛出 ComponentInitException”。这允许 Component的使用者捕获预期类型 同时还允许调试代码访问所有(依赖于实现的)细节的异常。

提供更丰富的上下文(异常链)

最后,在某些情况下,您可能希望为异常提供更多的上下文。在这种情况下,将异常包装在另一个异常中是有意义的,该异常包含有关错误发生时您正在尝试执行的操作的更多信息。例如:

class FileOperation {
public static function copyFiles() {
try {
$copier = new FileCopier(); // the constructor may throw


// this may throw if the files do no not exist
$copier->ensureSourceFilesExist();


// this may throw if the directory cannot be created
$copier->createTargetDirectory();


// this may throw if copying a file fails
$copier->performCopy();
}
catch (Exception $e) {
throw new Exception("Could not perform copy operation.", 0, $e);
}
}
}

这种情况类似于上面的例子(这个例子可能不是最好的例子) ,但是它说明了提供更多上下文的要点: 如果抛出一个异常,它告诉我们文件复制失败。但 为什么失败了吗?此信息在包装的异常中提供(如果示例要复杂得多,则可能包含多个级别)。

如果你考虑这样一个场景,例如创建一个 UserProfile对象导致文件被复制,因为用户配置文件存储在文件中,并且它支持事务语义: 你可以“撤消”更改,因为它们只在配置文件的一个副本上执行,直到你提交。

在这种情况下,如果你有

try {
$profile = UserProfile::getInstance();
}

结果发现一个“目标目录无法创建”异常错误,您有权利感到困惑。在提供上下文的其他异常层中包装这个“核心”异常将使错误更容易处理(“创建配置文件复制失败”-> “文件复制操作失败”-> “无法创建目标目录”)。

我们有可能产生异常的 try/catch ——因为

  • 在应用程序 中做出逻辑决策——如果根据异常类型做出逻辑决策,那么可能需要更改异常的类型。但是引发的异常类型在大多数情况下都是特定的。因此,这更是一个不对异常执行任何操作的理由,只需重新抛出异常即可。

  • 向用户显示不同的内容 ——在这种情况下,只需按原样重新抛出,然后决定在最高级别执行什么操作——在 Controller 中。您需要更改消息-以记录真正的技术消息,并显示给用户一个友好的。

  • DB 事务 -它可以属于上面两种类型中的任何一种-如果你做了一些合乎逻辑的决定或者你只是需要告诉用户一些事情。

因此,除非您有非常好的理由,否则处理异常应该只在控制器中的一个位置进行(否则会造成混淆) ,而且该位置必须是控制器的最顶部。

所有其他地方都应该被视为中介,你应该只看到例外——除非你有很好的理由不这么做。