代码的艺术与规范,写代码的人都应该看看!
前言:本篇章内容是对书籍《代码整洁之道》的整理与总结,便于我们写代码时随时将规范牢记心中。千万别说下次一定,因为稍后等于不 (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
2Date 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
3String 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
9public 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
3if(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
3assertTrue(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来写注释。
4.4 坏注释
-
自言自语
1
2
3
4
5try{
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
8try{
while((line = in.readLine()) != null){
lineCount++;
} // while
} // try
catch(Exception e){
e.printStackTrace();
} // catch -
归属与署名
没必要为小小的签名而注释,为啥不用Git呢?
1
/* Added by BA_NANA */
-
注释掉的代码
建议直接删除被注释的代码。当被注释掉的代码堆积在一起,就像是玻璃瓶碎渣一样。
1
2int a = 5;
// int b = a + 1; -
非本地信息
写注释时要注意描述最近的代码,不要写上下文信息。
1
2
3
4
5
6/**
* 端口号设置,默认8082端口
*/
public void setPort(int port){
this.port = port;
}上面给出的函数中,我们可以看到 8082 端口并不属于本地信息,是一种上下文信息,这种注释会造成一个问题:如果后期修改了默认端口,注释要跟着改动。
-
信息过多
不要在注释里写一些历史性话题(如某个加密方法的原理),或者写一些无关的细节描述
-
不明显的联系
不要写与函数内容关系不大的注释,这样的注释让人难懂。
-
函数头
不需要给短函数太多描述,最好的方法是起个好的函数名。
未完待续