Yebangyu's Blog

Fond of Concurrency Programming and Machine Learning

编写高质量代码(下)

上回我们从微观角度,以一个实际的例子,从正确、高效、易读等特性着手,介绍了如何编写高质量的代码。这次,我们从宏观出发,从软件开发流程入手,着重介绍其中的几个方面,包括代码规范、Code Review、测试等。

如果说上回的内容注重个人编码,那么本文将偏向团队开发。

代码规范

团队的代码规范,一般由领导和大佬们制定后,大家统一实行。这里面有几个问题:

真的需要代码规范吗?

言下之意,制定和执行代码规范是否浪费时间?

答案是:It depends。如果项目很庞大、代码质量要求很高,那么,制定和执行代码规范所花费的时间,将大大少于后期因为不规范开发带来的种种调试和维护成本。如果是小打小闹的代码,就无所谓了。

代码规范的制定为什么这么难?

原因众多,其中一个很重要的部分是团队每个人的口味和观点不尽相同。就代码风格而言,有人喜欢对内置类型变量i使用i++,有人坚持认为应该使用++i不管i是不是复杂类型。因此,制定代码规范需要在讨论之后最后拍板决定,这里面甚至需要独裁!是的,独裁!

代码规范制定需要注意什么事项?

如果代码规范限制太松,那么等于没有规范;如果太严,大大影响开发效率。这里面的尺度,需要根据项目需要、团队成员特点全面考量,进行取舍。

需要注意的是,没有任何一种代码规范是完美的。例如,在C++中,如果启用异常,那么代码的流程将会被各种异常处理中断,各种try catch throw让代码很不美观;如果禁用异常,也就是在开发的过程中不能使用异常特性,那么团队成员可能因为长期没有接触这项语言feature而造成知识和技能短板。

代码风格举例

举两个我认为比较重要、比较新鲜、比较有趣的代码风格。

1,使用引用需要判空吗?

1
2
void f(int &p);
void g(int *p);

我们都知道,在g中,使用*p前需要对p是否为NULL进行判断,那么f呢?如果质量非常关键、代码安全非常重要的场景,那么实际上,也是需要的。因为调用者可能这样:

1
2
3
int *q = NULL;
//......
f(*q);

因此,需要在f里增加if(NULL == &p)的判断。

2,级联if else语句。

首先看一个我个人认为不好的代码风格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int f(int a, int b)
{
  if (a >= 1) {
    if (b >= 1) {
      if (a >= b) {
        //do sth
      } else {
        //error1
      }
    } else {
      //error2
    }
  } else {
    //error3
  }
}

这个函数的核心在于do sth部分。其实我们可以改写为级联if-else形式,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int f(int a, int b)
{
  if (a < 1) {
    //error3
  } else if (b < 1) {
    //error2
  } else if (a < b) {
    //error1
  } else {
    //so, a>=1 && b>=1 && a>=b
    //do sth
  }
}

是不是优美多了?前面只做一些错误处理、前期准备、参数检查等,最后的else分支做实实在在的功能性事情。

Code Review

什么是Code Review?

很多人把它翻译为代码审查,我觉得太政治味了。程序员尤其是新手写完代码后,可能会有风格问题(比如不符合团队的代码规范)、安全性问题(比如忘记指针判空)、优雅性问题(比如大量冗余代码)、正确性问题(比如算法设计错误),那么在发布代码到公共库之前,提交给师兄或者mentor,让他帮你review一下代码,并提出可能的问题和建议,让你好好修改。这样的过程,就叫做Code Review。

我的天呐,那这不是很占用时间?

是的。一个写代码,一个看代码,看代码的时间可能并不比全新写一份代码少。那么,这又是何必呢?

主要的原因有:

1,review确实占用了开发时间,然而开发,或者说写代码,其实只占很少的时间比例。很多时间花在debug、调试、写文档、需求分析、设计算法、维护等等上。

2,代码质量非常重要,这点时间投入是值得的。与其后期苦逼追bug,不如前期多投入点时间和人力。

3,培养新人,让新手更快成长。

如何更好的执行Code Review

这里给几点建议:

1,不走过场。走过场,还不如不要这个流程。

2,作为Reviewer,看不懂代码就把作者拉过来,当面询问,不要不懂装懂,也不要爱面子不好意思问。

3,作为Coder,心里要有感激之情。真的。不要得了便宜还卖乖,感恩reviewer,感激reviewer对自己的进步和成长所做出的贡献,所花费的心血。中国人里狼心狗肺、忘恩负义、不懂感恩的人还算少吗?

4,作为Coder,给Reviewer Review之前,请先做单元测试并确保通过,并自己尝试先整体看一遍自己本次提交的代码。注意,不要给别人提还没调试通过的代码,这是非常不尊重别人的表现。

质量保证

1,测试不是专属QA的活儿,对自己写的代码提供质量保证,是程序员的职责。QA要负责的,是系统的质量,不是模块的质量。

2,测试,需要意识,需要坚持。我发现C++程序员、前端程序员的测试意识或者说质量意识最强;数据科学家或者数据工程师的质量意识最差,很多人甚至不写测试用例。当然,这不怪他们,毕竟,有时候代码里有个bug,准确率和召回率会更高。

3,测试用例的编写和设计需要保证一定的代码覆盖率,力求让每个分支和流程的代码都走到,然后分析运行结果是否是符合期望的,不要只考虑正确路径上的那些分支。

4,测试用例的编写和设计力求全面,考虑到方方面面。以非常经典的二分搜索为例:

int binary_search(int *p, int n, int target, int &idx);

binary_search函数返回值为0表示成功执行,输出参数idx返回target在有序数组p中(第一次出现)的位置,-1表示不存在。

那么测试用例至少应该涵盖:

  • p为NULL的情况

  • 数组大小n分别为负数、0、1、2时情况

  • 数组p不是有序数组的情况

  • target在数组中出现0次、1次、n次的情况

你是否都考虑到了呢?

4,有时候,自己书写测试用例显得刀耕火种,现在已经有很多辅助的工具,读者可以自行google一下。