Clean Code阅读笔记
为什么要写简洁代码?
1.3 混乱的代价
你当然曾为糟糕的代码所困扰过。那么——为什么要写糟糕的代码呢?是想快点完成吗?是要赶时间吗?有可能。或许你觉得自己要干好所需的时间不够;假使花时间清理代码,老板就会大发雷霆。或许你只是不耐烦再搞这套程序,期望早点结束。或许你看了看自己承诺要做的其他事,意识到得赶紧弄完手上的东西,好接着做下一件工作。这种事我们都干过。
我们都曾经瞟一眼自己亲手造成的混乱,决定弃之而不顾,走向新一天。我们都曾经看到自己的烂程序居然能运行,然后断言能运行的烂程序总比什么都没有强。我们都曾经说过有朝一日再回头清理。当然,在那些日子里,我们都没听过勒布朗(LeBlanc)法则:稍后等于永不(Later equals never)。
混乱的代价只要你干过两三年编程,就有可能曾被某人的糟糕的代码绊倒过。如果你编程不止两三年,也有可能被这种代码拖过后腿。进度延缓的程度会很严重。有些团队在项目初期进展迅速,但有那么一两年的时间却慢如蜗行。对代码的每次修改都影响到其他两三处代码。修改无小事。每次添加或修改代码,都得对那堆扭纹柴了然于心,这样才能往上扔更多的扭纹柴。这团乱麻越来越大,再也无法理清,最后束手无策。随着混乱的增加,团队生产力也持续下降,趋向于零。当生产力下降时,管理层就只有一件事可做了:增加更多人手到项目中,期望提升生产力。可是新人并不熟悉系统的设计。他们搞不清楚什么样的修改符合设计意图,什么样的修改违背设计意图。而且,他们以及团队中的其他人都背负着提升生产力的可怕压力。于是,他们制造更多的混乱,驱动生产力向零那端不断下降。如图11所示。
程序员面临着一种基础价值谜题。有那么几年经验的开发者都知道,之前的混乱拖了自己的后腿。但开发者们背负期限的压力,只好制造混乱。简言之,他们没花时间让自己做得更快!真正的专业人士明白,这道谜题的第二部分说错了。制造混乱无助于赶上期限。混乱只会立刻拖慢你,叫你错过期限。赶上期限的唯一方法——做得快的唯一方法——就是始终尽可能保持代码整洁。
How?
假设你相信混乱的代码是祸首,假设你接受做得快的唯一方法是保持代码整洁的说法,你一定会自问:“我怎么才能写出整洁的代码?”不过,如果你不明白整洁对代码有何意义,尝试去写整洁代码就毫无所益!坏消息是写整洁代码很像是绘画。多数人都知道一幅画是好还是坏。但能分辨优劣并不表示懂得绘画。能分辨整洁代码和肮脏代码,也不意味着会写整洁代码!写整洁代码,需要遵循大量的小技巧,贯彻刻苦习得的“整洁感”。这种“代码感”就是关键所在。有些人生而有之。有些人费点劲才能得到。它不仅让我们看到代码的优劣,还予我们以借戒规之力化劣为优的攻略。
命名
名副其实
// e.g.
function getItem () {
return this.list.filter(i => i.status === 0)
}
// 1. this.list 是什么?
// 2. getItem 的目的是什么?/ status为0的含义是什么?
const auditStatus = {
AUDIT_PASS: 0,
AUDIT_INVALID: 1,
AUDIT_UNKNOWN: 2
}
function getAuditPassFiles () {
return this.fileList.filter(i => i.status === auditStatus.AUDIT_PASS)
}
只要简单改一下名称,就能轻易知道发生了什么。这就是选用好名称的力量。
避免误导
const accountList = { // accountArray
'12345678': { username: 'aaa', mobile: '138****8888'}
}
// List?
// List 一词对程序员有特殊意义,如果包纳账号的容器不是真的 list,会引起误会。
// 只叫 accounts 都比脚 accountList 好
const accounts = {
'12345678': { username: 'aaa', mobile: '138****8888'}
}
做有意义的区分
function copyFile (a1, a2) { ... } // bad
function copyFile (source, destination) { ... } // good
function copyFile (from, to) { ... } // good
// 这样的名称纯属误导——完全没有提供正确信息;没有提供导向作者意图的线索。
// 废话是另一种没意义的区分。
function getActiveAccount();
function getActiveAccounts();
function getActiveAccountInfo();
function getActiveAccountObject();
// 如果缺少明确约定,变量moneyAmount就与money没区别,customerInfo与customer没区别,accountData与account没区别,theMessage也与message没区别。要区分名称,就要以读者能鉴别不同之处的方式来区分。
// 明确是王道。
使用读得出来的名字
function genymdhms () { ... } // bad
function modymdhms () { ... } // bad
function generationTimestamp () { ... } // good
function modificationTimestamp () { ... } // good
使用可搜索的名称
// 搜索200 - 困难
// 搜索 PLATIUM_VOLUME - 容易
const PLATIUM_VOLUME = 200
const BYTES_IN_GB = 1 * 1024 * 1024 * 1024
const BYTES_IN_MB = 1 * 1024 * 1024
// 单字母名称仅用于短方法中的本地变量。名称长短应与其作用域大小相对应。
// 若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。
每个概念对应一个词
task / task ?
localTask / driveTask ?
使用解决方案领域名称
const jobQueue // FIFO
const tokenStack // FILO
添加有意义的语境
设想你有名为firstName、lastName、street、houseNumber、city、state和zipcode的变量。当它们搁一块儿的时候,很明确是构成了一个地址。不过,假使只是在某个方法中看见孤零零一个state变量呢?你会理所当然推断那是某个地址的一部分吗?
可以添加前缀addrFirstName、addrLastName、addrState等,以此提供语境。至少,读者会明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为Address的类。
不要添加无意义的语境
// movie.xunlei.com
XlMovie.vue
XlChange.vue
XlDialog.vue
只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。
最后的话
我们有时会怕其他开发者反对重命名。如果讨论一下就知道,如果名称改得更好,那大家真的会感激你。多数时候我们并不记忆类名和方法名。我们使用现代工具对付这些细节,好让自己集中精力于把代码写得就像词句篇章、至少像是表和数据结构(词句并非总是呈现数据的最佳手段)。
函数
怎么才能让函数表达其意图?该给函数赋予哪些属性,好让读者一看就明白函数是属于怎样的程序?
短小
函数的第一规则是要短小。第二条规则是还要更短小。
只做一件事
函数应该做一件事。做好这件事。只做这一件事。
要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。
每个函数一个抽象层级
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。
无副作用
副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。
不要重复自己
重复会导致问题,因为代码因此而臃肿,且当算法改变时需要修改4处地方。而且也会增加4次放过错误的可能性。
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。例如,全部考德(Codd)[14]数据库范式都是为消灭数据重复而服务。再想想看,面向对象编程是如何将代码集中到基类,从而避免了冗余。面向方面编程(AspectOrientedProgramming)、面向组件编程(ComponentOrientedProgramming)多少也都是消除重复的一种策略。看来,自子程序发明以来,软件开发领域的所有创新都是在不断尝试从源代码中消灭重复。
// 例子:PC端云盘的播放函数
async play (mode: VideoPlayMode, file: IMediaFiles, playfrom: string, dlna?: boolean): Promise<void> {
if (this.$store.state['media-player'].isPlayButtonDisabled) {
return
}
if (file.trashed) {
this.$emit('restore', { fromPlay: true })
return
}
if (this.checkIsLinkExpired(file)) {
log.info('REFRESH video link')
await this.initData()
file = this.fileDetail
}
if ((this.isCloudAdding || this.isLocalUploading) && (!Array.isArray(file.medias) || !(file.medias.length > 0))) {
this.$eventTrack('yunpan_play_fail_toast', { type: 'adding' })
this.$message({
message: '云播失败,请稍后重试',
id: 'pending_task',
unique: true,
type: 'error',
position: 'middle',
duration: 2000
})
return
}
if (file.web_content_link) {
try {
if (this.cancelableRetry !== null) { // 已有解冻任务则先取消再开始下一个
this.cancelRetry()
}
await this.checkIsFreezed(mode, file.web_content_link, Number(file.size))
} catch (error) {
if (error.message === 'cancel') {
console.warn('cancel')
return
}
this.$message({
message: '该视频读取时间长,请稍后再看',
type: 'error',
position: 'middle',
duration: 5000
})
}
}
if (!file.web_content_link) {
if (this.isLocalUploading) {
this.$eventTrack('yunpan_play_fail_toast', { type: 'upload' })
}
this.$message({
message: '该文件暂时无法播放',
id: 'no link',
unique: true,
type: 'error',
position: 'middle',
duration: 2000
})
return
}
try {
this.$store.commit('media-player/setIsPlayButtonDisabled', true)
log.info('play file:', file.name, mode)
const element = document.getElementById('pan-preview')
if (element) {
const rect = element.getBoundingClientRect()
log.info('dlna', dlna)
updateXmpPosition(rect)
}
if (!file.changeRect) {
await playWithXmp(
{
playUrl: file.web_content_link,
fileName: file.name || '',
isEmbed: mode !== 'independent',
openFrom: 'ThunderPanPlugin',
panFileId: file.id,
gcid: file.hash,
medias: file.medias,
dlnaPlay: dlna,
playType: 'server_xlpan',
playFrom: playfrom,
mimeType: file.mime_type
})
this.$store.commit('media-player/setIsPlayButtonDisabled', false)
window.addEventListener('resize', this.onResize.bind(this))
}
} catch (err) {
this.$store.commit('media-player/setIsPlayButtonDisabled', false)
log.error(err)
this.$message({
message: '播放失败',
type: 'error',
position: 'middle',
duration: 3000
})
}
}
注释
注释不能美化糟糕的代码
写注释的常见动机之一是糟糕的代码的存在。我们编写一个模块,发现它令人困扰、乱七八糟。我们知道,它烂透了。我们告诉自己:“喔,最好写点注释!”不!最好是把代码弄干净!带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多。与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。
用代码来阐述
有时,代码本身不足以解释其行为。不幸的是,许多程序员据此以为代码很少——如果有的话——能做好解释工作。这种观点纯属错误。你愿意看到这个:
// Check to see if the employee is eligible for full benefits
if((employee.flags&HOURLY_FLAG) && (employee.age>65))
还是这个?
if(employee.isEligibleForFullBenefits())
只要想上那么几秒钟,就能用代码解释你大部分的意图。很多时候,简单到只需要创建一个描述与注释所言同一事物的函数即可。
好注释
提供基本信息的注释
对意图的解释
- 注释不仅提供了有关实现的有用信息,而且还提供了某个决定后面的意图。
阐释
- 注释把某些晦涩难明的参数或返回值的意义翻译为某种可读形式,也会是有用的。
// fmt: MMddhhmmss 构成的格式字符串 export function formatTime ( dateObj: Date, fmt: string ) { type OType = { 'M+': number; 'd+': any; 'h+': any; 'm+': any; 's+': any; 'q+': number; S: any; [prop: string]: any; }; const o: OType = { 'M+': dateObj.getMonth() + 1, // 月份 'd+': dateObj.getDate(), // 日 'h+': dateObj.getHours(), // 小时 'm+': dateObj.getMinutes(), // 分 's+': dateObj.getSeconds(), // 秒 'q+': Math.floor((dateObj.getMonth() + 3) / 3), // 季度 S: dateObj.getMilliseconds() // 毫秒 } if (/(y+)/.test(fmt)) { fmt = fmt.replace( RegExp.$1, (dateObj.getFullYear() + '').substr(4 - RegExp.$1.length) ) } for (const k in o) { if (new RegExp('(' + k + ')').test(fmt)) { fmt = fmt.replace( RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(String(o[k]).length) ) } } return fmt }
警示
async getGlobalConfig () { /** * 此处天坑,注意! * 除了手雷搜索页以外,还有一个网盘SDK也用了本搜索页。 * sdk的xlConfig会返回空,所以全局配置相关的属性访问都需要更严谨的合法判断。 * 另外测试时最好把sdk的情况也过一下。 */ let [sniff, xlConfig] = await Promise.all([ this.getSniff(), this.getXlConfig() ])
放大
- 放大、解释某些不合理细节
坏注释
误导性注释
多余的注释 / 废话注释
class Example { /* Defaultconstructor.*/ constructor () { ... } }
括号后面的注释
- 有时,程序员会在括号后面放置特殊的注释。尽管这对于含有深度嵌套结构的长函数可能有意义,但只会给我们更愿意编写的短小、封装的函数带来混乱。如果你发现自己想标记右括号,其实应该做的是缩短函数。
注释掉的代码
归属与署名
/* AddedbyRick */
- 源代码控制系统非常善于记住是谁在何时添加了什么。没必要用那些小小的签名搞脏代码。你也许会认为,这种注释大概有助于他人了解应该和谁讨论这段代码。不过,事实却是注释在那儿放了一年又一年,越来越不准确,越来越和原作者没关系。重申一下,源代码控制系统是这类信息最好的归属地。