NULL,异常,错误码,空对象?
函数运行结果分为两类:一类预期结果,也就是正常输出的结果,一类非预期结果,异常或出错情况下的输出。
1.返回NULL
很多人认为这是种不好的设计思路,主要理由:
-
如果某个函数可能返回NULL值,我们使用它时,忘了做NULL值判断,就可能会抛出异常。
-
如果定义了很多返回值可能为NULL的函数,那么代码中会充斥着大量的NULL值逻辑判断,写起来比较繁琐,且跟正常业务逻辑耦合在一起影响代码的可读性。
尽管返回NULL值有许多弊端,但对于以get,find,select,search,query等单词开头的查找函数来说,数据不存在,并非一种异常情况,是一种正常行为,所以返回不能存在语义的NULL比返回异常更加合理。
对于查找数据不存在的情况,函数到底该用NULL值还是异常,有一个比较重要的参考标准是,看项目中的其他类似查找函数都是如何定义的,只要整个项目遵从统一约定即可。
2.返回空对象
当函数返回的数据是字符串类型或者集合类型时,我们可以用空字符串和空集合代替NULL值,来表示不存在的情况。这样我们在使用函数时,就可以不用做NULL值判断
3.抛出异常对象
最常用的函数出错处理方式就是抛出异常,异常可以携带更多的错误信息,比如函数的调用栈信息;除此之外,异常可以将正常逻辑与异常逻辑的处理分离开来,增加可读性。
3.1 不可恢复异常 和 可恢复异常
-
对于bug(比如数组越界)和不可恢复异常(数据库连接失败),即使捕获了,也做不了太多事情。
-
对于可恢复异常,明确告知调用者需要捕获处理(比如,输入提现金额大于余额的异常,捕获后则可直接使用余额)
3.2 如何处理函数抛出的异常?
一般有三种方法:
-
直接吞掉,输出日志
-
原封不动的向上层调用者抛出
-
包装成新的异常向上层抛出
具体该选择哪种方式处理异常呢?三个参考原则:
-
如果 func1() 抛出的异常是可以恢复的,且 func2() 的调用者并不关心此异常,我们完全可以在func2() 内将func1() 抛出的异常吞掉,输出日志即可。
-
如果func1() 抛出的异常对func2() 的调用者来说,也是可以理解的,关心的,并且在业务概念上有一定相关特性,我们可以直接将func1()抛出的异常原封不动抛出。
-
例如,获取主机名
getHostName()
出错,抛出错误getHostNameError
, 其调用者getHostNameLastparm()
与其有业务相关性,也能理解它抛出的异常getHostNameError
,则可以直接将此异常抛出
-
-
如果func1() 抛出的异常太过底层,对func2() 的调用者来说,缺乏背景去理解,且业务概念不相关,我们可以将它重新包装成调用方可以理解的新异常。
-
例如,
getHostNameLastparm()
抛出的getHostNameError
异常,其调用方generateID()
直接将其抛出,那么generateID()
的调用者将无法理解这个底层异常,我们可以在generateID()
内部将其包装为一个新的异常getIDError
抛出。此时它的调用者将一目了然,而且也没有暴露底层实现细节,破环封装的特性
-
总结:异常是否继续向上抛出,要看上层代码是否关心这个异常。关心就将它抛出,否则就直接吞掉。是否需要包装成新的异常抛出,就看上层代码是否能够理解这个异常、是否业务相关。
编写一个函数,参数传进来为NULL, 空字符串如何处理?
理论上讲,参数传递的正确性应该由程序员来保证,我们无需做NULL值或空字符串的判断和特殊处理。但谁也没法保证程序员就一定不会传递NULL和空串。那怎么办?
-
如果函数是私有的,只在内部调用,完全在自己的掌控下,保证自己调用时不传NULL和空串就行了。
-
如果是一个公共的功能函数,无法掌控会被谁调用及如何调用,可以写上相应的注释提醒调用者;但为了尽可能提高代码健壮性,最好在公共函数中做NULL和空串判断。即使有些冗余。
参考来源: 极客时间 王争 老师的 《设计模式之美》