代码整洁之道——有意义的命名

谢谢《代码整洁之道》这本书,让我对Clean Code有了更深的理解,下面是我做的一些笔记

有意义的命名

下面是取个好名字的几条简单规则

名副其实

选个好名字要花时间,但省下的时间的时间比花掉的多,一旦发现有更好的名称,就换掉旧的。

  • 如果需要注释来补充,那就不算是名副其实。
1
int d; //消逝的时间,以日计
  • 命名中应当指明计量对象和计量单位
1
2
3
4
int elapsedTimeInDays;
int daySinceCreation;
int daySinceModification;
int fileAgeInDays;
  • 代码的模糊度要低
    代码的模糊度:上下文中未被明确体现的程度
    比方说下面的第一段代码
1
2
3
4
5
6
7
public List<int[]> getItem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x: theList)
if (x[0] == 4)
list1.add(x);
return list1;
}

读完之后,我们需要去了解以下问题的答案:

  1. theList中是什么类型的东西?
  2. theList零下标条目的意义是什么?
  3. 值4得意义是什么?
  4. 我怎么使用返回的列表?
    代码的问题没有体现在代码段当中,可那就是它们应该在的地方。
    比方说上述的代码所处情景是我们正在开发一种扫雷游戏,盘面是名为theList的单元格列表,那就将其名称改为gameBoard。
    盘面上每个单元格都用一个简单数组表示。零下标条目是一种状态值,而这种状态值为4代表“已标记”。只要改为有意义的名称,代码就得到了改进。
1
2
3
4
5
6
7
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数组来表示单元格,而是另写一个类。该类包括一个名副其实的函数(称为isFlagged),从而掩盖住哪个魔术数(4)。得到函数的新版本。

1
2
3
4
5
6
7
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}

只要简单改一下名称,就可以轻易地知道发生了什么,这就是简洁代码的威力。

避免误导

  1. 必须避免留下掩藏代码错误本意的错误线索,避免使用与本意相悖的词。(eg. List不应该出现在命名中,除非它真的是List类型,即便如此,最好也别在名称中写出类型名)
  2. 提防使用不同之处较小的名称,区分度要高,比方说(XYZControllerForEfficientHandlingOfStrings和XYZControllerForEfficientStorageOfStrings的区别能一眼看出来吗?)
  3. 以相同的方式拼写出同样的概念才是信息。拼写前后不一致就是误导。如果相似的名称以字母顺序(或其他相同的顺序)放在一起且差异很明显,那么就相当有助益,因为程序员大多不看仔细注释而是看名字直接挑一个对象。
  4. 误导性真正可怕的例子是用小写字母l和大写字母O作为变量名,尤其是组合在一起的时候。
1
2
3
4
5
int a = l;
if (O == l)
a = O1;
else
l = 01;

做有意义的区分

如果程序员只是为满足编译器或解释器而写代码,那么就会制造麻烦。比如说同一个作用范围内不能使用相同的名称,如果你随手改掉其中一个的名称,或者干脆以一个错误的拼写来充数(比如说class这个已经名称已经被占用,你就用klass来再命名,那么日后就会出现更正拼写错误后导致程序出错的情况,真是可怕)
当然,如果仅仅添加数字系列(a1,a2,a3,a4)或者废话也是远远不够的。
以数字系列来命名的名称纯属误导,因为这样不会提供任何正确信息,没有提供作者的任何意图。

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

如果把参数名改做source和destination,这个函数就会好很多

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

废话是一种没有意义的区分,假设你有一个Product类,还有一个ProductInfo或ProductData类,那么他们的后缀就无异于废话。
废话都是冗余。Variable永远不应该出现在变量名中,Table永远不应该出现在表明中,NameString难道要比Name好吗?难道Name会是浮点数??

1
2
3
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

怎么知道调用哪个函数呢?
如果缺少约定,那么moneyAccount与money没有区别,customerInfo与customer没区别,accountData与account没区别,theMessage也与Message没有区别。要区分名称,就要以读者能鉴别不同之处的方式来区分。

使用读得出来的名称

如果读不出来,讨论的时候就会像一个傻鸟(咸鱼)一样。
有家公司,程序里面就写了一个genymdhms(生成日期,年、月、日、时、分、秒),他们平常读作“gen why emm dee aich emm ess“(”gen-yah-mudda-hims“)······
比较一下

1
2
3
4
5
6
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102";
/* ··· */
}
1
2
3
4
5
6
class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;
private final String recordId = "102";
/* ··· */
}

使用可搜索的名称

如果常量是一个长数字,而且又被人错改过,就会逃过搜索,从而造成错误。(e不是一个便与搜索的变量名,它是最常用的字母),长名称优于短名称,搜索得到的名称优于用自造编码代写的名称。
单字母的变量仅用于短方法中的本地变量。名称长短应该与其其作用域大小相对应。

1
2
3
for (int j=0; j < 34; j++) {
s += (t[j]*4)/5;
}

