王争《设计模式之美》学习笔记
从上节课的 ID 生成器代码讲起
对于 generate() 函数,如果本机名获取失败,函数返回什么?这样的返回值是否合理?对于 getLastFiledOfHostName() 函数,是否应该将 UnknownHostException 异常在函数内部吞掉(try-catch 并打印日志)?还是应该将异常继续往上抛出?如果往上抛出的话,是直接把 UnknownHostException 异常原封不动地抛出,还是封装成新的异常抛出?对于 getLastSubstrSplittedByDot(String hostName) 函数,如果 hostName 为 NULL 或者是空字符串,这个函数应该返回什么?对于 generateRandomAlphameric(int length) 函数,如果 length 小于 0 或者等于 0,这个函数应该返回什么?
函数出错应该返回啥?
1. 返回错误码
在 C 语言中,错误码的返回方式有两种:
一种是直接占用函数的返回值,函数正常执行的返回值放到出参中;另一种是将错误码定义为全局变量,在函数执行出错时,函数调用者通过这个全局变量来获取错误码。
2. 返回 NULL 值
网上很多人不建议函数返回 NULL 值,认为这是一种不好的设计思路,主要的理由有以下两个:
如果某个函数有可能返回 NULL 值,我们在使用它的时候,忘记了做 NULL 值判断,就有可能会抛出空指针异常(NullPointer Exception,缩写为 NPE)。如果我们定义了很多返回值可能为 NULL 的函数,那代码中就会充斥着大量的 NULL 值判断逻辑,一方面写起来比较繁琐,另一方面它们跟正常的业务逻辑耦合在一起,会影响代码的可读性。 那我们是否可以用异常来替代 NULL 值:
对于以 get、find、select、search、query 等单词开头的查找函数来说,数据不存在,并非一种异常情况,返回代表不存在语义的 NULL 值比返回异常更加合理。不过,还有一个比较重要的参考标准是,看项目中的其他类似查找函数都是如何定义的,只要整个项目遵从统一的约定即可。如果项目从零开始开发,并没有统一约定和可以参考的代码,你只需要在函数定义的地方注释清楚,选择两者中的任何一种都可以。除了返回数据对象之外,有的还会返回下标位置,比如 Java 中的 indexOf() 函数,返回值类型为基本类型 int,有两种思路:
一种是返回 NotFoundException。一种是返回一个特殊值,比如 -1。显然 -1 更加合理,“没有查找到”是一种正常而非异常的行为。
3. 返回空对象
返回 NULL 值有各种弊端,应对这个问题有一个比较经典的策略,那就是应用空对象设计模式(Null Object Design Pattern),后面的章节会详细讲。当函数返回的数据是字符串类型或者集合类型的时候,我们可以用空字符串或空集合替代 NULL 值,来表示不存在的情况。
4. 抛出异常对象
最常用的函数出错处理方式就是抛出异常:
异常可以携带更多的错误信息。异常可以将正常逻辑和异常逻辑的处理分离开来,这样代码的可读性就会更好。
非受检异常(Unchecked Exception)
像 C++ 和大部分的动态语言(Python、Ruby、JavaScript 等)都只定义了一种异常类型:运行时异常(Runtime Exception)。对于运行时异常,我们在编写代码的时候,可以不用主动去 try-catch,编译器在编译代码的时候,并不会检查代码是否有对运行时异常做了处理。运行时异常也叫作非受检异常。对于代码 bug(比如数组越界)以及不可恢复异常(比如数据库连接失败),即便我们捕获了,也做不了太多事情,所以,我们倾向于使用非受检异常。非受检异常不需要显式地在函数定义中声明,那我们在使用函数的时候,就需要查看代码才能知道具体会抛出哪些异常。非受检异常不需要强制捕获处理,那程序员就有可能漏掉一些本应该捕获处理的异常。
受检异常(Checked Exception)
像 Java,除了运行时异常外,还定义了另外一种异常类型:编时异常(Compile Exception)。对于编译时异常,我们在编写代码的时候,需要主动去 try-catch 或者在函数定义中声明,否则编译就会报错。编译时异常也叫作受检异常。对于可恢复异常、业务异常,比如提现金额大于余额的异常,我们更倾向于使用受检异常,明确告知调用者需要捕获处理。
Java 支持的受检异常一直被人诟病
受检异常需要显式地在函数定义中声明。函数会冗长、可读性差。编译器强制我们必须显示地捕获所有的受检异常,代码实现会比较繁琐。受检异常的使用违反开闭原则。
如何处理函数抛出的异常
直接吞掉。原封不动地 re-throw。包装成新的异常 re-throw。是否往上继续抛出,要看上层代码是否关心这个异常。是否需要包装成新的异常抛出,看上层代码是否能理解这个异常、是否业务相关。