《代码整洁之道》—— 第2章 有意义的命名(读书笔记)

97 阅读9分钟

第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类,它们的名称虽然不同,意思却无区别。InfoData是意义含混的废话。
    
    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的几种实现(有不同的用途和属性)之一。那时候编译器并不做类型检查,程序员需要匈牙利语标记法来帮助自己记住类型。

  • 现在变得多余的原因

    1. 现代编程语言具有更丰富的类型系统,编译器也记得并强制使用类型
    2. 使用更小的类、更短的方法,好让每个变量的定义都在视野范围之内
    3. Java对象是强类型的,代码编辑环境已经先进到在编译开始前就侦测到类型错误的程度
    4. 增加了修改变量、函数或类的名称或类型的难度。
    5. 增加了阅读代码的难度
    6. 编码系统可能误导读者

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
CustomerWikiPageAccountAddressParser
// bad-1 ???
ManagerProcessorDataInfo
// 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个字母里面,GSDAccountAddress10个字母纯属多余和与当前语境毫无关联
// good
accountAddress和customerAddress都是不错的名称,不过用在类名上就不太好了。
Address是个好类名。
如果需要与MAC地址、端口地址和Web地址相区别,我会考虑使用PostalAddressMACURI。这样的名称更为精确,而精确正是命名的要点。

2.18 最后的话

取好名字最难的地方在于需要良好的描述技巧和共有文化背景。这个领域内的许多人都没能学会做得很好。 我们有时会怕其他开发者反对重命名。如果讨论一下就知道,如果名称改得更好,那大家真的会感激你。

改名可能会让某人吃惊,就像你做到其他代码改善工作一样。别让这种事阻碍你的前进步伐。 不妨试试上面这些规则,看你的代码可读性是否有所提升。如果你是在维护别人写的代码,使用重构工具来解决问题。效果立竿见影,而且会持续下去。