理论五:让你最快速地改善代码质量的20条编程规范(上)

前面我们讲了很多设计原则,后面还会讲到很多设计模式,利用好它们可以有效地改善代码质量。但是,这些知识的合理应用非常依赖个人经验,用不好有时候会适得其反。而我们接下来要讲的编码规范正好相反。编码规范大部分都简单明了,在代码细节方面,能立竿见影地改善质量。除此之外,我们前面也讲到,持续低层次、小规模重构依赖的基本上都是编码规范,这也是改善代码可读性的有效手段。

关于编码规范、如何编写可读代码,很多书籍已经讲得很好了,我在前面的加餐中也推荐过几本经典书籍。不过,这里我根据我自己的开发经验,总结罗列了 20 条我个人觉得最好用的编码规范。掌握这 20 条编码规范,能你最快速地改善代码质量。因为内容比较多,所以,我分为三节课来讲解,分别介绍编码规范的三个部分:命名与注释(Naming and Comments)、代码风格(Code Style)和编程技巧(Coding Tips)。命名

命名

大到项目名、模块名、包名、对外暴露的接口,小到类名、函数名、变量名、参数名,只要是做开发,我们就逃不过“起名字”这一关。命名的好坏,对于代码的可读性来说非常重要,甚至可以说是起决定性作用的。除此之外,命名能力也体现了一个程序员的基本编程素养。这也是我把“命名”放到第一个来讲解的原因。

取一个特别合适的名字是一件非常有挑战的事情,即便是对母语是英语的程序员来说,也是如此。而对于我们这些英语非母语的程序员来说,想要起一个能准确达意的名字,更是难上加难了。

实际上,命名这件事说难也不难,关键还是看你重不重视,愿不愿意花时间。对于影响范围比较大的命名,比如包名、接口、类名,我们一定要反复斟酌、推敲。实在想不到好名字的时候,可以去 GitHub 上用相关的关键词联想搜索一下,看看类似的代码是怎么命名的。

  1. 命名多长最合适?

在过往的团队和项目中,我遇到过两种截然不同的同事。有一种同事特别喜欢用很长的命名方式,觉得命名一定要准确达意,哪怕长一点也没关系,所以,这类同事的项目里,类名、函数名都很长。另外一种同事喜欢用短的命名方式,能用缩写就尽量用缩写,所以,项目里到处都是包含各种缩写的命名。你觉得这两种命名方式,哪种更值得推荐呢?

在我看来,尽管长的命名可以包含更多的信息,更能准确直观地表达意图,但是,如果函数、变量的命名很长,那由它们组成的语句就会很长。在代码列长度有限制的情况下,就会经常出现一条语句被分割成两行的情况,这其实会影响代码可读性。

实际上,在足够表达其含义的情况下,命名当然是越短越好。但是,大部分情况下,短的命名都没有长的命名更能达意。所以,很多书籍或者文章都不推荐在命名时使用缩写。对于一些默认的、大家都比较熟知的词,我比较推荐用缩写。这样一方面能让命名短一些,另一方面又不影响阅读理解,比如,sec 表示 second、str 表示 string、num 表示 number、doc 表示 document。除此之外,对于作用域比较小的变量,我们可以使用相对短的命名,比如一些函数内的临时变量。相反,对于类名这种作用域比较大的,我更推荐用长的命名方式。

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

  1. 利用上下文简化命名

public class User {
  private String userName;
  private String userPassword;
  private String userAvatarUrl;
  //...
}

在 User 类这样一个上下文中,我们没有在成员变量的命名中重复添加“user”这样一个前缀单词,而是直接命名为 name、password、avatarUrl。在使用这些属性时候,我们能借助对象这样一个上下文,表意也足够明确。具体代码如下所示:


User user = new User();
user.getName(); // 借助user对象这个上下文

除了类之外,函数参数也可以借助函数这个上下文来简化命名。关于这一点,我举了下面这个例子,你一看就能明白,我就不多啰嗦了。


public void uploadUserAvatarImageToAliyun(String userAvatarImageUri);
//利用上下文简化为:
public void uploadUserAvatarImageToAliyun(String imageUri);
  1. 命名要可读、可搜索

首先,我们来看,什么是命名可读。先解释一下,我这里所说的“可读”,指的是不要用一些特别生僻、难发音的英文单词来命名。

过去我曾参加过两个项目,一个叫 plateaux,另一个叫 eyrie,从项目立项到结束,自始至终都没有几个人能叫对这两个项目的名字。在沟通的时候,每当有人提到这两个项目的名字的时候,都会尴尬地卡顿一下。虽然我们并不排斥一些独特的命名方式,但起码得让大部分人看一眼就能知道怎么读。比如,我在 Google 参与过的一个项目,名叫 inkstone,虽然你不一定知道它表示什么意思,但基本上都能读得上来,不影响沟通交流,这就算是一个比较好的项目命名。

