《编写整洁代码的艺术》
事实上,我认为《Clean Code》《Readable Code Arts》《Refactoring》这三本书共同组成了《编写整洁代码的艺术》。它不仅包括代码在 命名、结构、注释、架构组织 上的clean,还包括程序员面对需求和交流时,如何保持自身代码的整洁和质量。
还有《Design Pattern》也是
代码整洁之道
整洁代码与进度
程序员总是被各种进度压着,但这就是写出糟糕代码的理由吗,不是的。看本书举的例子:
"假设你是位医生,病人请求你在给他做手术时别洗手,因为那会花太多时间,你会照办吗?本该是病人说了算;但医生却绝对应该拒绝遵从。为什么?因为医生比病人更了解疾病和感染的风险。医生如果按照病人说的办,那就是一种不专业的态度。"
"你说:‘不听经理的,我就会被炒鱿鱼’。多半不会,多数经理想要知道实情,即便他们看起来不喜欢实情。多数经理想要好代码,即便他们总是痴缠与进度。他们会奋力卫护进度和需求;那是他们该干的。你则当以同等的热情卫护代码"
对应到游戏领域,我的观点是:
- 专业的程序应该分析出需求给代码带来的bad effct,并告知策划实情。程序比策划更专业,更应该对自己的代码负责,而不是默默按策划要求完成然后出问题甩锅给策划!
- 及时同步方案,给出自己推荐的更佳方案,三方协同来保证做出最高质量。
- 预估人天应以保证代码质量的前提来估,如果不得不破坏质量来赶进度,那么完成之后要及时补锅。
迭代时保持重构
"让营地比你来时更干净"
每次check in代码都比check out时整洁,那么代码就不会腐坏。一次清理可能只是改好一个变量名,拆分一个过长函数的小操作,这点也是《refactoring》的重中之重。
这个观点认为新代码不应过度设计,而是在每次修改的过程中持续重构代码,保持代码整洁。持续重构应光明正大的放在项目的进度中,而不该认为是偷偷摸摸修改的设计漏洞。
有意义的命名
这里就总结一下几本书对于命名的经验吧
- 最好能用命名代替注释,直观的让它易于阅读易于理解。因为阅读代码的时间10倍于写代码的时间
- 方法用动词,对象用名词。同样的概念要统一命名(有时用专有名词)
- 命名可以带上 单位/属性/信息,取代注释
- bool值不要用flag,那是enum
短小精悍的函数
upgrade函数做到了只在一个抽象层级,对于判断升级条件,扣除升级道具是属于下一层抽象层级:
MAX_UPGRADE_LV = 100
UPGRADE_ITEM_ID = 123
UPGRADE_ITEM_NUM = 10
def upgrade(self):
if self.validateUpgrade():
self._costUpgradeItems()
self.lv += 1
def validateUpgrade(self):
if self.lv < MAX_UPGRADE_LV:
if self.getItemNum(UPGRADE_ITEM_ID) >= UPGRADE_ITEM_NUM:
return True
return False
def _costUpgradeItems(self):
self.delItem(UPGRADE_ITEM_ID, UPGRADE_ITEM_NUM)
编写可读代码的艺术
代码应当易于理解
主要是讲了一些必要性和好代码的衡量
可读性>简洁性
Simple:代码的写法应当使别人理解它所需的时间最小化
注释面向人,代码面向编译器:

