关于改善代码质量的编程规范

4,145 阅读14分钟

根据《设计模式之美》中部分章节,整理记录下的改善代码质量的编程规范。内容主要分为三个部分:命名与注释、代码风格、编程技巧。文中代码示例使用了Java作为示例,但并不限于Java,这些规范对于其他语言也是可以通用作为参考。

书写代码的质量非常依赖个人的开发经验,而下面总结出的这些规范,大部分简单明了,在阅读并掌握后,即使对于经验略少的开发者,在编写代码时对质量的提升也能够起到立竿见影的效果。

命名

1. 命名长度

实际开发中会遇到两种相对极端的命名方式,一种为了准确达意,命名尽可能的长,导致项目里类名、函数名都很长。另一种命名尽可能的短,能用缩写就用缩写,甚至字母。那么究竟那种方式更为合理?

我们要知道命名的原则是准确达意。理论上在足够表达其含义的基础上,命名越短越好。而在实际开发中,在遇到作用域较大的变量,为了减少可能产生的影响,更推荐用长的命名方式。

缩短命名的小建议,对于一些默认的、大家都比较熟悉的词可以使用缩写,可以让命名更短又不影响阅读理解。比如,sec 表示 second、str 表示 string、num 表示 number。

对于自己来说,对代码的逻辑很清楚,总感觉用什么样的命名都可以达意,实际上,对于不熟悉你代码的同事来讲,可能就不这么认为了。所以,命名的时候,一定要学会换位思考,假设自己不熟悉这块代码,从代码阅读者的角度去考量命名是否足够直观。

2. 利用上下文简化命名

public class User {
  private String name; //not userName
  private String password; //not userPassword
  private String avatarUrl; //not userAvatarUrl
  //...
}

类似情况下,借助上下文即可表意明确。同时也便于日后做一些抽象的操作。

3. 命名要可读、可搜索

命名可读指的是不要用一些特别生僻、难发音的英文单词或者不常见的缩写来命名。虽然我们不排斥一些独特的命名方式,但是起码得让大部分人看一眼就能知道怎么读。不影响沟通交流,这就算是一个比较好的命名。

命名可搜索指的是命名时最好能符合项目整体的命名习惯,便于在IDE中编写代码时联想补全以及搜索。比如大家都用“selectXXX”表示查询,你就不要用“queryXXX”或者“searchXXX”;大家都用“insertXXX”表示插入一条数据,你就不要用“addXXX”。统一规约是很重要的,能减少很多不必要的麻烦。

4. 如何命名接口和抽象类

接口有两种命名方式:一种是在接口中带前缀“l”;另一种是在接口的实现类中带后缀“lmpl”。对于抽象类的命名,也有两种方式,一种是带上前缀“abstract”,一种是不带前缀。这两种命名方式都可以,关键是要在项目中统一。

注释

注释跟命名同等重要。很多书籍认为,好的命名完全可以替代注释,如果需要注释,那说明命名不够好,需要在命名上下功夫,而不是添加注释。实际上个人觉得,这样的观点有点太过极端。命名再好,毕竟有长度限制,不可能足够详尽,这个时候,注释就是一个很好的补充。

1. 注释到底该写什么?

注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。

注释的内容主要包含这样三个方面:做什么、为什么、怎么做。

有些人认为,注释是要提供一些代码没有的额外信息,所以不要写“做什么、怎么做”,这两方面在代码中都可以体现出来,只需要写清楚“为什么”,表明代码的设计意图即可。

对于以上观点并不是特别认可,还是要分情况来处理,理由主要有以下3点:

  • 注释比代码承载的信息更多

    命名主要目的是解释“做什么”,对于函数和变量来说如果命名的好,确实可以不用在注释中再解释它是做什么的。但是对于类来说,包含的信息比较多,一个简单的命名就不够全面详尽了。这个时候,在注释中写明“做什么”就合情合理了。

  • 注释起到总结性作用、文档的作用

    通过阅读代码可以明确知道代码是“怎么做”的,也就是知道代码是如何实现的,那注释中是不是就不用写“怎么做”了?实际上也可以写。在注释中,关于具体的代码实现思路,我们可以写一些总结性的说明、特殊情况的说明。这样能够让阅读代码的人通过注释就能大概了解代码的实现思路,阅读起来就会更加容易。

    实际开发中,对于有些比较复杂的类或者接口,我们可能还需要在注释中写清楚“如何用”,举一些简单的 quick start 的例子,让使用者在不阅读代码的情况下,快速知道该如何使用。

  • 一些总结性注释能让代码结构更清晰

    对于逻辑比较复杂的代码或者比较长的函数,如果不好提炼、拆分成小的函数调用,那我们可以借助总结性注释来让代码结构更清晰、更有调理。

    public boolean isValidPasword (String password) {
      // check if password is null or empty
      if (StringUtils.isBLank (password)) { 
        return false;
      }
    
      // check if the Length of password is between 4 and 64 
      int Length = password.Length();
      if (Length < 4 || Length > 64) { 
        return false;
      }
    
      // check if password contains only Lowercase characters 
      if (!StringUtils.isAllLowerCase(password)) { 
        return false;
      )
    
      // check if password contains only a-z,d-9, dot
      for (int i = 0; i < length; ++i) 
        char c = password.charAt(i);
        if (!(c >= 'a' && c <= "z")||(c >='0'&& c<= '9')||c=='.'){
           return false;
        }
      }
      return true;
    }
    

