前言
虽然现在的开发工具为开发健壮的程序提供了很多支持,尤其在J2ee或者.Net平台上,包括契约设计,日志,异常处理,消息传递,数据库和网络连接控制等,但是在实际应用的时候,如何根据这些工具来开发一个健壮的程序仍然没有一个准确的答案,也许经验丰富的程序员和架构师可以做得很完美,但是对于一般人来说,仍然需要一个简单而清晰的方案来告诉他们如何去做。
读过契约论的人都会有一种联想,契约很像现实社会中的合同,需要买卖双方都必须遵守,但契约论缺乏处理违约的现实方法,因此把它交给了程序员自己。程序员自己呢?往往强行规定调用者必须遵守契约,不遵守的话就认为是程序写得有问题,于是结束程序的运行并告诉程序员哪里出现了问题。
当然如果认为契约的作用就是这些,那这种做法无可厚非。可是想想我们知道契约概念以前写的程序,不也是检查参数,然后打出log,错误的话再退出。所以契约论只是对我们已经做的事情做一些整理,澄清认识上的模糊,并更好的使用这一方法论,仅此而已。
借鉴现实社会会让我们收获更多。我老早就想,程序的世界应该变得更智能,更复杂,复杂到没有人敢去探究某个程序到底在怎样工作,而只是根据他的行为来做出判断。让我们把契约的概念扩展一下,契约规定了甲方(调用者)和乙方(被调用者)的权利和义务,一般来说还有一个仲裁机构,如果甲方的请求是不合理的,乙方除了通知甲方之外,还可以请求仲裁机构来处理,比如把甲方加入到黑名单里。同样,如果乙方没办法完成甲方的请求, 甲方也同样可以请求仲裁机构做一些事情,比如强制执行(检查失败的原因,并尽量解决)。
异常处理(try, throw, finally)一直是面向对象语言所鼓吹的功能,的确这个功能很有用,但是也让程序员陷入了一个可怕的怪圈:如何定义各种各样的异常以及如何去处理它们。记得有篇文章还特地讲述如何设计异常树,所以我真的希望一个开发平台在提供这些功能的时候,不要简单的说它有什么功能,还要告诉我们这帮愚笨的开发者采取什么样的开发模式才最划算。
异常处理还有一个显而易见的特点,总是由调用者来处理执行中的异常,即使这个异常是自己产生的。就象一个监管部门同时也在监管自己一样,一定会产生腐败,即使程序是死的,别忘了程序是活着的人开发的。一个健壮的系统应该让各子系统各司其职,权利不能太大,这样协调起来才会方便。
日志系统是最近几年才广泛应用的,对于广大程序员来说真是一个福音,我们再也不用自己编写debug的框架了。不过别忘了,日志系统不仅可以让你容易的找到错误或者让你看到程序正在稳定良好的运行,它有很多用处。
ASF(Application System Framework)不会给你一个完美的解决方案,但是你可以把她看作一个方法论,用作你设计系统的一个原则。这个方法论包括几个元素,如甲方(Client),乙方(Server),请求(Request),响应(Response),监控器(Monitor),日志管理(Logger)等,虽然不能在细节上解决一切,但至少可以让你觉得系统之间的交互不再成为问题。
异常的分类
在准备写程序或写好程序准备交货的时候,程序员通常要对他自己的作品做一些假设,比如一个人事管理系统,系统正常运行的必要条件是:
1. 数据库服务器运行正常
2. 应用服务器运行正常
3. 客户通过假定的渠道(浏览器或专用客户端)来访问,并且不执行需求定义以外的操作。
4. 应用程序启动时的状态是良好的,包括配置文件,用户数据等。
我们可以认为,上面任何一条出现了问题造成系统崩溃,程序员可以不负责任。当然负责的程序员可以采取一些措施来避免更大的损失,比如数据备份,容错处理等。碰到这样的问题,一般只有通知系统管理员来处理了,程序本身无能为力。因此我们把这类问题称为系统错误,而不称为异常。
现在考虑程序的逻辑,还是以人事管理系统为例,包括界面,人事数据管理,报表管理,数据库服务器等几个子系统,如果客户请求生成当前系统中所有人员的工资报表,程序的逻辑如下:
1. 用户向报表请求人员工资报表
2. 报表系统向人事数据管理系统请求人事数据和工资数据
3. 人事管理系统向数据库请求数据
4. 人事管理系统向报表系统返回数据
5. 报表系统根据数据和报表模板生成报表
6. 报表显示,等待下一步命令
作为程序员是很痛苦的,尤其是“面向接口编程”泛滥以后,因为程序员不清楚在哪个尺度上处理异常最合适,因此经常可以看到一个很小的函数里面放了一大堆处理异常情况,程序员不想这么做,他的经理肯定也不想这样(效率低,代码冗长)。其实根据经验我们也不难得出结论:只有你的程序提供了外部接口给别人用,检查输入参数才是必须的。其它情况下你可以不检查,也可以做一些检查以提高模块的独立性。
有点说远了,回到刚才说的统计工资报表的逻辑,我们可以认为独立的模块就是报表系统,人事数据管理系统和数据库系统,它们之间一定要检查接口参数和返回数据,检查的结果也有以下几种情况:
1. 用户没有权限
2. 人事数据管理系统没有准备好
3. 数据库正忙,或连接无法建立
4. 数据库无法执行给定的sql语句
5. 请求查询的人员或工资记录不存在
6. 返回的某个字段不是期望的类型
7. 某些关键的数据丢失,比如工资字段为空或0
8. 报表模板不存在
9. 数据和报表模板无法匹配。
10. 人事数据管理系统被禁止访问数据库
从业务层面看起来,可能发生异常的情况很多,而且基本上都和业务模型有关,也难怪没有一个统一的原则来处理了。但我们换一个角度来看,所有的系统和系统之间的交互都可以看作是客户端向服务器端发起的请求以及服务器所做的回应,联想到HTTP的协议,上面的种种情况也可以分为客户端错误和服务器端错误两种。更细分一下可以分为:
a. 客户端请求不符合预先规定的契约格式 (4)
b. 客户端请求无效(1,5,10-人事数据管理系统作为客户端)
c. 服务器返回的数据无效(6,7)
d. 服务器端忙(2,3)
e. 服务器端内部错误(10-人事数据管理系统作为服务器)
f. 客户端资源没有准备好(8,9)
如何处理异常
契约论提倡断言(assert)不是用来处理异常,但遗憾的是很多人混淆这一点。想象一下你的程序是对外开发的,客户端可能用各种方式来使用你的服务,要想让你的程序看起来很健壮,千万不要随便就停止运行(除非继续运行可能损害系统数据)。
碰到错误我们有几种选择:
1. 忽略它
2. 报告给管理员
3. 尝试其它完成任务的办法
4. 转换成其它异常继续交给别人处理
在运行过程中,程序至少应该对两种人负责,一是使用者,程序应该能够给使用者一个友好的界面,即使完不成用户心理上也会好一些。二是系统管理员,要让管理员知道今天发生了哪些错误,数据是否被破坏,以便管理员查找解决问题的办法。
一般来说系统是分层次的,用户只和界面层打交道,因此当数据层发生异常的情况时,比如数据库连接无法建立,反馈到用户那里应该显示“系统忙,请稍候”,或“系统正在维护中,谢谢关注”。因此各个层次的异常应该是不同的,即使错误原因只有一个。
服务器和客户端是相互监督的,任何一方发现另外一方违反了契约,就可以向系统管理员发出警告,并写入日志。
一般来说,开发工具和运行平台(SDK, platform)已经提供了很完善的底层异常表示,比如J2SE,提供了包括SQLException, IOException, ArrayIndexOutOfBoundsException在内的各种异常类,这些异常如果被写入日志,系统管理员就比较容易检查和修复错误。遗憾的是很多应用程序都把这些输出到屏幕上(tomcat也是这样^_^)。比较好的做法是对这些异常进行检查,从业务层的角度进行封装并报告给用户,同时把实际的Exception写入系统日志。