第2章 有意义的命名 Tim Ottinger
2.1 介绍
给变量、函数、参数、类和封包命名;给目录以及文件命名;开发中随处可见命名,我们要做好它
2.2 名副其实
- 贴切的命名需要花时间去想,但总体而言后面省下的时间会比花掉的更多
- 发现更好的命名,就换掉旧的
// 好的名称不需要注释来补充
// bad
int d; // 消逝的时间,以日计
// better
int elapsedTimeInDays;
// bad
// 虽然代码中规中矩并不复杂,但单凭这段代码不能明确体现程序的行为
// 1.theList是什么? 2.x的下标 0 是什么意思? 3.值4是什么意思? 4. list1是什么?
function getThem() {
const list1 = []
for(const x of theList) {
if(x[0] === 4) {
list1.push(x)
}
}
return list1
}
// better
// 场景说明:我们在开发一种扫雷游戏,theList的单元格列表,那就将其名称改为gameBoard;盘面上每个单元格(x)都用一个简单数组表示。零下标条目是一种状态值,而该种状态值为4表示“已标记”。
// 依据场景,我们将代码改为有意义的名称,代码变得明确很多!
const STATUS_VALUE = 0
const FLAGGED = 4
function getFlaggedCells() {
const flaggedCells = []
for(const cell of gameBoard) {
if(cell[STATUS_VALUE] === FLAGGED) {
flaggedCells.push(cell)
}
}
return flaggedCells
}
// good
// 不用int数组表示单元格,而是另写一个类。该类包括一个名副其实的函数(称为isFlagged),从而掩盖住那个魔术数(4)。于是得到函数的新版本:
function getFlaggedCells() {
const flaggedCells = []
for(const cell of gameBoard) {
if(cell.isFlagged()) {
flaggedCells.push(cell)
}
}
return flaggedCells
}
2.3 避免误导
程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词。
-
避免使用专有名词
// hp、aix和sco都不该用做变量名,因为它们都是UNIX平台或类UNIX平台的专有名称。 -
避免使用有歧义的名词
// 别用accountList来指称一组账号,除非它真的是List类型。List如果仅代表容器,用accountGroup或bunchOfAccounts,甚至直接用accounts都会好一些。 -
不要使用太相似的命名
// XYZControllerForEfficientHandlingOfStrings 和另一处的 XYZControllerForEfficientStorageOfStrings // 避免用小写字母l和大写字母O作为变量名,尤其在组合使用时。 let a = l; if (o === 1) { a = o1; } else { l = 01; }
2.4 做有意义的区分
-
写代码如果仅仅只是为了编译通过,能正常运行。那么这样的代码就会很麻烦。例如,在同一个作用域内,两个东西不能重名。于是为了编译通过,随手改掉一个名称,或以错误的拼写充数。这样做,不仅代码的可读性会逐渐变差,而且将来如果更正这个拼写会导致编译错误。
const a1 = {name: 'alex'} const a2 = {name: 'lucy'} -
不能明确区分
Product类、ProductInfo类、ProductData类,它们的名称虽然不同,意思却无区别。Info和Data是意义含混的废话。 getActiveAccount(); getActiveAccounts(); getActiveAccountInfo(); moneyAmount与money customerlnfo与customer accountData与account theMessage也与message -
不要冗余
Variable一词永远不应当出现在变量名中。Table一词永远不应当出现在表名中。NameString会比Name好吗?难道Name会是一个浮点数不成?如果是这样,就触犯了关于误导的规则。设想有个名为Customer的类,还有一个名为CustomerObject的类。区别何在呢?
2.5 使用读得出来的名称
// 反例 -> 正例(可以读出来的命名)
genymdhms(生成日期,年、月、日、时、分、秒) -> generationTimestamp
modymdhms(生成日期,年、月、日、时、分、秒) -> modificationTimestamp
2.6 使用可搜索的名称
MAX_CLASSES_PER_STUDENT // 检索容易
7 // 很难检索。单字母和数字常量很可能是其他名称的一部分
-
所以,长名称胜于短名称,搜得到的名称胜于用自造编码代写就的名称。
-
单字母名称仅用于短方法中的本地变量。名称长短应与其作用域大小相对应。若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。
2.7 避免使用编码
- 把类型或作用域编进名称里面,徒然增加了解码的负担。
- 每位新人都在弄清要应付的代码之外(那算是正常的),还要再搞懂另一种编码“语言”的负担。
- 带编码的名称通常也不便发音,容易打错。
2.7.1 匈牙利语标记法(Hungarian Notation, HN)
-
产生的历史原因
在Windows的C语言API的时代,HN相当重要,那时所有名称要么是个整数句柄,要么是个长指针或者void指针,要不然就是string的几种实现(有不同的用途和属性)之一。那时候编译器并不做类型检查,程序员需要匈牙利语标记法来帮助自己记住类型。
-
现在变得多余的原因
- 现代编程语言具有更丰富的类型系统,编译器也记得并强制使用类型
- 使用更小的类、更短的方法,好让每个变量的定义都在视野范围之内
- Java对象是强类型的,代码编辑环境已经先进到在编译开始前就侦测到类型错误的程度
- 增加了修改变量、函数或类的名称或类型的难度。
- 增加了阅读代码的难度
- 编码系统可能误导读者
2.7.2 成员前缀
-
不必用m_前缀来标明成员变量。应当把类和函数做得足够小,消除对成员前缀的需要。
-
使用某种可以高亮或用颜色标出成员的编辑环境。
class Part { constructor() { this.m_dsc = "" } setName(name) { this.m_dec = name } } //------------------------------------ class Part { constructor() { this.description = "" } setDescription(description) { this.description = description } }
2.7.3 接口和实现
不对接口进行编码。
ShapeFactory > ShapeFactorylmp > CShapeFactory > IShapeFactory
2.8 避免思维映射
聪明程序员有时会借脑筋急转弯炫耀其聪明。总而言之,假使你记得r代表不包含主机名和图式(scheme)的小写字母版url的话,那你真是太聪明了。
聪明程序员和专业程序员之间的区别在于,专业程序员了解,明确是王道,善用其能,编写其他人能理解的代码。
2.9 类名
类名和对象名应该是名词或名词短语
// good
Customer、WikiPage、Account和AddressParser
// bad-1 ???
Manager、Processor、Data或Info
// bad-2 使用动词
2.10 方法名
方法名应当是动词或动词短语,如postPayment、deletePage或save。
属性访问器、修改器和断言应该根据其值命名,并依Javabean标准加上get、set和is前缀。
2.11 别扮可爱
名称不要太耍宝。宁可明确,毋为好玩。
// bad
HolyHandGrenade (圣手手雷)
// good
DeleteItems
不要使用俚语,这类与文化紧密相关的笑话
// bad
whack() --美俚,劈砍
eatMyShorts() --美俚,去死吧
// good
kill()
abort()
2.12 每个概念对应一个词
给每个抽象概念选一个词,并且一以贯之。——避免同一概念用不同词
// bad-1
fetch、retrieve和get来给在多个类中的同种方法命名
// bad-2
代码中有controller,又有manager,还有driver且无根本区别
2.13 别用双关语
避免将同一单词用于不同目的。——避免同一词用于不同概念
// 比如在多个类里面都会有add方法,该方法通过增加或连接两个现存值来获得新值。
// 假设要写个新类,该类中有一个方法,把单个参数放到群集(collection)中。
// 把该方法命名为add,就是双关语了。
// 应该用insert或append之类词来命名才对。
2.14 使用解决方案领域名称
只有程序员才会读你的代码。所以,尽管用那些计算机科学(Computer Science, CS)术语、算法名、模式名、数学术语(技术性名称)。
依据问题所涉领域来命名(业务性名称)可不算是聪明的做法,因为不该让协作者老是跑去问客户每个名称的含义。
2.15 使用源自所涉问题领域的名称
如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域专家了。 优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题领域的概念。与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称。
2.16 添加有意义的语境
需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招了。
// 场景
设想你有名为firstName、lastName、street、houseNumber、city、state和zipcode的变量。当它们搁一块儿的时候,很明确是构成了一个地址。不过,假使只是在某个方法中看见孤零零一个state变量呢?你会理所当然推断那是某个地址的一部分吗?
// 稍微好点的方法
添加前缀addrFirstName、addrLastName、addrState等,以此提供语境。至少,读者会明白这些变量是某个更大结构的一部分。
// 更好的方法
创建名为Address的类。这样,即便是编译器也会知道这些变量隶属某个更大的概念了。
2.17 不要添加没用的语境
只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。
// 场景
开发名为“加油站豪华版”(Gas Station Deluxe)的应用,给每个类添加GSD前缀,例如创建了一个表示邮件地址的类,然后给该类命名为GSDAccountAddress。
// bad points
1. 不好检索,输入G结果会得到系统中全部类的列表
2. 命名过长
3. 命名无意义。在这17个字母里面,GSDAccountAddress有10个字母纯属多余和与当前语境毫无关联
// good
accountAddress和customerAddress都是不错的名称,不过用在类名上就不太好了。
Address是个好类名。
如果需要与MAC地址、端口地址和Web地址相区别,我会考虑使用PostalAddress、MAC和URI。这样的名称更为精确,而精确正是命名的要点。
2.18 最后的话
取好名字最难的地方在于需要良好的描述技巧和共有文化背景。这个领域内的许多人都没能学会做得很好。 我们有时会怕其他开发者反对重命名。如果讨论一下就知道,如果名称改得更好,那大家真的会感激你。
改名可能会让某人吃惊,就像你做到其他代码改善工作一样。别让这种事阻碍你的前进步伐。 不妨试试上面这些规则,看你的代码可读性是否有所提升。如果你是在维护别人写的代码,使用重构工具来解决问题。效果立竿见影,而且会持续下去。