阅读《代码整洁之道》有两种原因,第一种:你是个程序员;第二,你想成为更好的程序员。
读完后,你能获得如下技能:
- 知道更多关于代码的事儿
- 辨别好代码和糟糕代码的差异
- 学会如何写出好代码
- 学会如何将糟糕代码改成好代码
Later equals never. 有些事儿现在不做以后都不会做了。
程序员基础价值谜题
以前混乱的代码拖了自己后腿,但开发者背负着期限的压力,只好继续制造混乱。而制作混乱无助于赶上期限。
赶上期限的唯一办法:始终保持代码的整洁。
写整洁代码就像是绘画。多数人知道一幅画是好是坏,但能分辨优劣并不表示懂得绘画,能分辨代码优劣的人也不意味着会写整洁代码。
写整洁代码需要遵循大量的小技巧,贯彻刻苦习得的“整洁感”。这种“代码感”就是关键所在。
什么是整洁代码
整洁的代码只做好一件事。简洁,优雅。
每个函数、每个类、每个模块都全神贯注于一事,不受四周细节的干扰和污染。
有意义的命名
变量、函数、参数、类、包、文件。有很多地方需要命名。怎么命名才能简洁明了?
1. 名副其实
说起来简单,但这是一个很严肃的问题。选个好名字要花时间,但省下来的时间比花掉的多。一旦有好的命名,就换掉旧的。
- 如果名称需要注释来补充,那么说明名字没取好。(不是名副其实)
// 差的命名
int d; // 消逝的时间,以日计算。
// 好的命名
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
- 选择体现本意的名称能让人更容易理解和修改代码。
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList) {
if (x[0] == 4) {
list1.add(x);
}
}
return list1;
}
上面代码虽然简洁,但是我们能说清楚它到底做了啥吗? 问题不在于代码的简洁度,而是在于代码的模糊度:即上下文在代码中未被明确体现的程度。上面代码要求我们了解类似一下问题的答案:
- theList中是什么类型的东西
- theList零下标条目的意义是什么
- 值4的意义是什么
- 我怎么使用返回的列表
问题的答案没体现在代码段中,而这本就是它们应该在的地方。比如,我们正在开发一款扫雷游戏,我们发现盘面是名为theList的单元格列表,那就将其名称改为gameBoard。
盘面上每个单元格都用一个简单数组表示。我们还发现,零下标条目是一种状态的值,而这种状态值为4表示为“已标记”。只要改为有意义的名称,代码就会得到相当程度的改进。
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<>();
for (int[] cell : gameBoard) {
if (cell[STAUS_VALUE] == FLAGGED) {
flaggedCells.add(cell);
}
}
return flaggedCells;
}
还可以更进一步,不用int数组表示单元格,而是另写一个类。该类包括一个名副其实的函数(称为isFlagged),从而掩盖住那个魔术数(就是那个4)。于是得到函数的新版本:
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<>();
for (Cell cell : gameBoard) {
if (cell.isFlagged()) {
flaggedCells.add(cell);
}
}
return flaggedCells;
}
只要简单改一下名字,就能轻易知道发生了什么。这就是选好名字的力量
2. 避免误导
程序员必须避免留下掩盖代码本意的错误线索。应当避免使用与本意相悖的词。例如,别用accountList来指称一组账号,除非它真的是List类型(即便容器就是一个List,最好也别在名称中写出容器类型名)。如果包含账号的容器并非真是个List,就会引起错误的判断。所以,用accountGroup或bunchOfAccounts,甚至直接用accounts都会好一些。
提防使用不同之处较小的名称。相区分模块中某处的XYZControllerForEfficientHandlongOfStrings
和另一处的XYZControllerForEfficientStorageOfStrings
,会花多长时间呢? 这两个词外形实在是太像了。
误导性名称真正可怕的例子是使用小写的字母l和大写的字母O作为变量名,尤其是在组合使用的时候。问题在于,它俩看起来完全像是数字1和0。
3. 做有意义的区分
数字系列和废话名称,可以满足编译器,但是远远不够。
public static void copyChars(char[] a1, char[] a2) {
for (int i = 0; i < a1.length; i++) {
a2[i] = a1[1];
}
}
以数字系列命名(a1, a2, ......aN)是依义命名的对立面。这样的名称纯属误导——完全没有提供正确的信息,没有提供导向作者意图的线索。
如果将参数名改为source和destination,这个函数将会像样很多。
废话是另一种没有意义的区分。假设你有一个Product类。如果还有一个ProductInfo或ProductData类,那它们的名称虽然不同,意思却没区别。Info和Data就像a、an和the一样,是意义含混的废话。
如果缺少明确约定,变量moneyAmout就与money没区别,customerInfo与customer没区别,accountData与account没区别,theMessage也与message没区别。要区分名称,就要以读者能鉴别不同之处的方式来区分。
4. 使用读的出来的名称
能读出来的名称更容易记忆。如果名称读不出来,讨论的时候就像个傻鸟。
Date genymdhms // 生成日期,年、月、日、时、分、秒
Date generationTimestamp;
5. 使用可搜索的名称
找MAX_CLASSES_PER_STUDENT
比找数字7要容易得多。同样字母e也不是个便于搜索的变量名。因为太常见了。
单字母名称仅用于短方法中的局部变量。名称长短应与其作用域大小相对应。如果变量或常量可能在代码中多处使用,则应赋予其以便于搜索的名字。
6. 避免将类型或作用域编进名称中
- 匈牙利语标记法(Hungarian Notation,HN)
早期编译器不做类型检查,程序员需要用匈牙利语标记法(Hungarian Notation,HN)来帮助自己记住类型。
在Windows的C语言API的时代,HN非常重要,传说HN是为了纪念具有传奇色彩的微软程序员Charles Simonyi。这种标记法比较简单:即变量名以表明该变量数据类型的小写字母开始。
例如szCmdLine的前缀sz代表string end of zero.以0 结尾的字符串。
strPhone,代表Phone是字符串类型。
而今,大部分语言是强类型的,代码编辑环境都已经先进到在编译开始前就侦测到类型错误的程度!所以HN和其他类似的格式编码都多余了。
它们增加了修改变量,函数,或类的名称或类型的难度。
PhoneNumber phoneString;
//类型变化时,名称并未变化。
- 成员前缀
也不必用 m_ 前缀来标明成员变量。应当把类和函数做得足够小,消除对成员前缀的需要。
- 接口和实现
接口前导字母I被滥用。
7. 类名应该是名词或名词短语,方法名应该是动词或者动词短语
类名如Customer、WikiPage、Account。避免使用Manager、Processor、Data或Info这样的类名
方法名如postPayment、deletePage或save。属性访问器、修改器或断言应该根据其值命名,并依Javabean标准加上get、set和is前缀。
8. 同一概念对应统一个词
比如get、fetch、retrieve表达的含义差不多,尽量保持多个类使用同一种写法。
Controller、Manager、Driver含义相近,尽量保持一致。
9. 不用双关语
10. 使用解决方案领域名称
代码是给程序员读的,用计算机领域类的术语来命名是一个很好的做法。比如AccountVisitor、JobQueue这种。
如果不能用程序员熟悉的术语来命名,就采用所涉问题的领域名称命名。
11. 添加有意义的语境、不要添加没用的语境
如果你有命名良好的类、函数或命名空间来放置名称,给读者提供语境是最好不过了。如果没有,就用最后一招———给名称添加前缀。
比如,你有名为firstName、lastName、street、hourseNumber、city、state变量,搁一块儿的时候,很明确是一个地址。如果只是在一个方法中单独的看到一个state变量呢,你会理所当然得推断是一个地址吗?
可以添加前缀addrFirstName、addrLastName、addrState来提供语境。
假设有一个名为“加油站豪华版”(Gas Station Deluxe)的应用,在其中给每个类添加GSD前缀就不是什么好点子了。