我们在上一篇文章“一步一步,写出优雅的程序(一)”里面介绍了新手入门如何编写出一个可以满足功能的程序,但是,这这个程序中还有很多不尽人意的地方。这些不尽人意的地方包括:变量命名太随意,程序逻辑不够严谨,程序结构有待商榷等等。在这篇文章中,我们将会继续探讨这个程序的不足,并且不断地在实践中来弥补这个程序,使其足够严谨和优雅。
1、代码风格的提升
如图 1 所示,我们上一篇文章中编写的程序,假设此时你已经将一个程序的大致功能已经调试完成,那我们将这个程序称为你第一步编译出来的“草稿”。
图 1 草稿程序
首先,我们需要完善的是这个程序中这些变量和函数(这里没有函数)的命名方式。如果你去看一本关于编程方面的书,其中一定会给你介绍各种代码命名规则。目前,业界共有四种命名法则:驼峰命名法、匈牙利命名法、帕斯卡命名法和下划线命名法,其中前三种是较为流行的命名法。
驼峰命令法。正如它的名称所表示的那样,是指混合使用大小写字母来构成变量和函数的名字。如“printEmployeePaychecks();”,驼峰命名法一般以小写字母开头,后面的独立单词首字母大写,用以区分每个独立的单词。
匈牙利命名法。广泛应用于象 Microsoft Windows 这样的环境中。Windows 编程中用到的变量(还包括宏)的命名规则为匈牙利命名法,这种命名技术是由一位能干的 Microsoft 程序员查尔斯 - 西蒙尼(Charles Simonyi) 提出的。
匈牙利命名法通过在变量名前面加上相应的小写字母的符号标识作为前缀,标识出变量的作用域、类型等。这些符号可以多个同时使用,顺序是先 m_(成员变量)、再指针、再简单数据类型、再其它。这样做的好处在于能增加程序的可读性,便于对程序的理解和维护。
例如:m_lpszStr, 表示指向一个以 0 字符结尾的字符串的长指针成员变量。
匈牙利命名法关键是:标识符的名字以一个或者多个小写字母开头作为前缀;前缀之后的是首字母大写的一个单词或多个单词组合,该单词要指明变量的用途。
帕斯卡(pascal)命名法。与驼峰命名法类似,二者的区别在于:驼峰命名法是首字母小写,而帕斯卡命名法是首字母大写,如:
DisplayInfo();
以上三种程序的命名规则,各有优劣,我本人自己比较倾向帕斯卡命名法和驼峰命名法。
所以你可以自己选择一个自己顺手的命名规则。当然,还是那句话,不管哪种命名规则,只要做到易读性,无歧义,风格统一,不用拼音即可。由于我目前主要在做 Node.js,因此我个人选择 js 中用的最多的驼峰命名法。比如,图 1 的代码中,变量 a 使用了存储运算符号的,那其英语就是“calculational symbols”,而我又习惯多加一个前缀用来表示这个变量是全局变量还是局部变量,一般地,“loc”表示局部变量,“glo”表示全局变量,所以这个变量 a 最终根据我的风格会写成“locCalculateSymbol”,这种方式虽然长度比较长,但是我却可以一目了然地读出其含义。同样地,变量 b 和 c 用来表示操作数 1 和操作数 2,因此可以改写成“locOperateNumber1”和“locOperateNumber2”,变量 d 表示计算结果,因此可以写成“locCalculateResult”。最终变量规范化之后的代码如图 2 所示。
图 2 变量风格修改之后的代码
2、程序结构修改
我们之前讲述 C 语言的时候,讲过单个 if 语句其实只是一种判断执行语句,如果在做多个同一级条件判断的时候,我们可以使用选择执行语句,即 if…else if…else 这样的形式。但是,如果仅仅只是根据单个变量的不同赋值去执行不同的语句,还有一种程序结构更加合适,这种程序结构即为 switch…case 结构。因为使用 switch…case 语句编写的选择执行语句,可以帮助读者更加直观地观察这个程序的执行顺序。因此以后遇到这种“根据单个变量的不同赋值去执行不同的语句”的情况时,优先选择开关语句。因此图 2 中的代码可以改成如图 3 所示的代码。
图 3 程序结构修改
3、程序 Bug
一般软件的开发过程,不仅仅是指软件的架构设计,代码编写,调试,发布这四个过程,真正的软件开发过程还包含了软件测试,软件维护等等,这些过程称为“软件生命周期(Software Life Cycle,SLC)”。软件生命周期(Software Life Cycle,SLC)是软件的产生直到报废或停止使用的生命周期。软件生命周期内有问题定义、可行性分析、总体描述、系统设计、编码、调试和测试、验收与运行、维护升级到废弃等阶段,也有将以上阶段的活动组合在内的迭代阶段,即迭代作为生命周期的阶段。如图 4 所示。
图 4 软件生命周期
因此,我们程序员在写代码时,为了避免后续测试中产生的一大堆 Bug,一定要对每一个软件的子模块进行反复测试。软件测试有很多方式,这种用于测试单个函数的方式被称为“单体测试”或者“单元测试”。以后我们会具体讲述如何使用 Python 来编写单元测试程序。其原理为,产生足够多的条件来测试一个函数,并且监测这个函数在不同输入下的响应是否与我们预期的一致。单体测试的目的是使得整个函数代码里面的每一条语句都能满足我们对这个函数的设计期望。
但是目前我们没有单体测试程序,应该如何来大致地测试自己写的代码呢?我这里有一个方法,可以根据代码的结构进行测试。如,我们图 3 中的代码里面是一个开关语句,因此需要测试的前提是,产生一组条件,使得这个开关语句里面的所有语句都能被执行。这就需要我们在自测的时候分别输入“+,-,*,/”这四种运算符。四种运算符可以确保开关语句的四个条件分支都能被测试到。
接着,我们再来具体分析每一种运算。对于算术运算来说,“+,-,*”这三种运算来说,无论操作数为何值,它们都能正常运行,唯独使用“/”符号进行的除法运算会要求除数不能为 0,因此我们可以得出如下的测试步骤。
(1)输入“+”进入加法运算,此时随意的两个操作数都能满足条件。如图 5 所示。
图 5 加法运算测试
(2)输入“-”进入加法运算,此时随意的两个操作数都能满足条件。如图 6 所示。
图 6 减法运算测试
(3)输入“*”进入加法运算,此时随意的两个操作数都能满足条件。。如图 7 所示。
图 7 乘法运算测试
(4)输入“/”进入加法运算,此时应注意除 0 运算会出错。如图 8 所示。
图 8 除法运算测试
图 8 中,我们发现一旦当第二个操作数输入为 0 时,程序会停止输出结果,如果此时运行在没有智能操作系统的计算机上,会产生一个非可屏蔽的除 0 中断。因此,我们发现了这个程序一个致命的 Bug,即除法时候的第二个操作数为 0。此时,应该重新修改代码来解决这个 Bug。
如何来解决这个 Bug 呢?我们知道,这个 Bug 的产生主要是由于执行“locCalculateResult = locOperateNumber1 / locOperateNumber2;”语句时,locOperateNumber2 变量为 0 时产生的。因此我们只需要判断 locOperateNumber2 变量,如果为 0 时,不去执行这句语句即可。因此我们的代码可以修改成如图 9 所示的代码。
图 9 修改除 0 错误后的代码
此时,如果再有除 0 错误发生的时候,除 0 语句就不会被执行,也不就不会产生除 0 错误了。如图 10 所示。
图 10 除 0 错误的测试
如果大家自己编写代码时,一定要充分考虑代码可能会出现的 Bug,并且规划好重复的代码自测手段,要做到自己写每一个函数之前,就已经考虑好测试的条件和输出。只有如此,你的代码才不会有那么多 Bug。