Clean Code 之命名

在软件开发过程(Where)中,程序员(Who)随时随地都有可能在命名,命名之事,无处不在。在编程(When)中,常见的命名范围:项目名、封包名、类名、方法名、变量名、参数名。

本文结合《Clean Code》有意义的命名章节和《Alibaba Java 开发手册》部分内容。从以下三个方面分析命名之道:

  1. What:什么是好的命名
  2. Why:为什么需要好的命名
  3. How:如何实现优雅命名

什么是好的命名

简单来说,好的命名就是,当别人看到你的代码时,可以清晰明确了解代码本身含义,不要额外付出去询问或查找每个命名的具体含义。

好的命名能体现出代码的特征,含义或者是用途,让阅读者可以根据名称的含义快速理清程序的脉络。

无论是命名和注解,目的都是为了让代码和工程师进行对话,增强代码的可读性,可维护性。优雅的代码往往都见名知意。

《Clean Code》这本书明确指出:

好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。

若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。

优雅一词在代码领域流传甚久。

优雅的命名即是注释,别人一看到你的命名就知道你的变量、方法或者类是做什么的!

优雅一词,含义在于:外表或举止上令人愉悦的优美和雅观;令人愉悦的精致和简单。

其中,愉悦格外重要,显然优雅的代码也是令人阅读起来十分愉悦的。

因此,好的命名必然是让每一位读者都赏心悦目,怀着愉悦心情阅读的。

为什么需要好的命名

那么,为什么需要好的命名呢,难道只是为了让读者赏心悦目吗?

不仅仅如此。

借用《Java编程语言代码规范》一段开场白:

一个软件需要花费80%的生命周期成本去维护。   
几乎没有任何软件的整个生命周期仅由其原作者来维护。   
编码规范改善软件的可读性,让工程师更快更彻底地理解新的代码。   
如果你将源代码转变为一个产品,那么您需要确保它和你创建的其它产品一样是干净且包装良好的。

所以说,好的命名,其可读性,可维护性就很高。

接下来,引入以下几个示例了解为什么。

示例一

public List<int[]> getThem() {
  List<int[]> list1 = new ArrayList<int[]>();
  for(int[] x : theList)
    if (x[0] == 4)
      list1.add(x);
  return list1;
}

看完上例代码,是否有种看到初学编码时代的自己所写代码的感触。

根据这段代码,我们唯一能了解到的信息可能就是这是一段获取列表数据的代码。具体是什么,无法从代码明确。

如果需要明确此代码的具体含义,加上大量注释才可完成此目的。

那么,看完这段代码,你是否有很多问号?

  • getThem到底获取了什么数据?
  • theList代表什么?
  • list1返回后用于做什么?
  • 为什么theList元素等于4就被添加至list1?

短短的几行代码便反应出这么些问题。

如果我们使用好的命名方式来修改代码,可以得到以下代码:

public List<Cell> getFlaggedCells() {
  List<Cell> flaggedCells = new ArrayList<int[]>();
  for(Cell cell: gameBoard)
    if (cell.isFlagged())
      flaggedCells.add(cell);
  return flaggedCells;
}

看到这部分代码,是否使你心情愉悦了呢?

简单的修改了命名,我们便能从代码理解其深层含义。回答上例几个问题:

  • 此方法获取了被标记的单元格列表数据。
  • 之前的theList代表gameBoard,游戏地图的单元列表。
  • 之前的list1存放的是被标记的单元格。
  • 4则代表此单元格处于被标记状态。

由此,我们便可知道,为什么。赋予好的命名可以提高代码可读性,便于理解交流。

好的命名可以使代码变得更加“优雅”。

示例二

public static void copyChars(char a1[], char a2[]) {
  for (int i = 0; i < a1.length; i++) {
    a2[i] = a1[i];
  }
}

根据这段代码,我们已经可以明确其目的在于复制字符数组。

但读者依然会有疑惑,对于a1、a2这类数字系列命名,其不显示具体含义,且外形极其相似。

如果不深入了解,未必明白a1、a2的编码者所传递的意图。

所以,我们可以修改参数名,以体现有意义的区分。

public static void copyChars(char source[], char destination[]) {
  for (int i = 0; i < a1.length; i++) {
    destination[i] = source[i];
  }
}

只是修改了参数名,其就很好的区分了两参数区别,使阅读代码的人能迅速鉴别不同之处。

示例三

for (int j = 0; j < 34; j++){
  sum += (taskEstimate[j] * 4) / 5;
}

看到这段代码,可以明确其在遍历使用公式求和,但是如果并不明确其中具体算法,依然会处于懵逼树下懵逼果的状态。

并且,即使知道了数字常量所代表的含义,若要搜索其使用位置,也是十分困难的。

在《重构》中,将此方法中出现的常量数字称为Code Smell中的Magic Number,即魔法数字,代表无法理解其具体含义的数字。

