PHP中的异常
PHP5引入了异常,这是一个看着简单,实际复杂的功能,本文结合PHP的具体情况讨论一二。
为什么要有异常?
在PHP4的时代,那时候还没有异常,编码时如果要处理一些特殊情况,一般是通过返回错误码(比如类似1,2,3之类的整数)的方式来处理,类似于shell中的处理方式,随着项目尺度的增大,这种方法带来的问题会越来越明显,可以通过下面演示代码来论证这一点:
先看看错误码的调用方式:
01 $foo = foo();
02 if ($foo == FOO_ERROR_CODE) {
03 // foo error
04 }
05 $bar = bar();
06 if ($bar == BAR_ERROR_CODE) {
07 // bar error
08 }
再看看异常的调用方式:
01 try {
02 $foo = foo();
03 $bar = bar();
04 } catch (FooException $e) {
05 // foo exception
06 } catch (BarException $e) {
07 // bar exception
08 }
我们可以看到,错误码缺乏描述性,而且代码容易倾向if/else之类的拉面风格。另外,如上面代码所示,蓝色代表代码的正常流程,红色代表错误处理,可以清楚看到,采用错误码的调用方式,代码的正常流程和错误处理是混杂在一起的;采用异常的调用方式,代码的正常流程和错误处理是分离的,所以说采用异常的调用方式后,代码的可读性会大大优于采用错误码的调用方式。
什么时候使用异常?
程序出现非预期结果的时候就应该使用异常,这听起来有些模糊,举例子来说一下:
function find_person_by_id($id)
假设我们想通过主键查找某人,此时如果没有找到(比如说数据库一致性出现问题)就应该抛出异常,因为从逻辑上讲,既然你通过主键检索,那么这个人应该是必然存在的,如果找不到,就属于非预期结果。
function search_person_by_name($name)
假设我们想通过名字搜索某人,此时如果没有找到就不应该抛出异常,而应该返回空,或者类似NullPerson之类的对象,此时找不到是可以预期到的结果,既然是搜索,就可能找到,也可能找不到,所以不应该抛出异常。
仔细体会方法名字中的find何search字样,你应该能体会到我说的意思,当然,这样的原则多少有些完美化倾向,实际使用的时候大家自己推敲。
异常的坏味道?
PHP作为脚本语言,在使用自定义异常前必须先导入这个异常的定义文件,这多少让人有点纠结,设想一段代码可能会抛出十种(或者更多)类型的异常,那么甭管最终运行结果会怎么样,每次请求前你都得预先包含这些异常的定义文件,这个问题似乎没有太好的解决方法,不过你可以尽可能使用PHP内置的SPL异常类型,因为他们是不需要导入的,缺省就存在。
另外,在一个分层结构的程序中,异常同样也应该是分层的,比如说程序分为表现层,应用层,持久层,那么异常也应该对应分为表现层异常,应用层异常,持久层异常。这样区分的意义在于不会在当前层抛出一个它很难理解的异常,设想我们在表现层抛出了一个数据库无法连接(Too many connections之类的)的异常,这是多么尴尬的一件事情。以这种情况为例,最佳情况是持久层的异常应该全部在它的上一层,也就是应用层捕捉处理,即便在应用层不能立刻处理,也应该转换成一个表现层可以理解的异常在抛出,比如说可以抛出异常说:系统正忙,请稍后再试。顺着这个方向继续思考,那么每个层次的异常都应该有一个基类,比如说持久层应该有一个统一PersistenceException基类,所有的持久层异常都从这个基类继承,这样做的意义在于,当我们在应用层捕捉异常的时候,可以多一个catch(PersistenceException $e)的保险,避免有持久层异常没有得到处理而泄漏到表现层。
异常的坏味道还远不止上面提到的这些,有时候,try/catch本身就是坏味道!设想我们有一个Person模型,里面有一个前面说过的find_person_by_id方法,可能会抛出异常,此外,还有很多其他方法也都会抛出异常,那么当在若干Action中调用这个Person模型的时候,就会不断的try,不断的catch,周而复始,try/catch代码到处都是!发现问题了么?没错,问题就是重复!这个问题不好解决,当然,如果你的系统架构比较好的话,还是有办法的,比如可以通过使用装饰器模式来规避这个问题,关于装饰器模式的理论基础可以参考我以前写的Web框架审美观,通过使用装饰器模式,我们可以把每个模型的异常捕捉过程单独拿出来,作为一个装饰器存在,当有Action需要使用这个模型的时候,就可以有针对性的使用这个装饰器。
从一个理想状态来看,假设你使用MVC框架的话,try/catch的过程应该属于框架的工作职责(具体行为可以通过配置来改变),你的Action代码里不应该存在try/catch代码,这样既可以消除重复代码,而且还最大限度的保证了Action代码本身的可读性。