《码出高效》系列笔记(二):代码风格

向代码致敬,寻找你的第[83]行。

Posted by MatthewHan on 2019-09-09

良好的编码风格和完善统一的规约是最高效的方式。

前言

本篇汲取了本书中较为精华的知识要点和实践经验加上读者整理,作为本系列里的第二篇章:代码风格篇。

本系列目录

命名规约

代码风格一般不会影响程序运行,通常与数据结构、逻辑表达无关。往往指代不可见字符的展示方式、代码元素的命名方式和代码注释风格等,但是却会隐藏潜在风险。虽然编码习惯不存在明显优劣之分,但是在团队开发效率上也许会是一个巨大的内耗,牺牲小我,成就大我,提升效能也许来的更关键。
但是我们都是独一无二有灵魂有思想的个体,不是clone出来的机器人,难免在理解和习惯有所偏差,如何去统一部分习惯呢?这本《码出高效》最早就是以规约而出名,本人也遵循了本书的中的大部分规则用于日常的开发中,书中有很多点因为阿里大量业务经验的存在而比我们考虑规范周全得多,所以直接采用本书的一些“规定”往往会便捷的多。

命名符合本语言特性

每种语言都有自己独特的命名风格,有些语言在定义时提倡以前缀来区分局部变量、全局变量、控件类型。比如li_count表示local int局部整型变量,dw_report表示data window用于暂时报表数据的控件,有些语言规定以下划线为前缀来进行命名。在Java中,所有代码元素的命名均不能以下划线或美元符号开始或结束

命名体现代码元素特征

  1. 类名采用大驼峰形式(UpperCamelCase),一般为名词,例如:Object、StringBuffer、FileInputStream等。
  2. 方法名采用小驼峰形式(lowerCamelCase),一般为动词,与参数组成动宾结构,例如Object的wait()、StringBuffer的append(String)、FileInputStream的read()等。
  3. 变量包括参数、成员变量、局部变量等,也采用小驼峰形式。
  4. 常量的命名方式比较特殊,除了局部常量外字母全部大写,单词之间用下划线连接。

在Java命名时,以下列方式体现元素特征:

  • 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英文单词。包名统一使用单数形式,但是类名如果有复数含义,则可以使用复数形式。
  • 抽象类命名是用Abstract或Base开头;异常类明明采用Exception结尾;测试类命名以它要测试的类名开始,以Test结尾。
  • 类型与中括号紧挨着相连来定义数组,例如:String[] args
  • 枚举类名带上Enum后缀,枚举成员名称参考常量的命名方式。

命名最好望文知义

从名称上就能理解某个词句的确切含义是坠吼滴,带到自解释的目的。

  • 所以要避免不规范的缩写,比如condition缩写成condi、consumer缩写成cons,类似随意的缩写会严重降低代码的可理解性。
  • 避免中文拼音、中英混合的方式,比如DaZePromotion(打折促销类)、PfmxBuilder(评分模型抽闲工厂类)。alibaba、baidu、taobao这类国际通用的名称,视为英文。

常量

作为在作用域内保持不变的值,一般用final关键字进行修饰,根据作用域划分成:全局常量、类内常量、局部常量。

  1. 全局常量:指类的公开静态属性,使用public static final修饰。
  2. 类内常量:私有静态属性,使用private static final修饰,
  3. 局部常量分为方法常量和参数常量,前者是在方法或代码内定义的常量,后者是定义形式参数是,增加final标识,表示此参数值不能被修改。
1
2
3
4
5
6
7
8
9
10
11
12
public class EasyCoding {
public static final String GLOBAL_CONSTANT = "shared in global";
public static final String CLASS_CONSTANT = "shared in class";

public void f(String a) {
final String methodConstant = "shared in method";
}
public void g(final int b) {
// 编译出错,不允许对常量参数进行重新赋值
b = 3;
}
}

常量在代码中具有穿透性,使用甚广,所以必须是一个恰当的命名并且保证长期使用。常量是为了干掉一些可能会在迭代中改变的魔法值,比如某业务中,12345五种代表着课程的审核状态,在团队规模小时口口相传加上注释可以保证不出错,但是在业务扩展的过程中会变得更加复杂(课程的等级状态也有12345五种,很容易混淆),则需要一套枚举类和全局常量类提高可读性来管理这些状态。

书中认为,系统成长到某个阶段后,重构是一种必然选择。优秀的架构设计不是去阻止未来一切重构的可能性,毕竟技术栈、业务方向和规模都在不断变化,二是尽量让重构来得晚一些,幅度小一些。

变量

广义来说,在程序中变量是一切通过分配内存并赋值的量,分为不可变量(常量)和可变变量。
变量命名需要满足小驼峰形式,体现业务含义即可。重点强调:在POJO类中,针对布尔类型的变量,命名不可以加is前缀。例如ORM映射关系中,数据库is_deleted字段,在类中不可以这样声明Boolean isDeleted;,因为getter方法也是isDeleted(),因为框架反向解析时会误以为对应的属性是deleted,导致获取不到属性,进而抛出异常。我们可以通过中添加下映射就vans辣!

代码展示风格

缩进、空格和空行

