前言:本篇章内容是对书籍《代码整洁之道》的整理与总结,便于我们写代码时随时将规范牢记心中。千万别说下次一定,因为稍后等于不 (Later equals never)。

同时,我会对其中一些内容做一些修改和删减,如果表述或思想有误,欢迎在博客下方留言指出。

一. 整洁的代码

为什么要写整洁且规范的代码?对于一门语言或框架的初学者来说,写的模块并不复杂,代码之间耦合度并不高,因此并不重视代码规范。

但是对于一个要逐渐更新迭代的项目来说,不规范的代码是令人绝望的。当你后期想要维护代码时,你会有点无从下手,甚至一度想要 重构 这个项目,那么为何当初不多花点时间在代码整洁这件事情上?

建议大家都去看看这本书,它在多个方面阐述了如何写整洁的代码,其代码示例基于Java。如果你学习的语言不是Java,也应该去看看或游览完本篇博客,因为整洁代码的思想是通用的。

最后我想说:写代码是一门艺术,当你回过头来看当初写的整洁代码,沁人心脾。

二. 命名规范

2.1 名副其实

选好名字,最好让人一看到名字就懂是什么意思,如果发现更好的命名要及时替换旧的

  • 坏代码

    1
    int d;	// 消逝的时间
  • 好代码

    1
    int elapsedTimeInDays;	// 消逝的时间

2.2 避免误导

避免使用与本意相悖的词,谨慎使用不同之处较小的名称(因为两个名字难以辨别),不使用小写字母 l 和大写字母 O 作为变量名

  • 坏代码

    1
    2
    3
    4
    5
    6
    7
    // 其本意并不是List,但是使用了List名称
    String accountList = "[{"user":"admin"},{"user":"superadmin"}]";

    // 不要使用字母 l 和 O 作为变量名,容易和数字0和1混淆
    int a = l;
    if(O == l) a = Ol;
    else l = 0l;

2.3 做有意义的区分

当类或变量重名需要更改时,不要为了区分而随意改个名字。

举例:

Product类和ProductData类虽然名称不同,但是意思没区别。

Variable不应该出现在变量名中,Table不应该出现在表名中。

nameString对于name命名来说就是废话,因为name一般都是字符串类型,足以区分。

2.4 使用读的出来的名称

不要使用单字母缩写、拼音进行命名,应规范使用英文单词。

  • 坏代码

    1
    2
    3
    4
    // 生成日期:年月日时分秒
    Date genymdhms;
    // 选课时间
    Date xksj;
  • 好代码

    1
    2
    Date generationTimeStamp;
    Date selectCourseTime;

2.5 使用可搜索名称

合理使用长名称替代常量,使用易于搜索的名字。

  • 坏代码

    1
    2
    // 一周的工资
    int salary = 120*5;
  • 好代码

    1
    2
    3
    4
    5
    // 一天的工资
    const int SALARY_PER_DAY = 120;
    // 一周工作几天
    const int WORK_DAYS_PER_WEEK = 5;
    int salaryForWeek = SALARY_PER_DAY * WORK_DAYS_PER_WEEK;

2.6 避免映射思维

有些特例可以不遵循前面几点的命名方式。例如:

在作用域较小且无冲突情况下,循环计数器可能被命名为 i , j, k

2.7 类名

类名和对象名应该是 名词名词短语

类名示例:Customer / Account / WikiPage

2.8 方法名

方法名应该是 动词 或者 动词短语

方法名示例:postPayment / deletePage

2.9 别花里胡哨

不要使用俚语、地方文化特殊含义词语来命名。

  • 坏代码

    1
    2
    // 搞快点
    int gkd = 0;

2.10 每个概念对应一个词语

抽象概念应该选出一个词,并且在项目中一直使用。

例如:get / select / fetch 的含义都一致,如果决定使用 get 作为获取的含义,那么就不要使用另外的单词来表示获取。

2.11 不要使用双关语

将同一单词或术语用于不同概念,就是双关语。

例如 add 在多个类中方法代表着 两个值相加 的含义,而有个类的 add 方法是 将单个参数存入集合中,这样看似很规整,但是他们的含义并不同,这就变成了双关语。

2.12 优先使用解决方案领域名称

只有程序员才会阅读你写的代码,所以 优先使用计算机科学的相关术语 进行命名。

如果实在无法命名,就可以考虑使用 所涉问题领域的专业名称

2.13 使用有意义的语境

使用 良好命名的类、函数或名称空间 来放置名称,给读者提供语境,实在没辙可以给名称加 前缀

  • 坏代码

    1
    2
    3
    String firstName;
    String lastName;
    String city;
  • 好代码

    1
    2
    3
    4
    5
    // 提供这是作为地址的语境
    String addrFirstName;
    String addrLastName;
    String addrCity;
    // 或者建立一个Address类时,就能提供这种语境,则无需给变量添加前缀 addr

