程序员们无时无刻不在于Bug和错误做着斗争。早在面向过程的C语言时代,错误一般是通过函数接口的返回值来指定的。我们事先对接口做好返回值约 定,然后调用者根据约定内容检查调函数回值,从而得知了函数调用的结果。典型的约定,有比如返回NULL表示调用失败,有比如返回0表示成功其他表示各种 错误原因等等。这种方式在当时看上去是个比较完美的解决方案,但是,随着程序的规模增大,这种方式日渐显出了疲态。
通过约定和检查返回值的方式检查错误,其结果是开发人员会将大量时间用于做返回值检查,这样程序逻辑里充斥着与主功能无关大量判断语句,这降低 了代码的可读性。不仅如此,开发者往往疲于检测返回值,写出了一堆一堆冗长而又臃肿的代码。但是,这还不是最为严峻的挑战。请读者考虑一下这么一种场景, 如果我们的某段代码调用了函数A,而函数A调用了30多个函数,每个函数定义了自己的返回值以指明特定错误,那么定义A的返回值将是一个大大的挑战。再试 想一下,假设这30个函数又有更深入的调用链呢?看到问题了吧,随着程序抽象层系越高,为其设计返回值约定就越为困难,相对的要检查其返回值所需的代码就 约为冗杂。 在我们疲于应付日渐臃肿的返回值约定的时候,伟大的计算机科学家们发明了异常这个好东西,它能在一定层次上缓解之前方法的问题。考虑之前的例 子,当A函数调用的30个函数之一的下层某个函数发生错误时,它只是简单的抛出这个异常,这个异常不会再经手中间层次的函数而直接汇报给了A函数的调用 者。于是乎,中间函数不需要再定义自己也难以维护的复杂的返回值约定,这大大减轻了设计者的工作量以及开发者的代码量。 那么异常抛出后,到底是由谁来处理呢?异常的设计哲学是,谁能处理谁处理。底层的接口检测到错误的发生,但它没有足够的上下文做出处理决定,因 此只能通过向上汇报的方式来找人处理。底层函数的调用者往往知道如何对某个错误做出合适的处理。而且使用异常后,我们不再需要不断地检查函数返回值,只需 要在catch语句里指明正确的处理方式即可。这样,程序的主逻辑和错误处理代码被泾渭分明的分离开来,增强了代码的可读性。 我们现在已经知道了异常的好处,那么我们该如何设计和使用异常呢?我的想法是,遵循“异常和错误分离”原则,注意我这里的异常和错误不是 java中的概念。为了理解这个原则,我们首先思考一下为什么程序需要抛异常?很容易理解,我们之所以抛异常,是因为我们发觉程序运行时,我们对系统的各 种状态的假设不能够被满足。比如我们发觉调用系统写文件失败了,又比如我们发觉某个传入对象参数是非法的null值。由于状态不满足我们的断言,所以本接 口所承诺的功能将不能实现,于是乎我们需要汇报异常给上级,告诉它这件事儿我干不了了!但是,在考虑看看,究竟是那些因素造成了我们对系统状态的断言失败 了呢?我认为有这三个原因:操作系统的、用户的、以及其他模块的。操作系统的原因就诸如读文件失败,访问网络失败等诸多问题;用户的原因包括误操作,错误 的输入等情况;而其他模块的错误则是由于自己编程错误而造成的,它可能是本函数调用的函数有bug进而不能实现功能,也可能是本函数的调用者有bug进而 传入了错误的数据。 正是由于这三类因素的存在,才使得我们最终不能完成我们的任务。但这与我的“异常和错误分离”原则有何关系呢?实际上,这里的“异常”即是指系 统和用户的因素,这类因素属于不可控因素,但是绝大部分可以事先预见到的。既然是预先可以预见到,显然应该使用一个checked exception来直接标明,这体现了我们对系统和用户可能出现的错误有过预见性的思考,因此所有接口都需要显式地使用throws语句指明这些个异 常。当然,使用checked exception会使得代码臃肿,但一方面由于这类异常随着程序规模的增大其数量增长较慢,且其种类较少,另一方面由于我们可以通过使用继承的层次结构 抽象数量较多的具体异常,从而可以将checked exception数量抑制在可控范围内。而在“异常和错误分离”原则中,“错误”则是指系统中其他部件的错误。这类错误的特点是,它们都是因为程序编码 有错误产生的,换句话说,正是因为本系统的其他构件有了bug,才使得我们调用某个函数产生了错误的结果,或者是我们被传入了一个非法的参数。由于编码 bug数量众多且难以预测其种类,因此表达这类错误便适合于使用unchecked exception,在实现上我们使用一个RuntimeException并辅以相应信息就行了。 因此,所谓“异常和错误分离”即是指,“异常”我们用checked exception指出,而“错误”我们使用unchecked exception指明。 我们了解了怎样设计异常,下面我们来思考一下,作为被调用者和调用者,我们又该在异常处理方面做些什么。 首先作为被调用者,一个问题是,我们究竟需不需要检查参数的合法性。一种想法是,既然我是被自己系统其他模块调用的,那么我们有理由相信,如果 其他模块工作正常的话,我将势必以一个正确的参数被调用,那么我再去检测传入参数的合法性还有意义么?我们把这个想法先放到一边,让我们再考虑一下,面向 对象的基本思想。在面向对象的设计哲学里面,实际上所有的东西都是对象,对象是OO程序的基本功能构件,一个对象应该是一个完整的、稳定的实体,它不应该 被外部错误的影响。这样看来,既然我们是以对象来看世界的,我们显然是不应该相信别的类,即是它是我们一个系统的。这样想来,我们着实需要对传入参数做出 严格检查,然后在检查到错误时,抛出相应异常。现在我们知道,不检查参数,可以简化程序减少工作量,检查参数,可以增强程序构件的健壮性,也便于调试。我 们究竟要不要检查这类错误,我想,完全需要你作为设计者,在工作量和程序健壮性、易调试性间取得一个良好的折中:对于一些处于系统内部的内,我们的检查相 应的放松,而对于系统边界类,我们需要较为严格的检查。 话分两头,作为调用者,我们又该做些什么呢?我们既然以异常指明错误,那么是否意味着对于我们之前某函数返回null然后检测返回值是否为 null的代码,都可以通过抛出异常、捕获异常来替代呢?我认为不是的。之前自己曾设计了一个getCurrentAccount接口,功能是返回当前用 户的账号,并且我打算通过返回null表明用户尚没有设置当前账号。但我一直在思考,最后得出,这里显然还是不应该抛出异常,因为用户未设置当前帐户显然 不属于错误啊。我们退一步再讲,无论如何,我们总需要一个判断语句指出当前是否有设置账号,所以即使使用了异常,在这里也不能发挥出其优势。因此,异常技 术并不是将以前的返回值机制武断地替换掉,而是需要根据实际情况,判断这个情况是否是错误状态,然后再做决定是否采用异常机制。 前一段时间给团队里做设计十分苦逼,特别是希望让不同人实现代码时候能够让错误和异常得到有效控制。今儿整理了一下自己的思路,于是便有了此文,希望与大家共同探讨之。 (责任编辑:admin) |