把信息装到名字里
选择专业的词
get根据情境,用FetchPage()或者DownloadPage()代替getPage()size()在树中应该用height()表示高度,numNodes()表示节点数,用memoryBytes()表示内存中所占的空间
Get在互联网开发中显得不专业
找到更有表现力的词
| 单词 | 更多选择 |
|---|---|
| send | deliver、dispatch、 announce、 distribute、route |
| find | search、extract、locate、recover |
| start | launch、create、begin、open |
| make | create、set up、build、generate、compose、add、new |
避免泛泛的名字
- 在需要实际意义表现时,
tmp、retval、foo可以作为名字的一部分,但是不要轻易直接使用。如用tmp_file我们就可以知道这是个临时文件 - 某些情况使用空泛的名字也有好处,比如说在交换两个变量的时候使用
tmp,在循环迭代器中使用i、j、iter,但是在嵌套的循环中,可以用ci,mi,ui来标明,迭代器名称要更清楚
用具体的名字代替抽象的名字
- 比如建立和使用一个特殊的本地数据库作为本地日志输出,run_locally这个名字只表达了模糊的操作位置,却无法表示具体的作用,use_local_database就比较合适了
使用前缀或后缀来给名字附带更多信息
【命名就是一个小小的注释】
hexId表示了id是16进制的,如果确定是16进制很重要的话delay_ms带单位的值最好附带上单位:
| 函数参数 | 带单位的参数 |
|---|---|
| start(int delay) | delay --> delay_secs |
| createCache(int size) | size --> size_mb |
| throttleDownload(float limit) | limit --> max_kbps |
| rotate(float angle) | angle --> degree_cw |
- 附带其他重要属性:
| 情形 | 变量名 | 更好的名字 |
|---|---|---|
| 一个"纯文本"格式的代码,需要加密后才能进一步使用 | password | plaintext_password |
| 一条用户提供的注释,需要转义之后才能用于显示 | comment | unescaped_comment |
| 已转化为UTF-8格式的html字节 | html | html_utf8 |
| 以"url方式编码"的输入数据 | data | data_urlenc |
名字应该有多长
- 在小作用域中使用短的名字,相反在大作用域中使用长名字。现代编辑器能方便使你键入长名字
- 首字母缩略词和单词缩写应该是大家普遍接受和理解的,例如用doc代替document、str代替string。特定项目的缩写就不好了。
- 丢掉没用的词,
ConvertToString可以直接写成toString,这样也没有丢失任何信息
不会误解的名字
- 关键思想:要仔细审视名字,"这个名字会被别人误解成其他含义吗"
- 当要定一个值的上线或者下限时,max和min是很好的前缀
- 推荐用 min 和 max 来表示包含的极限。[min, max]
- 对于包含的范围,first 和 last 是好的选择。[first, last]
- 对于排除的范围,begin 和 end 是好的选择。[begin, end)
- start 和 stop ,则并不适合表示范围。
- 命名应与使用者的期望相匹配,例如:
get*()是个"轻量级访问器",list.size()应该是一个*O(1)*复杂度的操作。countSize/countElements才适合O(n)以上计算的方法 - 很多单词在用来编程时是多义性的,例如filter、length和limit
bool命名
is, has done, can, should,...able
或者是一些其他表判断的动词,need
-
最好避免使用反义的词 (例如 disable_ssl )
-
形容词可以加is也可以不加。比如isYoung、isSimple、isNaive可以直接写成young、simple、naive
-
但是如果这个形容词有常用的做动词的含义,那就要加is,比如empty这个词可以作动词表示清空的意思,那么表示是否为空就写成isEmpty而不是empty。
-
不要用flag, flag应该是enum。
代码审美
Key:
-
重新安排换行保持一致和紧凑。换行可以让代码更美观,现在的IDE也经常会帮你这么做。
-
用方法整理不规则的代码。
-
在需要时使用列对齐。注释对齐
列对齐非常美观简洁,而且能一眼看出各行间的不同,以及发现错误 。但是写起来比较麻烦,而且改动时更麻烦。
建议在写完自审时用列对齐,也更容易发现错误。
- 选择一个有意义的顺序,始终一致的使用它。 比如重要性,比如字母顺序
- 把声明按块组织起来。
就像我在声明Player的Attribute、Anim、State等变量时一样,代码分段会清晰很多
- 团队一致的风格比个人风格更重要
该写什么样的注释
Key:
注释的目的是尽量帮助读者了解的和作者一样多。
什么不需要注释?
-
不要为那些从代码本身就能快速推断的事实写注释
-
不要为了注释而注释
IDE要我写注释,我就非得写不可吗?明明没什么注释需要写
- 不要为不好的名字加注释->应该把名字改好
什么需要注释?
- 加入"导演评论",记录你写代码时重要的想法。这是好注释