三. 函数规范

3.1 尽量短小

函数的行数不要太多。

阿里巴巴的《Java开发手册》中建议,单个方法的总行数 不超过80行

函数的缩进层级不该多于一层或两层。

3.2 只做一件事

要判断函数是否不止做了一件事,就是看能否再拆出一个函数。

3.3 每个函数一个抽象层级

如果一个方法同时存在:

getHtml()等位于较高抽象层的方法

PathParser.render(pagePath) 等位于中间抽象层的方法

.append("\\n")等位于相当低的抽象层的方法

则往往会让人迷惑。

这条规则不好理解也不好遵守,你可以理解为在同一个方法中不要同时进行 细粒度粗粒度 的操作。

3.4 使用描述性的名称

长而具有描述性的名称,要比短而令人费解的名称好。命名方式要保持一致,使用与模块名一脉相承的短语、名词和动词给函数命名。

3.5 函数参数

  • 参数数量

    函数的参数数量越少越好,实在迫不得已再往上加参数。因为参数多了以后,其组合方式会越来越多,不易于调试。

  • 一元函数的普遍形式

    第一种:有输入值、返回值的函数,例如转换函数

    第二种:有输入值、无返回值的函数,我们称之为一个 事件,用以改变系统状态

    注意:要 避免将转换函数写成一个事件,因为这会很奇怪。

  • 标识参数

    不要向函数中传入布尔值(标识参数)来标记函数该做什么,因为这样违反了3.2章节提到的:函数只做一件事!

  • 二元函数

    你可能经常会弄混 assertEquals(expected, actual) 中两个参数的位置,所以应尽量将二元函数转换为一元函数。

    举例:将writeField(outputStream, name) 方法转换为 outputStream.writeField(name),这样调用起来是不是更舒服?

    当然了,尽量根据项目实际情况来吧。

  • 参数对象

    如果函数需要三个或三个以上的参数,可以考虑将其中一些参数分装为类了。

    坏代码

    1
    double calCubeVolume(double x, double y, double z);

    好代码

    1
    2
    // 创建一个Cube的类,包含x, y ,z三个成员变量
    double calCubeVolume(Cube cube);
  • 动词与关键字

    给函数取个好名字,解释函数的意图,以及参数的顺序。

    例如,我们可以将 assertEqual 改成 assertExpectedEqualsActual,这样我们一眼就能看出参数的顺序,不易混淆。

3.6 不要有副作用

不要做与函数名无关的事,避免使用输出参数,因为可能会带来副作用。

  • 坏代码(与函数名无关的事)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public boolean checkPassword(String username, String password){
    User user = userService.getByName(username);
    if(user != null && user.getPassword.equals(password)){
    // 根据函数名,并未说明会执行 initialize 操作
    Session.initialize();
    return true;
    }
    return false;
    }

    如果有开发者只想检查用户密码,并不想初始化session,就极有可能造成误用上述函数。因此,函数名要清晰展示出功能。

    可以将上述函数名改为 checkPasswordAndInitializeSession

  • 坏代码(输出参数)

    1
    2
    // 这是什么意思呢?将s加入到footer后面吗?
    appendFooter(s);

    这时我们就要花时间去检查函数声明

    1
    public void appendFooter(StringBuffer report)

    所以,我们要避免这种使用输出参数的情况,可以改为下列方法调用

    1
    report.appendFooter();

3.7 分割指令与询问

函数要么做什么事,要么回答什么事。当二者得兼时会造成混乱。

  • 坏代码

    1
    2
    3
    4
    // 函数声明
    public boolean set(String attribute, String value);
    // 函数设置某个属性,如果不存在那个属性则返回 false
    if(set("username", "banana"))...

    看上述代码,将 判断属性是否存在设置用户名 两个功能混淆在一起,那么这是在设置用户名前检查了属性,还是在设置用户名后检查属性呢?因此会造成语义不清,应该将 指令询问 分隔出来。

  • 好代码

    1
    2
    3
    4
    5
    // 询问
    if(attributeExists("username")){
    // 指令
    setAttribute("username", "banana");
    }

3.8 使用异常替代返回错误码

不要用 if-else 来判断错误后,return 错误码。应当使用异常返回错误码,这样就能将错误处理代码分离出来,简化代码。

  • 抽离 try / catch 代码块

    1
    2
    3
    4
    5
    6
    7
    8
    // 好代码示例
    public void delete(Page page){
    try{
    deletePageAndAllReferences(page);
    }catch(Exception e){
    logError(e);
    }
    }
  • 错误处理就是一件事

    处理错误的函数不应该做其他事情,即 try / catch / finally 代码块后不应该有其他内容。

3.9 不要重复

函数中尽量减少重复的代码段,避免冗余,因为这不利于后期修改与维护。

3.10 如何写出规范的函数