像Python这种没有大括号的语言,对缩进的使用非常严格,Java虽然没有这么严格,但是井然有序的风格让review变得更加高效。

  • 缩进:缩进表示层次对应关系,由于不同编辑器对Tab的解析不一致,而空格在编辑器中往往是兼容的,所以一般规定采用4个空格作为默认的缩进方式,当然现在IDE这么智能,早就不需要手打4个空格了,可以再Tab键和空格之间实现快速转换。其中IDEA设置Tab键为4个空格时,请勿勾选Use tab character;而在eclipse中,必须勾选Inset spaces for tabs。高版本的IDEA好像已经是默认4个空格代替tab缩进。
    IDEA中的配置

  • 空格:空格用于分隔不同的的编程元素,空格使得各元素之间错落有致,方便定位。一般有如下规定:

    1. 任何二目、三目运算符的左右两边都必须加一个空格。
    2. 注释的双斜线与注释内容之间有且仅有一个空格。
    3. 方法参数在定义和传入时,多个参数逗号后面必须加空格。
    4. 没有必要增加若干空格使变量的赋值等号遇上一行对应位置的等号对齐。
    5. 如果是大括号内为空,那么简洁的写成{}即可,大括号中间无需换行和空格。
    6. 左右小括号与括号内部的相邻字符之间不要出现空格。
    7. 左大括号前需要加空格。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class EasyCoding {
/**
* 不需要等号的位置一致
*/
public static String one = "1";
public static String two = "2";
public static Long three = 2L;
public static String weChat = "weChat";

public static void main(String[] args) {
// 缩进4个空格,并且在try关键字与左大括号之间保留一个空格
try {
// 二目运算符的左右必须有一个空格
int count = 0;

// 三目运算符的左右两边必须有一个空格,小括号相邻字符无需空格。
boolean condition = (count == 1) ? true : false;
int num = (count == 0) ? 99 : -1;

/**
* if关键字与小括号之间保留一个空格
* 小括号与大括号之间保留一个空格
* 建议使用IDE的自动补全
*/
if (condition) {
System.out.println("996");

/**
* if-else后无论逻辑复杂与否,都需要加上大括号,并且之间保留一个空格
* else不用换行
*/
} else {
System.out.println("965");
}
// 多个实参逗号后面必须有一个空格
String fuckTheWorld = getStr(one, two);
System.out.println(fuckTheWorld);

// catch体是不应该出现空内容的,但是这里为了讲解需要。{}中无需换行和空格。
} catch (Exception e) {}
}

/**
* 多个形参,逗号后面保留一个空格
* @param one
* @param two
* @return
*/
private static String getStr(String one, String two) {
// 任何二目运算符的左右必须有一个空格,包括赋值运算符,加号运算符。
return one + two;
}
}
  • 空行:空行用来分隔功能相似、逻辑内聚、意思相近的代码片段,是的代码布局更加清晰。一般在如下地方可以添加空行:
    1. 方法定义
    2. 属性定义结束
    3. 不同逻辑
    4. 不同于一
    5. 不同业务

换行与高度

  • 换行:单行字符不超过120个,超过必须要换行,换行遵循以下原则:
    1. 第二行相对第一行缩进4个空格,从第三行开始,不再继续缩进,参考示例
    2. 运算符与下文一起换行
    3. 方法调用的点符号与下文一起换行
    4. 方法调用中的多个参数需要换行时,在逗号后面换行
    5. 在括号前不要换行
1
2
3
4
5
StringBuffer sb = new StringBuffer();
sb.append("我").append("要")...
.append("正")...
.append("能")...
.append("量");
  • 方法行数限制:
    方法是执行单位,也是阅读代码逻辑的最高粒度模块。代码逻辑要分为主次、个性与共性,抽取次要逻辑作为独立方法,共性逻辑抽取陈共性方法(日期、参数校验、权限判断)。
    约定单个方法的总行数不超过80行。

控制语句

控制语句遵循如下约定:

  • 在if、else、for、while、do-while等语句中必须使用大括号。即使只有一行代码,也要加上大括号。
  • 在条件表达式中不允许有赋值操作,也不允许在判断表达式中出现复杂的逻辑组合。
1
2
3
4
// 反例:如下。
if ((file.open(fileName, "w") != null) && (...) || (...)) {
...
}
1
2
3
4
5
// 正例:而是应该赋值给一个布尔变量。
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
...
}
  • 多层嵌套不要超过3层。还记得太吾绘卷的一个if-else走天下吗?如果确实比较复杂的判断逻辑,可以采用卫语句、策略模式、状态模式来实现。其中卫语句即代码逻辑先考虑失败、异常、中断、退出等直接返回的情况,以方法多个出口的方式,解决代码中判断 分支嵌套的问题,这是逆向思维的体现。
1
2
3
4
5
6
7
8
9
10
11
12
13
public void func {
if (condition1) {
...
}
if (condition2) {
...
}
if (condition3) {
...
}
...
return;
}
  • 避免采用取反逻辑运算符。判断x是否小于1,应该采用if (x < 1)而不是if (!(x >= 1))

代码注释

Javadoc规范

类、类属性和类方法的注释必须遵循Javadoc规范,使用文档注释(/* \/)的格式。规范编写的注释,可以生成规范的JavaAPI文档,为外部用户提供有效支持。IDE也会自动提示所用到的类、方法的注释。

  1. 枚举类十分特殊,他的代码极为稳定。(我以前就有这个疑问,枚举类的一般的description属性加上name已经可以描述该枚举,为什么还要再加上注释。但是枚举类和全局常量一样,穿透性极强,而且在定义前就要深思熟虑,因为影响较大,所以注释是必须。)
  2. 注释的内容不仅限于解释属性值的含义,还可以包括注意事项、业务逻辑。(修改代码,可以加上修改和创建时间。)
  3. 枚举类的删除或者修改都存在很大的风险。(一般需要标注为过时属性,不可直接删除。)

简单注释

包括单行和多行注释,特别强调此类注释不可写在代码后方,必须写在代码上方。双划线的注释与注释内容保留一个空格。