因此,采用给常量赋予具体含义的命名可以解决此类问题。如下所示:

int realDaysPerIdealDay = 4;
final int WORK_DAYS_PER_WEEKS = 5;
final int NUMBER_OF_TASKS = 34;
for (int j = 0; j < NUMBER_OF_TASKS; j++){
  int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
  int realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEKS;
  sum += realTaskWeeks;
}

对每个常量赋予真实意义的名字后,对于此代码理解更加容易。

测估完成所有任务的周数的算法。

既提高了代码可读性,又便于在任何位置搜索相关常量值。

示例四

public class AbsClass {
  boolen condi;
  void fu(String pa){
    if(condi){
      ...
    }
    ....
  }
}

此代码中,随意使用简写,导致各命名含义模糊,无法理解。

即使使用简写模式,也应当使用大家有共识,都认可的方式才好。

如将简写命名改成全称,如下:

public class AbstractClass {
  boolen condition;
  void function(String param){
    if(condition){
      ...
    }
    ....
  }
}

对比即可发现,全称命名使代码意义直线上升,清晰明了。

好的命名不应当为了方便而随意使用简写。杜绝完全不规范的缩写,避免望文不知义。

示例五

final int MAX_COUNT = 10;
final long EXPiRED_TIME = 1000l;

此例中常量命名全部大写,且long类型以l结尾。

但其中常量命名为了缩短名称长度。使得直知道是最大数量、过期时间,并不知其属于什么类型、领域。并且long类型以“l”结尾,“l”与1不易区分。

final int MAX_STOCK_COUNT = 10;
final long CACHE_EXPiRED_TIME = 1000L;

修改以后,代码更为清晰,确定了其中具体含义。因此好的命名要力求语义表达完整清楚,不要嫌名字长。

并且若不采用L结尾,很有可能误以为TIME值为10001。

...

根据以上介绍,相信每位读者心中都有一个答案,为什么要用好的命名。

不好的命名会增大代码理解难度,降低可读性,可维护性,从而导致开发人员费神费力,效率低下。

简单而言:好的命名可以让读者赏心悦目,怀着愉悦心情接受所有代码。优雅的命名可以提高代码可读性、区分性、可维护性等优点。

如何实现优雅命名

很多人可能有了疑问,到底如何可以实现优雅命名呢?

本文将推荐以下几点:

  1. 类名和对象名应当是名词或名词短语,避免动词或意义模糊的词语。

  2. 方法名应当是动词或动词短语。

  3. 类名使用UpperCamelCase风格。方法名、参数名、变量名统一使用lowerCamelCase风格。常量名全部大写,单词用下划线隔开。

  4. 包名统一小写,点分隔符之间有且只有一个自然语义单词。

  5. 抽象类命名使用Abstract或Base开头,异常类命名使用Exception结尾,测试类命名以Test结尾。枚举类名带上 Enum 后缀。

  6. 选择体现本意的命名以让人更容易理解和修改代码,命名简单直接,不要用俗语。避免中英混用,以及歧视性词语。

  7. 避免使用与本意相悖的词,避免留下掩藏代码本意的错误线索。在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。例如:使用list命名非List结构的数据。

    eg. Map<int, User> UserList;
            Map<int, User> UserMap;
  8. 提防外形相似度较高的名称。eg. O和0,l和1

  9. 对命名做有意义的区分,以让读者准确鉴别不同之处。相同含义的名字不要重复出现,会导致混淆。

  10. 长名称优于短名称,搜得到的名称优于自造编码的名称。只要能描述清楚含义,尽量使用完整词组表达命名。单字母名称仅用于短方法的本地变量。名字长短应与其作用于带下相对应。

  11. 避免使用编码,已无需使用m_前缀来标明成员变量,已无意义。

  12. 避免出现同一个概念具有多个命名,应当统一为每个抽象概念选一个词,并一以贯之。

  13. 杜绝完全不规范的缩写,避免望文不知义。但可以使用领域专有术语。DO/VO/DTO/PO/POJO/UID等。如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。eg.工厂模式:public class OrderFactory;

  14. 单纯的命名无法解释含义,可引入语境前缀,多个字段具有相同语境前缀,可采用类封装,并给予合适命名。但不要在已具有语境的类中添加冗余的前缀。只要短名称即可描述清楚,则无需长名称描述。

  15. 分离解决方案领域和问题领域的概念,与问题领域更贴近代码应采用源自问题领域的名称。

Reference

  1. 《Clean Code》

  2. 《Java 开发手册》:https://github.com/alibaba/p3c/blob/master/Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E5%B5%A9%E5%B1%B1%E7%89%88%EF%BC%89.pdf

  3. 智能命名工具codeIf:https://unbug.github.io/codelf/

  4. DO、BO、DTO、VO、AO、PO、UID 名词意义:https://zhuanlan.zhihu.com/p/105390453