我什么时候写自己的异常类?

fro*_*ous 20 php oop exception

我一直在想,因为当我需要编写自己的异常类扩展时,我进入了OOP的阴暗水域并写了几个分布式库.

到目前为止,我只是使用内置的异常类,它似乎很好地为我服务.是否有必要,如果可以的话,我可以编写一个异常子类.

dec*_*eze 42

当需要区分不同类型的错误时,应该使用自己的Exception类型扩展Exception类.投掷Exception公正意味着出了问题.你不知道出了什么问题.你应该放弃一切吗?这是预期的错误吗?投掷一个UserIsNotAllowedToDoThisException反而意味着更具体的东西.重要的是区分哪些代码可以处理什么样的错误:

try {
    new Foo($bar);
} catch (UserIsNotAllowedToDoThisException $e) {
    echo "Sorry, you're not allowed to do this.";
}
Run Code Online (Sandbox Code Playgroud)

当不允许某些内容时,此代码处理简单的情况.如果Foo会抛出一些其他异常,比如TheDatabaseJustCrashedAndIsBurningException,你不想在这里知道这个,你需要一些全局错误处理程序来处理它.通过区分什么地方出了错,它可以让你适当地处理问题.


好的,这里有一个更完整的例子:

首先,如果使用正确的OOP,则需要 Exceptions才能使对象构造失败.如果不能使对象构造失败,则忽略了OOP的很大一部分:类型安全性,因此数据完整性.参见例如:

class User {

    private $name = null;
    private $db = null;

    public function __construct($name, PDO $db) {
        if (strlen($name) < 3) {
            throw new InvalidArgumentException('Username too short');
        }
        $this->name = $name;
        $this->db = $db;
        $this->db->save($this->name);  // very fictional DB call, mind you
    }

}
Run Code Online (Sandbox Code Playgroud)

在这个例子中,我们看到了很多东西:

  • 我的User对象必须有一个名字.未能将$name参数传递给构造函数将使PHP失败整个程序.
    • 用户名必须至少为3个字符长.如果不是,则无法构造对象(因为抛出了异常).
  • 我的User对象必须具有有效且有效的数据库连接.
    • 未能传递$db参数将使PHP失败整个程序.
    • 未能传递有效PDO实例将使PHP失败整个程序.
      • 我不能传递任何东西作为第二个参数,它需要是一个有效的PDO对象.
      • 这意味着如果PDO实例的构造成功,我有一个有效的数据库连接.从此以后我不需要担心或检查我的数据库连接的有效性.这就是我构建一个User对象的原因; 如果构造成功,我有一个有效的用户(有效的意思是他的名字至少3个字符长).我不需要再检查一下.永远.我只需要为User对象键入提示,PHP负责其余部分.

所以,你会看到OOP + Exceptions给你的力量.如果您拥有某种类型的对象实例,则可以100%确保其数据有效.这是在任何中途复杂应用程序中传递数据阵列的巨大进步.

现在,__construct由于两个问题,上面的内容可能会失败:用户名太短,或者数据库出于任何原因无法正常工作.该PDO对象是有效的,因此连接在构造对象时正在工作,但也许它在此期间失效了.在这种情况下,调用$db->save将抛出自己的PDOException或其子类型.

try {
    $user = new User($_POST['username'], $db);
} catch (InvalidArgumentException $e) {
    echo $e->getMessage();
}
Run Code Online (Sandbox Code Playgroud)

所以我使用上面的代码来构造一个User对象.我事先检查用户名是否至少3个字符,因为这会违反DRY原则.相反,我只是让构造函数担心它.如果构造失败InvalidArgumentException,我知道用户名不正确,所以我会让用户知道.

如果数据库关闭怎么办?然后我不能继续在我当前的应用程序中做任何事情.在这种情况下,我想完全停止我的应用程序,显示HTTP 500内部服务器错误页面.这是一种方法:

try {
    $user = new User($_POST['username'], $db);
} catch (InvalidArgumentException $e) {
    echo $e->getMessage();
} catch (PDOException $e) {
    abortEverythingAndShowError500();
}
Run Code Online (Sandbox Code Playgroud)

但这是一个糟糕的方式.数据库可能随时在应用程序中的任何位置失败.我希望在每次通过数据库连接到任何地方时进行此检查.我要做的是让异常泡沫化.事实上,它已经冒出来了.异常没有抛出new User,它是在嵌套函数调用中引发的$db->save.Exception已经至少走了两层.所以我会让它更进一步,因为我已经设置了我的全局错误处理程序来处理PDOExceptions(它记录错误并显示一个很好的错误页面).我并不想在这里不用担心此特定错误.所以,它来了:

使用不同类型的异常可以让我在我的代码的某些点忽略某些类型的错误,让我的代码的其他部分处理它们.如果我有某种类型的对象,我就不必质疑或检查或担心它的有效性.如果它无效,我首先不会有它的实例.而且,如果它未能有效(如突然失败的数据库连接),对象可以通过自身的信号发生了错误.所有我需要做的是catch在合适的点(这是非常高的了),我也异常没有需要检查的东西成功与否我在程序的每一个点.结果是更少,更强大,更好的结构化代码.在这个非常简单的例子中,我只使用了泛型InvalidArgumentException.在具有接受许多参数的对象的更复杂的代码中,您可能想要区分不同类型的无效参数.因此,您将创建自己的Exception子类.

尝试通过仅使用一种类型的Exception 来复制它.尝试仅使用函数调用和复制它return false.你需要更大量的代码这样做,每次你需要做的是检查.编写自定义异常和自定义对象需要更多的代码和明显的复杂性,但是它可以为您节省大量代码,并且从长远来看使事情变得更加简单.因为任何不应该的东西(比如用户名太短的用户)都会导致某种错误.您不需要每次都检查.相反,您只需要担心要包含错误的层,而不是您是否会找到它.

并且"编写自己的例外"真的没有任何努力:

class UserStoppedLovingUsException extends Exception { }
Run Code Online (Sandbox Code Playgroud)

在那里,您已经创建了自己的Exception子类.现在,您可以throwcatch它在你的代码中的适当位置.您不需要做更多的事情.事实上,您现在已经正式声明了应用中可能出错的类型.这不是打败了很多非正式文件ifelses和return falses吗?

  • @deceze:你是对的,我认为所有"_using` return false`更好_"意见只是因为没有经验丰富的项目真正需要具有适当层次结构的异常. (2认同)
  • @frosty我扩展了我的例子,请再看看.如果那不能说服你......运气不好.:) (2认同)