本文将从变量,语句,代码块,子程序,到类以及框架设计,详细描述了如何编写高质量的程序。尽管大部分原则你可能都知道了,但还是有些点会带给你惊喜。
变量
变量初始化原则
- 声明的时候初始化
- 在靠近变量第一次使用的位置初始化,就近原则。
- 理想情况下,在靠近第一次使用变量的位置声明和定义该变量,但是在JS里面却习惯将变量声明提前。
- 注意计数器和累加器的修改。
- 在类的构造函数中初始化数据成员
- 确定是否需要重新初始化
- 把每个变量用于唯一用途
变量作用域优化
作用域指变量在程序内的可见和可引用范围。介于同一变量多个引用点之间的代码可称为”攻击窗口(window of vulnerability)”,应把变量的引用点尽可能集中在一起,减小”攻击窗口“的范围。
- 尽量缩短变量的引用范围
- 尽量缩短变量的存活时间
- 把相关语句提取成单独的子程序
- 尽量少使用全局变量。使用全局变量可以让程序写起来很方便,因为全局变量可以随时访问和使用,但是这样很难维护和管理,如果换人来维护这些代码他很难知道这些变量在哪里在什么时候会被修改。
变量命名原则
- 规范命名的目的是提高程序的可读性同时易于调试
- 变量名需要准确描述其代表的事物
- 变量名的平均长度在10到16个字符时更易于调试。这并不是说你要把所有的变量都控制在这个范围,命名的最终目的提高可读性和可维护性,当你检查代码时发现大部分变量名都很短或者含义不清时,那你的命名肯定有问题
- 长名变量适合全局变量,短名的适合局部变量
- 将计算值限定词作为后缀。Total,Sum,Average,Max,Min,Str,Pointer等表示计算的限定词一般放在后面。
- 使用业界约定俗称的变量。比如i,j,temp,flag这些,不用解释都知道。
- 使用团队命名规范,不同团队,不同语言的命名原则会有不同,优先服从规范。
代码阅读的次数远大于编写的次数,确保你的名字更易于阅读,而不是易于编写。
变量缩写原则
- 使用标准的缩写,如min,sub,str等
- 去掉所有非前置元音,如computer->cmptr,screen->scrn,apple->appl
- 去掉虚词and,or,the等
- 使用单词的前几个字母,统一在每个单词的第N个字母后截断
- 去除无用后缀,如ing,ed等
- 保留每个音节中最引人注意的发音
- 确保不要因为缩写而改变了变量的含义,或者缩写后的变量名有歧义或者很难理解
语句
直线型代码
组织直线型代码最主要的原则就是按照依赖关系进行排列。所谓依赖关系就是下一行代码是否会依赖上一行代码的执行,是则为顺序相关依赖,否则为顺序无关依赖。可以用好的子程序名,参数列表,注释来让顺序相关依赖变得更明显。如果代码之间没有顺序依赖关系,那就设法使相关的语句尽可能地接近。
条件语句
if语句使用原则
- 先处理正常路径,再处理不常见情况
- 考虑else语句。虽然5到8成的代码都会有else语句,但有些情况是在程序一开始就做一个if判断,是则返回,不执行后面的代码,这样可以避免将后面的代码全都嵌套在else子语句中。但无论是否有else,请都将子句用大括号括起来。
- 简化复杂的条件检测。在if/elseif语句中,经常会有很复杂的逻辑判断,为了提高可读性,可将这些逻辑判断封装成布尔函数。
- 考虑将if/elseif 替换成case.
case 语句
case语句适合处理简单易分类的数据,如果你的数据并不简单,请使用if/elseif语句。
- 按字母/数字顺序排列各种情况
- 优先处理正常情况
- 按执行效率排列case语句
- 如果在某个case后面没有break,请注释说明。
- 利用default子句来检测错误
表驱动法
直接访问表
在前端开发,针对后台返回的错误码,通常不会直接用if/else判断错误码来显示相应地错误信息,而是将错误码-错误提示存放在”表“对象中,通过传入错误码来返回错误提示,这就是最简单的表驱动法——直接访问表。
当然我们可能会遇到更加复杂的情况,比如某活动要给1到100岁的人提供优惠,不同年龄的人群优惠可能相同也可能不同。如果将年龄作为key,优惠作为value,那么最笨得方法是存储100个键值对,当然这里面的值会有重复的。
解决方法就是做键值转换,将年龄转化成另外一个键,然后让该键对应到具体优惠。
索引访问表
键值转换提供了一个很好地思路,那就是将表的”查询条件“和”查询记录"分开管理,建立索引。索引访问表适合处理表记录占用空间比较大得情况,操作索引中的记录往往比操作主表本身的记录更方便廉价,并且由于索引和主表是分开的,同一个主表可以根据不同查询条件建立不同索引,灵活性更强,后期可维护性也更好。
阶梯访问表
索引访问的一个问题就是如果键的取值范围很大的话,那建立的索引就会很长很占空间,阶梯访问表则是对某些情况下的一种优化。
阶梯访问的基本思想是:表中的记录对于不同的数据范围有效,而不是不同的数据点。相对于索引访问,通常将输入数据映射到指定数据范围,饭后取得对于值的过程是比较耗时的,这其实是一种用时间换空间的方式。具体采用哪种表驱动方法,就看时间和空间哪个对你更重要了。
高质量的子程序
创建子程序最主要的目的是提高程序的可管理性,当然也有其他一些好的理由。其中,节省代码空间只是一个次要原因,更重要的是能提高可读性、可靠性和可修改性。
高质量的子程序可以:
- 降低和隔离复杂度
- 引入中间层,易懂的代码
- 提高可移植性
- 改善性能
- 隐藏实现细节,隐藏全局数据
- 限制变化带来的影响
- 形成中央控制点
- 达到特定的重构目的
高质量的子程序应该是功能上高内聚的,有着良好的命名。说到命名,一直很矛盾,怎样才能算是一个好的命名?按什么标准?书中给了参考:
- 描述子程序所做的所有事情。要完整的描述一个子程序,名字可能会很长,这个时候除了使用缩写,还应该思考一下这样的子程序本身是不是有问题。
- 避免使用无意义或模糊的词。计算机是明确的,doSomething这样的函数名只是用来教学。
- 不要通过数字来标识。看到handle1,handle2这样的命名是不是很愤怒,哈哈。
- 根据需要确定子程序名字的长度。研究表明,变量名的最佳长度是9到15个字符。我不知道这个调查是针对特定编程语言还是所有编程语言,按理说应该是语言无关,但我怎么有种感觉,Java或者C++代码的命名普遍比JS中的要长?
-给函数命名时要对返回值有所描述。就是说看到函数名就知道它会返回什么。比如xxx.isReady()看名字就知道返回布尔型,xxx.next()返回下一个与xxx相关的对象。 - 给过程起名时使用语气强烈的动宾形式。比如printDocument,checkOrderInfo。但是在面向对象语言中,比如JS,通常不用加宾语,因为宾语就是对象本身,比如document.print(),orderInfo.check()。
- 准确使用对仗词。比如add/remove,open/close。fileOpen对fileClose,fileOpen对fClose就会很奇怪。
- 为常用操作确定命名规则。
书中还说了一个比较有趣的问题,子程序可以写多长?理论上认为的子程序最佳长度是一屏代码或打印出来一到两页纸的长度,约20200行(原书是50150行)。人们已经在子程序长度的问题上做了大量统计和研究,但并非所有的这些统计都适合现代编程。不过有一点,如果你的子程序超过了200行,那你就要小心了。
子程序通常会有参数,如何组织这些参数也是门学问。下面是一些指导原则:
- 按照输入-可修改-输出的顺序排列参数,也可以考虑按照该排列规则对参数进行规范命名。
- 让所有子程序参数排列顺序保持一致。
- 使用所有参数。很遗憾,这是JS的先天缺陷,你需要更加小心。
- 把状态或者出错变量放到最后。
- 不要把子程序的参数用作工作变量,应该在子程序中使用局部变量。
calcDemo(inputVal){
inputVal = inputVal + currentAdder(inputVal)
// do something with inputVal
...
return inputVal
}
这样的代码虽然没有任何错误,但是容易造成误解,因为最后返回的inputVal已经不是最初传入的inputVal了,正确的做法是在函数内部使用局部变量指向inputVal然后返回该局部变量。这里是工程代码,不是在竞赛网站上,不能为了简洁而简洁,少写一行代码并不会给你加分。
- 在接口中对参数的假定加以说明。
- 限制子程序的参数个数。7是个很神奇的数字,让你的参数保持在七个以内。
- 为子程序传递用以维持其接口抽象的变量或对象。我在很多代码中发现,函数参数并不是一个个变量,而是一个对象,通过该对象来传递参数。
这是一个富有争议的问题。假如一个对象有10个属性,但是处理方法只用到了3个属性,那么直接传递对象就暴露了其他属性,这破坏了封装原则,增加了代码耦合。另一种观点则认为传递整个对象能使子程序更加灵活,使接口更加稳定易于扩展。
那到底何时传变量,何时传对象呢?作者认为关键在于子程序的接口想要表达何种抽象。如果要表达的抽象是子程序期望的特定数据,那么应该直接传数据,如果要表达的抽象是想拥有某个特定对象,就应该传对象。
比如,你发现在调用子程序之前都要先创建一个对象,调用完后又从对象中取出这些数据,那说明你需要的是数据而非对象。如果你发现自己经常需要修改子程序的参数表,而每次修改的参数都来自同一个对象,那说明你需要的是整个对象。
说完参数,最后来说说返回值。如果把函数按语义划分,可以分为“函数”和“过程”,”函数”有返回值,而“过程”返回void或者没有返回值。什么时候使用”函数“,什么时候使用”过程”,其实通过函数名就应该能确定下来。比如xxx.next()和xxx.fire(),前者一看就是”函数“,而后者是”过程“。
如果你使用”函数“,肯定会存在返回错误返回值的风险,尤其是当函数内有多条分支时。为减小这一风险,请确保:
- 检查所有可能的返回路径
- 不要返回指向局部数据的引用或者指针
防御式编程
防御式编程的核心其实就是容错。当子程序遭遇到各种非法输入数据时也能工作。对于这些非法数据,通常有三种方式来处理:
- 检查所有来源于外部的数据。文件,用户,网络等接口的数据都属于外部数据,这些都是不安全的。
- 检查子程序所有的输入参数。子程序的输入数据来源于其它子程序,这里做检查是为了防止程序内部产生了非预期的数据。
- 决定如何处理错误的输入数据。根据项目需求,你可以返回错误码,记录日志,返回一个默认的合法值或返回与前次相同的数据,具体方案视需求而定。
第一点和第二点都是数据校验,第三点是对校验结果的处理方式。一切错误都来自于输入输出。理论上对于所有外部数据都要进行校验,因为这些数据都是不可靠不确定的,需要通过一个”过滤系统”将其过滤成确定类型的数据。这个”过滤系统”就是隔栏(barricade)。在隔栏的外面应该使用错误处理技术,在内部应该使用断言。因为隔栏内部的数据都是被清理过的,如果在内部出错那应该是程序的错误而非数据的错误。
还有一种容错方式叫异常。异常是把代码中得错误或异常事件传递给调用方代码的一种特殊手段。异常跟断言的使用情景相似,都是用来处理那些罕见或者永远不应该发生得情况。书中给出了使用异常的一些建议:
- 用异常通知程序的其他部分,进行错误消息传递。
- 只有在其他编码方式无法解决的情况下才使用异常。
- 不要把本可在局部处理的错误当成一个未捕获的异常抛出去。
- 避免使用空得catch语句,这是一种不负责任的写法。
- 了解所有函数库可能抛出的异常。
- 建立一套几种的异常处理机制。
- 考虑异常的替换方案,确保你的程序是真的需要处理异常。
过度的防御式编程会使程序变得臃肿缓慢,增加软件的复杂度,变得难以维护。所以在进行编码时呀考虑好什么地方需要防御,然后调整优先级,因地制宜。
系列文章
- Code Complete — 编程之前
- Code Complete — 创建高质量的代码
- Code Complete — 代码改善
- Code Complete — 软件工艺(未发布)