写代码如同写文章,先根据自己的思想写出来,再慢慢打磨。通过 分解函数、修改名称、消除重复 等方法,最后组装成一个让自己满意的、规范的函数。

四. 注释规范

就如本文的封面图一样,程序员最讨厌写注释,也最讨厌别人不写注释,这很矛盾不是吗?

4.1 注释不能美化烂代码

写注释的目的不应该为了掩饰这是一段垃圾代码,带有 少量注释整洁而有表达力 的代码,比有一堆注释的垃圾代码好得多!

4.2 用代码来阐述

尽量使用代码来描述其作用,当代码本身不足以解释其行为再加注释。

  • 烂代码

    1
    2
    3
    4
    // 检测用户是否实名认证且是否成年
    if(user.cert && user.age > 18){
    ...
    }
  • 好代码

    1
    2
    3
    if(user.isCertAndAdult){
    ...
    }

4.3 好注释

  • 法律信息

    版权及著作权声明要在每个源文件开头处注释,下面给出一个例子:

    1
    2
    // Copyright (C) 2020 by Object Mentor, Inc. All rights reserved.
    // Released under the terms of the GNU General Public License version 2 or later.
  • 提供信息的注释

    例如给某个方法的返回值做注释:

    1
    2
    // Returns an instance of the Responder being tested.
    protected abstract Responder responderInstance();
  • 对意图的注释

    可以用来描述此段代码的意图。

    1
    2
    3
    4
    // 打印输出0到5
    for(int i=0; i<=5; i++){
    System.out.println(i);
    }
  • 阐释

    你可以理解为将抽象的参数翻译成某种可读的形式。

    1
    2
    3
    assertTrue(a.compareTo(a) == 0);    // a == a
    assertTrue(a.compareTo(b) != 0); // a != b
    assertTrue(aa.compareTo(ab) == -1); // aa < ab

    不过这样会存在注释本身就不正确的情况,容易造成误导。

  • 警示

    可以用来警告其他程序员会出现某种后果。

  • TODO注释

    使用该注释来标记应该做,但目前尚未完成的工作。大多数好的IDE会给你列出来。

    1
    2
    3
    4
    // TODO 目前暂不需要这个方法,以后再加入
    public User getUserByPhone(){
    return null;
    }
  • 放大重要性

    可以通过注释来强调这段代码的重要性。

  • JavaDoc

    对于Java代码编程,可以使用标准Java库中的Javadoc来写注释。

    可以参考:https://www.runoob.com/java/java-documentation.html

4.4 坏注释

  • 自言自语

    1
    2
    3
    4
    5
    try{
    loadedProperties.load(propertiesStream);
    }catch(IOException e){
    // 没有异常说明一切正常
    }
  • 多余的注释

    1
    2
    3
    4
    5
    6
    // 如果用户密码不匹配,返回false,否则返回true
    if(user.getPassword != password){
    return false;
    }else{
    return true;
    }
  • 误导性注释

    不要写让别人误解函数作用的注释。

  • 循规式注释

    并不是每个函数都要有 Javadoc,或者每个变量都要有注释。这只会让代码变得更散乱。

  • 日志式注释

    有人喜欢在模块开始处写每次修改代码的变化日志,这种记录应当全部删除。(为什么不用Git来管理呢?)

  • 废话注释

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /** The name. */
    private String name;

    /**The password*/
    private String password;

    // 无参构造函数
    public User(){
    }
  • 位置标记

    不要在代码中为了标记一处特别位置,写令人奇怪的注释。

    1
    // 标记一下..........................
  • 括号后面的注释

    不要像下面那样给括号后面加注释,这么做可能就是你的函数太繁琐了。

    1
    2
    3
    4
    5
    6
    7
    8
    try{
    while((line = in.readLine()) != null){
    lineCount++;
    } // while
    } // try
    catch(Exception e){
    e.printStackTrace();
    } // catch
  • 归属与署名

    没必要为小小的签名而注释,为啥不用Git呢?

    1
    /* Added by BA_NANA */
  • 注释掉的代码

    建议直接删除被注释的代码。当被注释掉的代码堆积在一起,就像是玻璃瓶碎渣一样。

    1
    2
    int a = 5;
    // int b = a + 1;
  • 非本地信息

    写注释时要注意描述最近的代码,不要写上下文信息。

    1
    2
    3
    4
    5
    6
    /**
    * 端口号设置,默认8082端口
    */
    public void setPort(int port){
    this.port = port;
    }

    上面给出的函数中,我们可以看到 8082 端口并不属于本地信息,是一种上下文信息,这种注释会造成一个问题:如果后期修改了默认端口,注释要跟着改动。

  • 信息过多

    不要在注释里写一些历史性话题(如某个加密方法的原理),或者写一些无关的细节描述

  • 不明显的联系

    不要写与函数内容关系不大的注释,这样的注释让人难懂。

  • 函数头

    不需要给短函数太多描述,最好的方法是起个好的函数名。

未完待续