1
2
3
4
5
6
7
8
int realDaysPerIdealDay = 4;
const 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 = (realdays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}

避免使用编码

编码已经太多,无需自找麻烦,这对于解决问题来说纯属多余的负担。
如今HN和其他类型编码形式都属多余,他们增加了修改变量、函数或类名的难度,制造了让编码系统误导读者的可能性。
不必使用m_前缀来表明成员变量,应该把类和函数做的足够小,消除对成员前缀的需要。
此外,人们会很快学会忽视前缀,读的代码越多,眼里就越没有前缀···
接口最好不要加修饰,如果接口和实现只能选择一个,宁可选择实现。

避免思维映射

不应当使读者在脑中把你的名称翻译成他们熟知的名称。
单字母名称就是一个问题,在循环计数器中使用单字母(i,j,k)是一个传统,这无可非议(l除外),但是读者必须在脑中映射真正的概念。
总之来说,明确才是王道。否则,你最多是一个聪明的程序员,而不能称为专业的程序员。

类名

类名和对象应该是名词或者名词短语,避免使用Manager、Processor、Data、Info这样的类名,类名不应该是动词。

方法名

方法名应该是动词或者动词短语,属性访问器、修改器和断言应该根据其值来命名,依照标准来加上get、set和is前缀。

1
2
3
string name = employee.getName();
customer.setName("Mike");
if (paycheck.isPosted()) ···

重载构造器时,使用描述了参数的静态工厂方法名。

1
Complex fulcrumPoint = Complex.FromRealNumber(23.0);

可以考虑将相应的构造器设置为private,强制使用这种命名手段。

别扮可爱

谁会知道HolyHandGrenade(圣手手雷)是用来干什么的呢?不如使用DeleteItems,宁可明确,毋宁好玩。
此外的例子还有使用whack()表示kill(),用eatMyShorts()表示abort(),这些都是不可取的。

一个概念对应一个词

给每一个抽象概念选一个词,并且一以贯之。
函数的名称应该独一无二,而且要保持一致。如果代码中有controller还有manager,driver就会让人困惑。

别用双关语

避免将同一个单词用作不同的用途,比如说把单个参数放到群集中应该使用insert/append而不是add
代码作者应该尽力写出易于理解的代码。我们想要大众化的作者尽职写清楚的平装书模式,而不是学者挖地三尺才能明白个中意义的学院派模式。

使用解决方案领域的名称

只有程序员才会读你的代码,所以尽管使用计算机术语、算法名、模式名、数学术语。

使用源自所涉及问题领域的名称

如果不能用程序员熟悉的术语来给手头的工作命名,就用所涉及的问题领域而来的名称吧。
优秀的程序员和设计师的工作之一就是分离解决方案和问题领域的概念。与所涉及领域更为贴近的代码,应当使用源自问题领域的名称。

添加有意义的语境

很少有名称是可以自我说明的,大多数都不能。所以你要有良好命名的类、函数或者名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void printGuessStatistcis(char candidate, int count) {
String number;
String verb;
String pluralModifier;
if (count == 0) {
number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1) {
number = "1";
verb = "is";
pluralModifier = "";
} else {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format(
"There %s %s %s%s", verb, pluralModifier, candidate, pluralModifier
);
print(guessMessage);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private class GuessStatistcisMessage {
private String number;
private String verb;
private String pluralModifier;
public String make(char candidate, int count) {
createPluralDepentMessageParts(count);
return String.format(
"There %s %s %s%s", verb, number, candidate, pluralModifier
);
}
private void createPluralDepentMessageParts(int count) {
if (count == 0) {
thereAreNoLetters();
} else if (count == 1) {
thereIsOneLetter();
} else {
thereAreManyLetters(count);
}
}
private void thereAreManyLetters(int count) {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
private void thereIsOneLetter(int count) {
number = "1";
verb = "is";
pluralModifier = "";
}
private void thereAreNoLetters(int count) {
number = "no";
verb = "are";
pluralModifier = "s";
}
}

不要添加没用的语境

如果有一个名为”加油站豪华版“(Gas Station Deluxe)的应用,你在每个类前面加GSD,那么你每次查找方法,按下G就会出现所有类的列表,恨不得有一英里长,为什么非要搞得IDE没法帮助你?
只要短名称足够清楚,就比长名称要好,别给名称添加不必要的语境
对于Adress类的实体来说,AccoutAddress和customerAddress都是不错的名称,不过用在类名上就不太好了,Address是一个好类名,如果需要与MAC地址、端口地址、Web地址相区别,考虑使用PostalAddress、MAC和URL、这样的名称更为精确。而精确正是命名的要点。

最后的话

取好名字最难的地方在于需要良好的描述和共有文化背景。
我们有时会害怕其他开发者反对重命名。但是如果讨论一下就知道,如果名称改得更好,大家真的会感激你。
试试以上方法,看看你代码的可读性是否会有所提升。

您的打赏是对我最大的鼓励!