Clean Code - 有意义的命名

程序媛和她的猫

共 2696字,需浏览 6分钟

 · 2022-03-19

最近生活工作趋于稳定,所以决定重新看一遍《代码整洁之道》,记录一些读书摘要,便于记忆。

在软件开发过程中,随处可见命名。我们给变量函数参数封包命名,我们给源代码源代码所在目录命名,我们给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,前三个字母纯属多余,何必呢?


浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报