我们再来讲一下命名可搜索。我们在 IDE 中编写代码的时候,经常会用“关键词联想”的方法来自动补全和搜索。比如,键入某个对象“.get”,希望 IDE 返回这个对象的所有 get 开头的方法。再比如,通过在 IDE 搜索框中输入“Array”,搜索 JDK 中数组相关的类。所以,我们在命名的时候,最好能符合整个项目的命名习惯。大家都用“selectXXX”表示查询,你就不要用“queryXXX”;大家都用“insertXXX”表示插入一条数据,你就要不用“addXXX”,统一规约是很重要的,能减少很多不必要的麻烦。

  1. 如何命名接口和抽象类?

对于接口的命名,一般有两种比较常见的方式。一种是加前缀“I”,表示一个 Interface。比如 IUserService,对应的实现类命名为 UserService。另一种是不加前缀,比如 UserService,对应的实现类加后缀“Impl”,比如 UserServiceImpl。

对于抽象类的命名,也有两种方式,一种是带上前缀“Abstract”,比如 AbstractConfiguration;另一种是不带前缀“Abstract”。实际上,对于接口和抽象类,选择哪种命名方式都是可以的,只要项目里能够统一就行。

注释

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

  1. 注释到底该写什么?

注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。总结一下,注释的内容主要包含这样三个方面:做什么、为什么、怎么做。我来举一个例子给你具体解释一下。


/**
* (what) Bean factory to create beans. 
* 
* (why) The class likes Spring IOC framework, but is more lightweight. 
*
* (how) Create objects from different sources sequentially:
* user specified object > SPI > configuration > default object.
*/
public class BeansFactory {
  // ...
}

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

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

命名的主要目的是解释“做什么”。比如,void increaseWalletAvailableBalance(BigDecimal amount) 表明这个函数用来增加钱包的可用余额,boolean isValidatedPassword 表明这个变量用来标识是否是合法密码。函数和变量如果命名得好,确实可以不用再在注释中解释它是做什么的。但是,对于类来说,包含的信息比较多,一个简单的命名就不够全面详尽了。这个时候,在注释中写明“做什么”就合情合理了。

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

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

实际上,对于有些比较复杂的类或者接口,我们可能还需要在注释中写清楚“如何用”,举一些简单的 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 a~z,0~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;
}
  1. 注释是不是越多越好?

注释太多和太少都有问题。太多,有可能意味着代码写得不够可读,需要写很多注释来补充。除此之外,注释太多也会对代码本身的阅读起到干扰。而且,后期的维护成本也比较高,有时候代码改了,注释忘了同步修改,就会让代码阅读者更加迷惑。当然,如果代码中一行注释都没有,那只能说明这个程序员很懒,我们要适当督促一下,让他注意添加一些必要的注释。

按照我的经验来说,类和函数一定要写注释,而且要写得尽可能全面、详细,而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码的可读性。

重点总结

  1. 关于命名
  • 命名的关键是能准确达意。对于不同作用域的命名,我们可以适当地选择不同的长度。作用域小的变量(比如临时变量),可以适当地选择短一些的命名方式。除此之外,命名中也可以使用一些耳熟能详的缩写。

  • 我们可以借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名。

  • 命名要可读、可搜索。不要使用生僻的、不好读的英文单词来命名。除此之外,命名要符合项目的统一规范,不要用些反直觉的命名。

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

  1. 关于注释
  • 注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。总结一下,注释的内容主要包含这样三个方面:做什么、为什么、怎么做。对于一些复杂的类和接口,我们可能还需要写明“如何用”。

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

There are only two hard things in Computer Science: cache invalidation and naming things.-- Phil Karlton

命名达意、准确:
不知道如何命名,推荐:Codelf(变量命名神器) https://unbug.github.io/codelf/
Search over projects from Github, Bitbucket, Google Code, Codeplex, Sourceforge, Fedora Project, GitLab to find real-world usage variable names.

关于注释语言:
公司的项目看项目要求(中英文都可以)
自己的个人项目一定要用英文,因为一开始我就考虑到要做国际化的项目(目标是全球用户)。
如何写注释可以多看看JDK源码中的注释,能够学到很多东西。

