Clean Code 之命名
Clean Code 之命名
在软件开发过程(Where)中,程序员(Who)随时随地都有可能在命名,命名之事,无处不在。在编程(When)中,常见的命名范围:项目名、封包名、类名、方法名、变量名、参数名。
本文结合《Clean Code》有意义的命名章节和《Alibaba Java 开发手册》部分内容。从以下三个方面分析命名之道:
- What:什么是好的命名
- Why:为什么需要好的命名
- 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。
...
根据以上介绍,相信每位读者心中都有一个答案,为什么要用好的命名。
不好的命名会增大代码理解难度,降低可读性,可维护性,从而导致开发人员费神费力,效率低下。
简单而言:好的命名可以让读者赏心悦目,怀着愉悦心情接受所有代码。优雅的命名可以提高代码可读性、区分性、可维护性等优点。
如何实现优雅命名
很多人可能有了疑问,到底如何可以实现优雅命名呢?
本文将推荐以下几点:
类名和对象名应当是名词或名词短语,避免动词或意义模糊的词语。
方法名应当是动词或动词短语。
类名使用UpperCamelCase风格。方法名、参数名、变量名统一使用lowerCamelCase风格。常量名全部大写,单词用下划线隔开。
包名统一小写,点分隔符之间有且只有一个自然语义单词。
抽象类命名使用Abstract或Base开头,异常类命名使用Exception结尾,测试类命名以Test结尾。枚举类名带上 Enum 后缀。
选择体现本意的命名以让人更容易理解和修改代码,命名简单直接,不要用俗语。避免中英混用,以及歧视性词语。
避免使用与本意相悖的词,避免留下掩藏代码本意的错误线索。在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。例如:使用list命名非List结构的数据。
eg. Map<int, User> UserList; Map<int, User> UserMap;
提防外形相似度较高的名称。eg. O和0,l和1
对命名做有意义的区分,以让读者准确鉴别不同之处。相同含义的名字不要重复出现,会导致混淆。
长名称优于短名称,搜得到的名称优于自造编码的名称。只要能描述清楚含义,尽量使用完整词组表达命名。单字母名称仅用于短方法的本地变量。名字长短应与其作用于带下相对应。
避免使用编码,已无需使用m_前缀来标明成员变量,已无意义。
避免出现同一个概念具有多个命名,应当统一为每个抽象概念选一个词,并一以贯之。
杜绝完全不规范的缩写,避免望文不知义。但可以使用领域专有术语。DO/VO/DTO/PO/POJO/UID等。如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。eg.工厂模式:public class OrderFactory;
单纯的命名无法解释含义,可引入语境前缀,多个字段具有相同语境前缀,可采用类封装,并给予合适命名。但不要在已具有语境的类中添加冗余的前缀。只要短名称即可描述清楚,则无需长名称描述。
分离解决方案领域和问题领域的概念,与问题领域更贴近代码应采用源自问题领域的名称。
Reference
《Clean Code》
智能命名工具codeIf:https://unbug.github.io/codelf/
DO、BO、DTO、VO、AO、PO、UID 名词意义:https://zhuanlan.zhihu.com/p/105390453