Clean Code - 有意义的命名
共 2696字,需浏览 6分钟
·
2022-03-19 10:33
最近生活工作趋于稳定,所以决定重新看一遍《代码整洁之道》,记录一些读书摘要,便于记忆。
在软件开发过程中,随处可见命名。我们给变量、函数、参数、类和封包命名,我们给源代码及源代码所在目录命名,我们给jar文件、war文件和ear文件命名。
一个好的命名,能够显著提高代码的可阅读性,让人一看就知道这段代码是什么意思,下面列出了取个好名字的几条简单规则。
1、名副其实
一个好的名称,可以让我们看到这个变量、函数或类的时候,立马就能够知道它是干嘛的。
一个好的名称应该可以告诉你,这个变量、函数或类为什么会存在,做了什么事,应该怎么用。
如果名称需要注释来补充,那就不算是名副其实。
此外,如果一旦发现有更好的名称,就立马换掉旧的。
示例一
反例:
int d; // 消逝的时间,以日计
名称 d 什么也没说明,如果没有注释,都不知道是干啥的,就像你定义一个变量 i,没有注释,谁知道它是干啥的。d 这个名称并没有引起对时间消逝的感觉,跟别说以日计了。我们应该选择指明了计量对象和计量单位的名称。
正例:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
使用 elapsedTimeDays 代替上面的 d,elapsedTimee 指明了计量对象,即消逝的时间,InDays 指明了计量单位,以日计算。一看这个变量,就知道它是干嘛的,一目了然,根本不需要注释。
示例二
反例:
public List<int[]> getThem(){
List<int[]> list1 = new ArrayList<int[]>();
for(int[] x : theList)
if(x[0] == 4)
list1.add(x);
return list1;
}
上面这段代码,谁能看出来它要做什么事(我想哪怕是创作者本人,时间久了,他也不知道这段代码是要干嘛了)。
这段代码并不复杂,没有复杂的表达式,只有三个变量和两个常量,那为什么这么难懂呢?原因就是它太模糊了。
(1)theList 中是什么类型的东西?
(2)theList 零下标条目(x[0] 中的 0)的意义是什么?
(3)值 4 的意义是什么?
(4)我怎么使用返回的列表?
正例:
上面这段代码其实是一个扫雷游戏的一个功能点,即收集扫雷盘面上被标记的单元格,但是由于这四个模糊点,导致我们无法看懂这段代码到底要干嘛。下面我们就针对这四点,对这段代码做一下修改,使它的意图能够清晰的暴漏出来。
首先根据功能点的描述,将函数名修改为 getFlaggedCells,list1 是一个集合,里面装着被标记的单元格,我们将其改名为 flaggedCells。
扫雷盘面是名为 theList 的单元格列表,我们将其改名为 gameBoard,int 数组表示盘面上的每个单元格,我们将其改名为 cell。
零下标条目是一种状态值,我们创建一个常量 STATUS_VALUE,用这个常量代替魔法数 0。状态值 4 表示“已标记”,我们创建一个常量 FLAGGED,用这个常量代替魔法数 4。
经过上面对命名的修改,代码变得明确多了。
static final int STATUS_VALUE = 0;
static final int FLAGGED = 4;
public List<int[]> getFlaggedCells(){
List<int[]> flaggedCells = new ArrayList<int[]>();
for(int[] cell : gameBoard)
if(cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
对于上述代码,还可以更进一步优化,使其意义更加明确。我们不再用 int 数组表示单元格,而是另写一个类 Cell,Cell 包含一个函数 isFlagged,使用这个函数替换上面代码中的逻辑判断,可以使代码的意义更加明确。
public class Cell{
public Boolean isFlagged(){
List<int[]> flaggedCells = new ArrayList<int[]>();
for(Cell cell : gameBoard)
if(cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
}
2、避免误导
避免留下掩藏代码本意的错误线索;避免使用与本意相悖的词。我们举几个例子来说明一下。
示例一
即便容器就是个 List 或者 Map,最好也别在名称中写出容器类型名。
别用 accountList 来指称一组账号,除非它真的是 List 类型,因为 List、Map 等都是 Java 中容器的类型,有特殊的含义,如果包含账号的容器不是个 List,就会引起错误的判断。
所以用 accountGroup 或 bunchOfAccounts,甚至直接用 accounts 都会好一些。
示例二
hp、aix 和 sco 都不该用来做变量名,因为它们都是 UNIX 操作系统的专有名词。
当我们编写三角计算程序时,hp 看起来是个很好的缩写(hypotenuse,斜边),但是也不要使用。
示例三
提防使用不同之处较小的名称。
XYZControllerForEfficientHandlingOfStrings 和 XYZControllerForEfficientStorageOfStrings,这是两个很长的变量,除了中间 Handling 和 Storage 这很短的一部分不同外,其他完全相同,使用的时候需要看很长时间,才能区分出来,如果使用快捷键,很容易就弄错了。
示例四
不要使用小写字母 l 和大写字母 O 作为变量名,组合使用也不可以,原因在于它们看起来太像阿拉伯数字 1 和 0 了,很容易就搞混了。
3、做有意义的区分
对于不同含义的变量、函数或者类,在命名的时候,不光要能够被编译器区分出来,更应该让读者能够区分出来。我们举几个例子来说明一下。
示例一
通过添加数字来进行区分,这只能让编译器满意,但对于程序员来说,只会降低代码的可读性以及后期的可维护性。
public static void copyChars(char a1[], char a2[]){
for(int i = 0; i < a1.length; i++){
a2[i] = a1[i];
}
}
见上面这个例子,a1 和 a2 对于编译器来说,它可以分辨出来这是两个变量,但是这种用数字进行区分的方式,对于程序员来说,根本不知道这二者有何不同啊。
观察上面的代码,函数名是 copyChars,那么这个函数的功能就是实现字符的复制。既然是复制,就会有源参数和目标参数,所以我们可以将参数名改为 source 和 destination ,这样的话,这个函数的含义将会更加清晰。
public static void copyChars(char source[], char destination[]){
for(int i = 0; i < source.length; i++){
destination[i] = source[i];
}
}
示例二
废话是另一种没有意义的区分。举个例子,假设你有一个 Product 类,如果还有一个 ProductData 或者 ProductInfo 类,它们的名称虽然不同,意思却无区别。Data 和 Info,就像英文中的 a、an 和 the 一样,是意义含混的废话。
4、使用读得出来的名称
在命名的时候,不要用傻乎乎的自造词,最好使用恰当的英语单词。这样做,既可以提高代码的可读性,也可以提高团队内部的沟通效率。
反例:
public class DtaRcrd102{
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102";
/*...*/
}
正例:
public class Customer{
private Date generationTimestamp;
private Date modificationTimestamp;
private final String recordId = "102";
/*...*/
}
5、使用可搜索的名称
长名称胜于短名称,因为短名称很难在一大段代码中找出来。
示例一
找 MAX_CLASSES_PER_STUDENT 很容易,但找数字 7 就麻烦了,它可能是某些文件名或其他常量定义的一部分,出现在因不同意图而采用的表达式中。
示例二
e 也不是一个便于搜索的好变量名,它是英文中最常用的字母,在每个程序、每段代码中都可能出现。
示例三
反例:
for(int j = 0; j < 34; j++){
s += (t[j] * 4) / 5;
}
首先看一下反例所示的代码片段,它使用了一些很短且没有意义的名称(s、t),还有一些魔法数(34、4、5),拿到这段代码的人根本无法猜出这段代码要干啥。
正例:
int realDaysPerIdealDay = 4;
static final int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for(int j = 0; j < NUMBER_OF_TASKS; j++){
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEK;
sum += realTaskWeeks;
}
再看正例所示的代码片段,使用命名清晰的英文单词代替了这几个变量,使用长名称代替了这几个常量,通过这些修改,代码就像文章一样,能够读出它的含义,这才是一段好代码。
采用能表达意图的名称 WORK_DAYS_PER_WEEK 替换魔法数 5,貌似拉长了代码,但利远远大于弊,能更加清楚表达作者的意图。
6、避免使用编码
规则一:避免把类型和作用域编进名称里面。
反例:
String phoneNumberString;
正例:
String phoneNumber;
规则二:不必使用 m_ 前缀来标明成员变量。
根据多年的开发习惯,在读代码的时候,我们很容易无视前缀(或后缀),只看名称中有意义的部分,代码都地越多,眼中就越没有前缀,最终,前缀变成了不入眼的废料,变成了旧代码的标志物。
反例:
public class Part{
private String m_dsc;
void setName(String name){
m_dsc = name;
}
}
正例:
public class Part{
private String description;
void setDescription(String description){
this.description = description;
}
}
7、避免思维映射
不应当让读者在脑中把你的名称翻译为他们熟知的名称。
示例一:单字母变量名就是个问题,比如循环计数器避免使用字母 i、j、k,比如避免使用字母 a、b、c 进行命名。
专业程序员了解“明确是王道”,能够编写其他人能理解的代码。
8、类名和方法名
规则一:类名和对象名应该是名词或名词短语。
举例,应该使用 Customer、WikiPage、Account 和 AddressParser 这样的类名,避免使用 Manager、Processor、Data 或 Info 这样的类名。
规则二:类名不应当是动词。
规则三:方法名应当是动词或动词短语。
9、每个概念对应一个词
举个栗子,fetch、retrieve 和 get 都有获取的意思,如果代码中有多处需要使用获取这个词,最好都使用其中的一个,而不是一个地方用这个,一个地方用另一个,这不是一个很规范的代码。
反例:
public String getName(){
/*...*/
}
public String fetchAddress(){
/*...*/
}
正例:
public String getName(){
/*...*/
}
public String getAddress(){
/*...*/
}
10、别用双关语
避免将同一单词用于不同的目的。
反例:
public List addXXX(String name) {
names.add(name);
return names;
}
public int addXXX(int x, int y){
int z = x + y;
return z;
}
见反例程序,第一个 add 方法的功能是将某个元素添加到集合中,第二个 add 方法的功能是实现两个数值的相加。同一个单词有了两种不同的含义,这就是双关语,双关语很容易使人混淆,当我们读这段代码的时候,看到两个 add ,很可能认为二者都是一样的意思。
正例:
public List insert(String name) {
names.add(name);
return names;
}
public int add(int x, int y){
int z = x + y;
return z;
}
见正例程序,我们把第一个 add 方法改成了 insert(或者append),这样代码一下子就简洁明了起来,我们一看代码就知道,insert 是添加的意思,add 是相加的意思,这才是易于理解的代码嘛。
11、使用解决方案领域名称
记住,只有程序员才会读你的代码,所以,尽管用那些计算机科学术语、算法名、模式名、数学术语吧。
举个栗子,如果使用的是访问者(Vistor)模式,名称 AccountVistor 富有意义,如果创建的是一个队列,使用 JobQueue 命名,则会更加清晰。
12、使用源自所涉问题领域的名称
如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。
13、添加有意义的语境
很少有名称是能自我说明的--多数都不能。反之,如果你需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境。
举个栗子,假设你有名为 firstName、lastName、street(街道)、houseNumber(门牌号)、city(城市)、state(州)和 zipcode(邮政编码)这几个变量,当它们搁在一块儿的时候,很明确是构成一个地址。如果只是在某个地方看见孤零零一个 state 变量,你还能理所当然地推断出那是某个地址的一个部分嘛,毕竟 state 经常被我们用来描述状态。
那应该怎么办呢?就像上面说的,哪怕在某处只看到一个 state 变量,也能很快猜出它是某个地址的一部分呢?有两个方法,描述如下。
一是添加前缀,比如 addrFirstName、addrLastName、addrState,以此来提供语境。当我们哪怕只看一个 addrState 变量,根据前缀 addr 马上就可以知道它是隶属于某个地址的一部分。
二是将这些变量封装到一个 JavaBean 中。比如上面那些变量,我们可以创建一个 Address 类,然后将这些变量作为它的属性。
public class Address{
private String firstName;
private String lastName;
private String street;
private String houseNumber;
private String city;
private String state;
private String zipcode;
}
14、不要添加没用的语境
只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。
举个栗子,假设有一个名为“加油站”(Gas Station)的应用,如果在其中给每个类都添加 GSD 前缀就没有必要了,有两个弊端,如下所述。
一是不利于搜索,当你想要搜索某个包含 G 的类(或者变量、方法)时,你输入 G,按下自动完成键,结果会得到系统中全部类的列表,这样搞得 IDEA 的搜索功能也失效了呀。
二是代码冗余,假设有一个地址类,命名为 Address 就已经很清晰了,如果你偏偏要命名为 GASAddress,前三个字母纯属多余,何必呢?