所谓无规矩不成方圆。同样我们在做一些事情的时候是有套路可寻的。在写代码的时候我们也需要遵循一些前人总结好的规范,使得自己写代码更具有可读性,可维护性。
今天从这篇文章中学到了关于命名和注释的一些见解。关于命名,以前自己会犯的一个错是将拼音和英文会混在一起用,现在想想估计太年轻,命名搞得不伦不类。现在已经改了,一直督促自己用英文。命名的原则是准确达意,在平时的开发中都我都是尽量使用完整的单词,即使这样命名会很长,但是能够清楚的表达。文章中说的借助上下文来简化属性、函数的命名自己没有实践过。
关于注释,在自己的开发习惯中对于类和方法都是要写注释的,但是自己对注释的理解比较浅,只是写了做什么。并没有从文中提到的“做什么、为什么、怎么做”这三个维度去考虑。对于一些非常简单的方法,如只有几行,一眼就能读懂其要表达的逻辑的,我一般不写注释。对于注释的维护的确是有一定的成本。某些时刻一个逻辑复杂的方法有变动,可能会忘记同步修改其对应的注释,就会导致之后的阅读理解有误导倾向。个人觉得类、方法的注释很重要,以前遇到自己不懂的,我都会到某度上去查,发现往往很多搜索结果解释的不够准确,常常会误导我。后面我几乎都是通过阅读官方文档来学习,如通过阅读JDK中JavaDoc的文档就能准确的理解某个函数能做什么,该怎么用。
关于注释该用英文还是中文这个问题,个人觉得用中文可能比较好,因为大家母语都是中文,你不能按照自己的标准来想别人也能读懂你写的英文。除非是做国际化项目以及所在团队能接受注释全用英文那自然更好。

在 User 类这样一个上下文中,我们没有在成员变量的命名中重复添加“user”这样一个前缀单词,而是直接命名为 name、password、avatarUrl。

但示例代码好像都带了 user 前缀?
作者回复: 这节课里的代码都没带吧

哎呀 说到注释 我之前还跟一个斯里兰卡来的同事吵了一架,真的面红耳赤,我都觉得我的眼珠子要瞪他给瞪出来了,如果是在武侠小说 估计打一架是在所难免的了。他的观点是所有的函数都加注释,而且还是那种 在函数上面 /**/ 的块里 @paramter name user's name, 这种,然后还要加上@author 他的名字。我的观点是 因为本身业务不是很复杂就是简单的CRUD, 每个函数都是特别短的那种,注释只是加在逻辑复杂的函数 还有就是一开始就知道这块后面要重构的地方加TODO注释,还是有一些确实很tricky的地方。 我这个观点主要是注释有维护成本,如果有注释,在修改了逻辑之后,需要把注释一并更新,但是有的人就会忘记更新,这样就跟让人迷惑。 最后组里的其他人都赞成有需要的时候加,后面看他提交了两次代码,都是每个函数 即使set, get上也加上了注释, 然后类文件的头部也写了满满的注释。 在Review 第二次他提交的代码的时候,发现他把上一个类文件头上的注释copy到新加的类文件上 连注释里面的类名都是之前那个文件的 原封不动的copy了过来。 我这暴脾气 当时真想去揍他一顿。

命名在golang惜墨如金的官方文档中也有很大的篇幅介绍,可见其重要性。语义清晰、尽量短、团队内部统一避免一词多名。

实践中的做法,项目名模块名虽然作用域很大,但是数量少、使用频率非常高,使用简写,在公司级别统一,并形成说明文档,公司内部统一语言和共识,项目的readme.md中也要自说明。
项目比较重要的概念,使用频率比较高的名词要统一命名,起名前告知强制投票参与。记入项目的readme。如CRUD用同一用create、read、update、delete优化IDE代码智能提示。
作用域大的类名、属性名、方法名长命名。短小的类中和函数类内局部变量可以端命名,不合理的也方便重构。
工具箱:
github上联想词协助起名,少用生僻词。
https://developer.github.com/v3

课后讨论
2.主要看团队中英语水平了,一般来说都是参差不齐,想要读懂,还是需要功力的;建议还是中文注释了

对于命名和注释说下自己的看法:
1.我的第一原则:见名知意。假如要给骆驼类命令,可以利用有道词典翻译成英语Camel
2.驼峰式命名,类首字母大写,函数首字母小写。
3.可以借鉴jpa中的命令法,比如:findUserById(),updateUserByStatus()
4.命名能不省略就不省略。

注释:1.开发时比较难理解的,用注释标识下,以免以后看到又忘了,毕竟自己写的代码,过了一个月,半年,你还能回忆起来多少呢?
2. 一些特有的逻辑,要注释标注。
3.非业务代码,也要尽量注释,以便同事复用。

原文地址:https://www.cnblogs.com/ukzq/p/14821336.html