阅读 33

Clean Code 之旅 - 异常、边界、单元测试

异常

在很多的代码中,错误处理就占了大量的代码量。错误处理是很重要,可以说是程序中不可或缺的成分,但是错误处理不能影响代码的逻辑。

之前也提到过,相比于返回错误码,直接抛异常的方式更为恰当些,这里的原因在于,底层函数返回一个错误码,上一层的接收函数需要针对这个错误信息进行 if/else 条件判断,时常还会产生嵌套的 if/else 条件判断,这会增添许多额外的逻辑,严重影响代码的可读性。并且错误码一旦定义了就不好修改,错误码之间的关系也很难表示出来。相对应的,异常机制可以很好地使普通逻辑和错误处理逻辑分开来,我们再利用函数对异常的部分进行封装,这样就可以让代码整洁许多,而且异常类可以利用面向对象的一些性质来进行很好的管理。

在 JAVA 中,异常被分为 checked 和 unchecked,作者建议多使用 unchecked 异常,因为 checked 异常会形成 exception chain,就是说,如果底层函数抛出一个异常,上层函数都必须对其进行声明并处理,一直这样下去就会成为一条异常链。这当中很重要的一点是,封装被破坏了,上层函数必须去了解底层函数所抛出的异常,一个异常会导致多个函数关联在一起,程序的耦合性增大,模块的可读性降低。

另外一点就是函数或是 API 最好不要返回 Null 值,如果真要这么做,可以考虑使用抛异常或是返回特殊对象来替代。这里的原因还是为了减少接收函数中的逻辑判断。函数的输入参数也是一样的,尽量不要传 Null 值,因为为了判断参数是否为 Null 会增添很多额外的操作,传一些不需要额外判断,并且逻辑正常进行的 “零值” 来代替 Null 是最好不过的。


边界

我们很多时候不会自己造轮子,基本上会用一些开源的第三方类库。但是这些类库并不是完美的,有些时候甚至会产生一些我们意料之外的问题,比如兼容性问题,或是类库更版导致一些方法不再可用等问题。一个行之有效的方法是对第三方的类库进行封装,我们可以新建一个类对需要使用到的第三方类库进行包裹,如果要使用第三方类库,就调用这个包裹类,同时我们需要防止第三方类库的对象的传递。 这么做的好处在于,如果第三方类库出现问题,我们能够更快速地定位问题,不至于让这些陌生的对象充斥在系统的各个角落。

另外书中还介绍一个 “学习测试(Learning Test)” 的概念,就是在你使用一个第三方类库之前,你需要对其功能进行测试,看看它的功能是否能够满足你的预期。我们可以模拟一个简单的产品环境,然后在这之中使用这个第三方类库,对比它在特定的条件下输出的结果,然后再决定使不使用。同时,当第三方类库因为更新发生变化后,我们也可以通过 Learning Test 来判断更新是否影响到我们所需要的功能。

注重边界的目的在于将自己不可控的部分,比如第三方类库,变成相对可控的部分。至少划清边界后,如果是因为第三方类库版本更新造成的问题,我们可以快速定位并决定是否替换。


单元测试

首先需要强调的一点是,测试和业务代码同等重要。也就是说你在保证你的业务代码 clean 的同时,也要保证 test 也是 clean 的。另外就是测试的进度和业务代码的进度必须是一样的,业务代码写到哪,测试也必须跟到哪。你可能会问为什么,回答这个问题之前,你可以想想我们为什么要写测试?测试是为了保证业务代码的可靠性,但是需求是在变化的,业务代码也需要变化,业务代码变化了那么意味着之前的测试就不再适用了,我们也需要改测试,因此,我们有下面的逻辑:

  需求变化
    |
 业务逻辑变化
    |
相对应的测试要变
复制代码

一段被测试覆盖的业务代码就好像穿了一层防护服一样,你可以很好地对其维护、扩展以及重用,这是因为测试使变化变得可能。你通过测试可以了解一个模块可以做什么,不可以做什么,做了改变会有什么样的问题,为什么产生这样的问题?需要怎样调整?而如果没有测试,一个系统在你面前就像是一个黑盒子,你对其中的细节并不会有很深的了解,即使这是你自己写的代码。

书中介绍了 TDD,在这个模式下,测试代码和业务代码可以很好地关联起来,并且测试可以覆盖到所有的角落。关于 TDD,之前有写过,可以查看 相关内容。书中给出了 TDD 的三大法则,我觉得还是蛮有借鉴意义:

  • 第一法则:在没有写出一个让对应业务逻辑失败的单元测试之前,不能写业务逻辑。
  • 第二法则:在完成一个使业务逻辑按预期失败的单元测试之前,不能写更多的单元测试。
  • 第三法则:在完成一个成功 Pass 掉对应的单元测试的业务代码之前,不能写其他的业务代码。

我们知道了单元测试的重要性之后,问题来了,我们怎么样保证测试的整洁?首先和正常代码一样,写测试也需要保证 单一职责原则,但是这一点也不用担心,因为测试是跟着代码走的,只要你的业务函数的设计保证了单一原则,那么测试按理来说也保证了这一原则。另外,测试一般来说有三个部分,第一个部分创建相关的测试数据,第二部分操作测试数据进行测试,第三部分对测试结果和自己的期望进行对比,一个测试中我们必须清楚地划分这三个部分。最后,书中给出了 FIRST 的测试原则:

  • Fast(F):测试需要能够相对高效地运行,这是因为我们要经常运行测试来确保整个系统的稳定。
  • Independent(I):测试之间不能相互依赖。确保这一点,你需要保证你的测试按任何的顺序运行都会产生相同的结果。
  • Repeatable(R):需要保证测试是可重复的。第一次运行测试成功,再不改动原代码的情况下,第二次,第三次,第一百次运行测试也都应该成功,并且需要保证的是测试不受系统环境的影响。
  • Self-Validation(S):测试的输出必须是一个 boolean 的结果,就是通过或者不通过,也就是说测试本身就需要有自证性,不能够通过其他的渠道进行验证。
  • Timely(T):测试必须是及时的,也就是说业务代码改了,测试也必须同步改。而且这里重要的一点是,测试最好是在业务代码之前改,因为如果先改业务代码,可能会导致改写出来的模块的测试很难改写。反过来,如果先写测试,我们至少可以给业务代码搭一个大致的结构,基本保证了模块的可测性。

总之,写好测试是个态度问题,当你把测试代码放到与业务代码同等重要的位置,也好测试也只是时间的问题。

关注下面的标签,发现更多相似文章
评论