2. 注释是不是越多越好?

注释太多和太少都有问题。

注释太多,有可能意味着代码写的不够可读,需要写很多注释来补充。除此之外,注释太多也会对代码本身的阅读起到干扰。而且,后期的维护成本也比较高,有时候代码改了,注释忘了同步修改,就会让代码阅读者更加迷惑。

如果代码中一行注释都没有,那只能说明这个程序员有点偷懒,我们要适当督促一下,让他注意添加一些必要的注释。

注释本身有一定的维护成本,所以并非越多越好,类和函数一定要写注释,而且要写的尽可能全面、详细,而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码的可读性。

其他

关于注释,应该使用英文还是中文来书写呢?工作中还是要根据团队情况,交付要求等因素来决定。个人开发的话根据自身英文掌握情况,毕竟注释的目的是要让代码更容易看懂,英文足够熟练,那直接英文即可。如果不够熟练可以试着中文注释+英文注释,本着提升英文水平目的同时兼顾注释的可读性。

代码风格

1. 类、函数多大才合适?

总体上来讲,类或函数的代码行数不能太多,但也不能太少。类活函数的代码行数太多,一个类上千行,一个函数几百行,逻辑过于繁杂,阅读代码的时候,很容易就会看了后面忘了前面。相反,类或者函数的代码行数太少,在代码总量相同的情况下,被分隔成的类和函数就会相应增多,调用关系就会变得更复杂,阅读某个代码逻辑的时候,需要频繁地在n多类或者函数之间跳来跳去,阅读体验也不好。

要给出一个精确的量化值很难。比如做饭,对于放少许盐,即便是大厨也很难告诉你一个特别具体的量。

对于函数代码行数的最大限制,网上有一种说法,就是不要超过一个显示屏的垂直高度,让一个函数的代码完整的显示在 IDE 中。这个说法还是比较有道理的。因为超过一屏之后,阅读代码时为了串联前后的代码逻辑,就可能需要频繁的上下滚动屏幕,阅读体验不好喝不说,还容易出错。

对于类的代码行数的最大限制,这个就更难给出一个确切的值了。类当中往往包含了若干功能,高度大部分情况下都过超出一屏的显示范围。这里只能给一个间接的判断标准。那就是当一个类的代码读起来让你感觉吃力了,实现某个功能不知道改用哪个函数了,想用哪个函数翻半天都找不到,只用到一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,就说明类的行数过多了。

2. 一行代码多长合适?

不同编程语言、不同规范、不同团队,对此的限制可能都不同。不管这个限制是多少,总体上我们尽量遵循一个原则:一行代码的长度不超过 IDE 显示的宽度。不论是需要鼠标滚动才能查看完整代码还是 IDE 自动折行,都或多或少会不利于代码的阅读。当然,这个限制也不能太小,太小会导致很多稍长点的语句被折成两行,影响代码整洁以至于影响代码阅读。

3. 善用空行分割单元块

对于比较长的函数,如果逻辑上可以分为几个独立的代码块,在不方便将这些独立的代码块抽取成小函数的情况下,为了让逻辑更加清晰,除了前面提到的注释,我们还可以用空行来分割各个代码块。

除此之外,在类的成员变量与函数之间、静态成员变量与普通成员变量之间、各函数之间、甚至各成员变量之间,我们都可以通过添加空行的方式,让这些不同模块的代码之间,界限更加明确。类似写文章,善于应用空行,可以让代码的整体结构看起来更加清晰有条理。

4. 四格缩进还是两格缩进?

这个还是取决于个人喜好,保证项目内部能够统一就行了。

