编写高质量代码

我们知道,intdouble能表示的数值的范围不同。其中,64位有符号整数的范围是[-9223372036854775808,9223372036854775807],而64位无符号整数的范围是[0,18446744073709551615]。这两个区间有一定的overlap,而double可以表示的范围更大。

现在,需要编写两个函数:给定一个double型的value,判断这个value是否是一个合法的int64_t或者uint64_t。本文说的“合法”,是指数值上落在了范围内。

这里我们用Double而不是double,原因是我们的double不是基础数据类型,而是通过一定方法实现的ADT,这个ADT的成员函数有:

通过调用get_next_digit,可以返回一个数字,不断调用它,可以得到所有digits。举个例子,对于值为45.67的一个Double对象,调用它的get_next_digit成员函数将依次得到

4 is_decimal = false //表示整数部分

5 is_decimal = false //表示整数部分

6 is_decimal = true //表示小数部分

7 is_decimal = true //表示小数部分

get_next_digit返回-1时,表示读取完毕。

如何利用Double类里的成员函数,来实现is_valid_uint64is_valid_int64这两个函数呢?

一些新手可能会写这样的代码:

这样的代码,存在诸多问题。

设计问题

不难发现,两个函数存在很多相似甚至相同的代码;而同一个函数内部,也有不少代码重复。重复的东西往往不是好的。重构?

性能问题

先获得所有digits,然后从最低位开始向最高位构造值,效率较低。难道没有可以从最高位开始,边获得边计算,不需要临时数组存储所有digits的方法吗?

正确性问题

随便举几个例子:

24行,tmp += digits[i] * base;有没有考虑到可能的溢出呢?

68行,难道有小数部分就一定不是合法的int64吗?那么,123.000?嗯?

规范问题

帅哥,这么多代码,一行注释都没有,这样真的好吗?

因此,毫无疑问,这是烂代码,不合格的代码,需要重写的代码。

以下是我个人认为比较好的设计和实现,仅供参考。

代码规范

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

真的需要代码规范吗?

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

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

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

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

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

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

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

代码风格举例

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

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

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

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

2,级联if else语句。

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

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

是不是优美多了?前面只做一些错误处理、前期准备、参数检查等,最后的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一下。

1 9 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部