- 记录对代码有价值的见解,例如:解释代码没法修复的缺陷、代码不整洁的原因
- 为代码中的瑕疵写注释,比如有如下几种标记:
| 标记 | 通畅的意义 |
|---|---|
| TODO: | 我还没有处理的事情 |
| FIXME: | 已知的无法运行的代码 |
| HACK: | 对一个问题不得不采用的比较粗糙的解决方案 |
| XXX: | 危险!这里有重要的问题 |
-
给常量加注释,需要描述清楚它是什么或者为什么它是这个值的原因,因为以后调整这个变量的人可能需要知道调整的规则。
-
站在读者的角度
- 为什么要这样写的地方,做出解释。(比如为什么不用常规方式实现)
- 公布可能的陷阱。(比如这个函数可能要执行1分钟才超时)
- "全局观"注释。为项目中的系统架构而写,帮助他人更好的理解类之间的关系
- 在一个类或者函数内部编写总结性的注释
- 克服"作者心理阻滞":就是懒得写注释
- 不管心里想什么,先把它写下来(打字快)
- 读一下这段注释,看看有没有什么可以改进的地方
- 不断改进
当你经常写注释,你会发现步骤1所产生的注释会越来越好,就越不需要后面两步了
写出言简意赅的注释
Key:
注释应当有很高的信息/空间率。简洁不废话
-
让注释保持紧凑。能1行讲明白为什么要写3行废话?
-
避免使用不明确的代词(不要用"这个","那个","它"这些模糊不清的代词)。
-
润色粗糙的句子,精确地描述函数的行为
-
在注释中用精心挑选的输入/输出例子进行说明。在函数行为复杂,描述困难不直观的情况下,可以使用输入输出的例子来直观的表示函数的用途。
-
声明代码的意图,而非细节。比如一个倒序循环显示价格的代码,就没必要只强调倒序显示,而应该指出显示的是价格,顺序是从高到低,省去读者思考的过程。
-
使用嵌入式注释来描述函数参数。
简化循环和逻辑
把控制流变得易读
关键思想:把条件、循环、以及其他对控制流的改变做的越"自然"越好。让读者不用停下来重读代码。
条件语句
- 条件语句中比较参数的顺序,有以下指导原则:
| 比较的左侧 | 比较的右侧 |
|---|---|
| "被问询的"表达式,它的值更倾向于不断变化 | 用来做比较的表达式,它的值更倾向于常量 |
这是和日常语言习惯是一致的,我们会很自然的说:"如果你的年收入至少是10万"
-
"尤达表示法":在有些语言中(包括C++和C,不包括Java)为了防止
if(obj==NULL)被写成if(obj=NULL),出现了if(NULL==obj)这样的写法,但是这样不利于理解,与上一条相悖,现代编译器已经能对if(obj=NULL)给出警告,所以这个写法已经过时了。 -
if/else语句块的顺序:
- ==首先处理正逻辑而不是负逻辑的情况==。例如,用
if(debug)而不是if(!debug) - ==先处理掉简单的情况==。这种方式可能还会让if和else在屏幕内都可见
- ==先处理有趣的或者是可疑的情况==
- ==首先处理正逻辑而不是负逻辑的情况==。例如,用
-
不要为了减少代码行数而使用三目运算符,它只适用于从两个简单的值中作出选择的情况,例如:
time_str += (hour >= 12) ? "pm" : "am";
因为带有复杂逻辑的三目运算符反而增加了代码的阅读时间
循环
- 避免do/while循环,它的continue语句会让人迷惑,while循环相对更加易读。实践中,大多数do/while循环都可以写成while循环
从函数提前返回
- 函数中使用多条
return语句是没有问题的 - 实现函数结尾的清理代码的更为精细的方式
| 语言 | 清理代码的结构化术语 |
|---|---|
| C++ | 析构函数 |
| Java、Python | try finally |
| Python | with |
| C# | using |
- goto语句对程序易读性的破坏
嵌套
- 深层次的嵌套严重影响代码的可读性
- 嵌套一开始是很简单的,但是后来的改动会加深嵌套
- 通过提早返回来减少嵌套
- 减少循环内的嵌套,与提早返回类似的技术是使用continue
代码流程
- 编程语言和库的结构让代码在"幕后运行",或者让代码难以理解,如:
| 编程结构 | 高层次程序流程是如何变得不清晰的 |
|---|---|
| 线程 | 不清楚什么时间执行什么代码 |
| 信号量/中断处理程序 | 有些代码随时有可能执行 |
| 异常 | 可能会从多个函数调用中向上冒泡一样地执行 |
| 函数指针和匿名函数 | 很难知道到底会执行什么代码,因为在编译时还没有决定 |
| 虚方法 | object.virtualMethod()可能会调用一个未知子类的代码 |
有时候这很有用,但是还是要减少使用的比例,否则跟踪代码就会像赌博一样。
拆分超长表达式
关键思想:把超长表达式拆分成更容易理解的小块
解释变量
- 引入一个额外的变量,使之成为一个小一点的子表达式
如 username = line.split(':')[0].strip()
总结变量
- 用一个很短的名字来代替一大块代码,会更容易管理和思考
如 user_own_document = (request.user.id == document.owner_id)
使用德摩根定理
- (这样的bool操作已经掌握)分别进行取反、转换与/或,反向操作是提取出"反因子"
不滥用短路操作
- 短路操作虽然可以很智能的运用在某些场景,使之成为条件控制的效果,但是影响代码的理解
- 但短路操作在很多情况下也能达到简洁的目的
拆分巨大的语句
- 复杂的逻辑会产生复杂的表达式,表达式复杂会增加代码的阅读难度,解决它需要转换思维,用更优雅的方式
- 巨大的语句的拆分需要找到重复的部分,进行简化
- 有时需要把问题"反向"或者考虑目标的对立面
变量与可读性
主要问题:
- 变量越多,就越难全部跟踪它们的动向
- 变量的作用域越大,就需要跟踪它的动向越久
- 变量改变得越频繁,就越难以跟踪它的当前值
减少不能改进可读性的变量
- 减少没有价值的临时变量,比如
- 没有拆分任何复杂的表达式
- 没有做更多的解释
- 只用过一次,因此没有压缩任何冗余代码
- 减少中间结果
- 减少控制流变量
while(/*condition*/&&!done){
if(...){
done = true;
continue;
}
}
done就是控制流变量,可以通过更好的运用结构化编程而消除:
while(/*condition*/){
if(...){
break;
}
}
当有多个嵌套的循环时,一个break可能不够,通常的解决方案是把代码挪到一个新方法中
缩小变量的作用域
- 避免滥用全局变量
- 让你的代码对尽量少的代码行可见
- ==把定义往下移,变量定义在使用之前即可==
只写一次的变量更好
-
那些只设置一次值的变量(或者const、final、常量)使得代码更容易理解
-
就算不能让变量只写一次,让变量在较少的地方改动仍有帮助
-
操作一个变量的地方越多,越难确定它的当前值
重新组织代码
抽取不相关的子问题
积极的发现并抽取出不相关的子逻辑
- 理解某个函数或者代码块高层次的目标
- 对于每行代码确定它是否为目标而工作
- 如果有很多代码行在解决 不相关的子问题,将它抽取到独立的函数中
什么是"不相关"的子问题
- 完全是自包含的,并不知道其他程序是如何使用它的
- 纯工具的代码,例如操作字符串、使用哈希表以及读/写文件
- 通用的代码库
优化现有的接口
- 永远不要安于使用不理想的接口
- 创建自己的包装函数和隐藏接口的粗陋细节
- 按需要重塑接口
过犹不及
- 更多的小函数意味着更多的东西需要关注
- 不要为了抽取而抽取
代码应当一次只做一件事情
相当于一个函数应该只做一件事情,但是一个函数也可以用空白行区分不同的事情,来达到逻辑上的清晰
列出任务
- 将所有任务列出来,其中一些任务可以很容易地编程单独的函数(或类)
- 难点在于准确描述列出的所有的小任务
分割任务
- 分开解决任务使代码变得更小
把想法变成代码
方法
- 用自然语言描述程序
- 用这个描述来帮助你写出更自然的代码
用自然语言说事情
- "小黄鸭法"解决所遇到的代码问题
- 如果你不能把问题说明白或者用词语来设计,估计是缺少了什么东西或者什么东西缺少定义
少写代码
- 重用库或者减少功能,可以节省时间并且让代码库保持精简节约
- 最好读的代码就是没有代码
不去费时间实现不需要的功能
- 没用的功能尽管很酷,但会让程序更复杂
质疑和拆分需求
- 从项目中消除不必要的功能,不要过度设计
- 重新考虑需求,解决版本最简单的问题,只要完成工作就行
让你的代码库越小,越轻量级越好
- 创建越多越好的"工具"以减少重复代码
- 减少无用代码或者没有用的功能
- 是项目中的子项目解耦
- 保持代码的轻量级
保持对标准库的API的熟悉,尽量使用标准库解决现实问题
测试与可读性
在测试代码中,可读性仍然很重要。如果测试的可读性很好,其结果是他们也会变得很容易写,因此大家会写更多的测试。并且,如果你把事实代码设计得容易测试,代码设计会变得更好。以下是如何改进测试的具体要点:
- 每个测试的最高一次应该越简明越好。最好每个测试的输入/输出可以用一行代码描述;
- 如果测试失败了,它所发出的错误消息应该能让你容易跟踪并修正这个bug;
- 使用最简单的并且能够完整运用代码的测试输入;
- 给测试函数取一个有完整描述性的名字,以使每个测试所测到的东西很明确。不要用Test1(),而要像Test___这样的名字。
最重要的是,要使它易于改动和增加新的测试。

测试驱动开发(TDD):是一种可选的编程风格,在写真实代码之前就写出测试。


Published by Kira on