还有一个选择的标准,就是跟业内推荐的风格统一、跟著名开源项目统一。

需要强调的是,不管是用几格缩进,一定不要用 tab 键缩进。因为在不同的 IDE 下,tab 键的显示宽度有可能会不同,虽然可以通过 IDE 设置去配置,但是为了避免意外的麻烦,最好还是能够养成不要使用 tab 缩进的习惯。

5. 大括号是否要另起一行

将大括号放到跟上一条语句同一行,可以节省代码行数。但是将大括号另起新的一行的方式,左右括号可以垂直对齐,哪些代码属于哪一个代码块,更加一目了然。

6. 类中成员的排列顺序

在 Google Java 编程规范中,依赖类按照字母顺序从小到大排列。类中先写成员变量后写函数。成员变量之间或函数之间,先写静态成员变量或函数,后写普通变量或函数,并且按照作用域大小依次排列。

代码风格小结

以上所有代码风格都没有对错和优劣之分,只要能在团队、项目中统一即可,不过最好能跟业内推荐的风格、开源项目的代码风格相一致。

编程技巧

1. 把代码分割成更小的单元块

打不分人阅读代码的习惯都是先看整体再看细节。所以,我们要有模块化和抽象思维,善于将大块的复杂逻辑提炼成类或者函数,屏蔽掉细节,让阅读代码的人不至于迷失在细节中,这样能极大的提高代码的可读性。不过,只有代码逻辑比较复杂的时候,我们其实才建议提炼类或者函数。毕竟如果提炼出的函数只包含两三行代码,在阅读代码的时候,还得跳过去看一下,这样反倒增加了阅读成本。

2. 避免函数参数过多

个人觉得,函数包含3、4个参数的时候还是能接受的,大于等于5个的时候,就觉得参数有点过多了,会影响到代码的可读性,使用起来也不方便。针对参数过多的情况,一般有2中处理方法。

  • 考虑函数是否职责单一,是否能通过拆分成多个函数的方式来减少参数。
  • 将函数的参数封装成对象。

除此之外,如果函数是对外暴露的远程接口,将参数封装成对象,还可以提高接口的兼容性。在往接口中添加新的参数时,老的远程接口调用者有可能就不需要修改代码来兼容新的接口了。

3. 不要用函数参数来控制逻辑

不要在函数中使用布尔类型的标识参数来控制内容部逻辑,true 的时候走这块逻辑,false的时候走另一块逻辑。这明显违背了单一职责原则和接口隔离原则。这种情况建议将其拆成两个函数,可读性上也要更好。

不过,如果函数是 private 私有函数,影响范围有限,或者拆分之后的两个函数经常同时被调用,我们可以酌情考虑保留标识参数。

4. 函数设计要职责单一

前面在说到单一职责原则的时候,针对的是类、模块这样的应用对象。实际上,对于函数的设计来说,更要满足单一职责原则。仙姑低于类和模块,函数的粒度比较小,代码行数少,所以在应用单一职责原则的时候,没有像应用到类或者模块那样模棱两可,能多单一就多单一。

5. 移除过深的嵌套层次

代码嵌套层次过深往往是因为 if-else、switch-case、for循环过度嵌套导致的。个人建议,嵌套最好不超过两层,超过两层后就要思考一下是否可以减少嵌套。过深的嵌套本身理解起来就比较费劲,初次之外,嵌套过深很容易因为代码多次索引,导致嵌套内部的语句超过一行的长度而折行,影响代码的整洁。

解决嵌套过深的方法也比较成熟,有下面4种常见的思路:

  • 去掉多余的 if 或 else 语句。
  • 使用编程语言提供的 continue、break、return 关键字,提前退出嵌套。
  • 调整执行顺序来减少嵌套。
  • 将部分嵌套逻辑封装成函数调用,以此来减少嵌套。

初次之外,常用的还有通过使用多态来替代 if-else、switch-case 条件判断的方法。

6. 学会使用解释性变量

常用的解释性变量来提高代码的可读性的情况有下面 2 种。

  • 常量取代魔法数字。(魔法数字即表意不明的数字。极大的影响代码可读性)
  • 使用解释性变量来解释复杂表达式。

统一代码规范

通过以上说到的命名、注释、代码风格、编程技巧这些知识点,可以发现大多数规范并没有一个准确的要求,只是建议或者推荐。最为重要的其实是,项目、团队乃至公司,应该制定统一的编码规范,并通过 Code Review 督促执行,这对提高代码质量有立竿见影的效果。