本文中的观点都是摘自《Clean Code》
书中有对观点更加详细的解释和说明
命名
有意义的命名
- 变量、函数、类的命名应当表明意义、作用以及使用方式
命名避免误导
- 不要将
account
的组合命名为accountList
,除非它真的是List
- 避免使用区别非常小的命名,比如
XYZControllerForEfficientHandlingOfStrings
和XYZControllerForEfficientStorageOfStrings
- 同样的概念使用相同命名,命名前后不一致也是问题
有意义的区分
- 不要对不同的东西使用相同的命名,即便是用数字下标区分也不合适
- 噪音词是无意义的区分。例如,命名为
ProductInfo
和ProduceData
基本没区别,Date
和Info
就是噪音词 - 噪音词是冗余的。例如,
variable
永远不应该成为一个变量的名称,table
也永远不应该成为一个表的名称 - 能使阅读代码的人区分出差别的命名才是有意义的命名
读得出的命名
- 编程本身也是一个社会活动,如果命名无法读出来,那就无法进行讨论
可搜索的命名
- 单字母命名和数字常量最大的问题就是没法在一篇文字中搜索出来
- 单字母命名仅可被用于本地变量
- 命名的长短与其作用域的大小对应
- 如果一个变量或常量在多个地方使用,则应赋予其可搜索的命名
避免使用前缀
- 不要使用前缀来标记成员变量
- 类和函数都应足够小,以此来避免使用前缀
类名
- 类名和对象名通常是名词或者名词短语,但是需要避免使用类似于
Data
、Info
这样的词汇
方法名
- 方法名通常是动词或者动词短语
其他
- 每一个概念对应一个词,例如,使用
get
、fetch
来给多个类中同样的方法命名 - 不要使用双关语,避免将同一个单词用于不同的目的,即遵循一词一义的原则
- 添加有意义的语境,不要添加不必要的语境
函数
短小
- 函数最重要的原则就是短小
- 每个函数封顶20行是比较好的
代码块和缩进
- if-else语句和while语句中的代码块都应只有一行,这行大多是函数调用
- 这样不仅可以保持函数短小,同时代码块中调用的具有说明性命名的函数也增加了文档价值
- 函数的缩进层级不能超过一层或两层,易于阅读和理解
只做一件事
FUNCTIONS SHOULD DO ONE THING. THEY SHOULD DO IT WELL. THEY SHOULD DO IT ONLY.
- 如果函数只是做了该函数名下同一抽象层级的几个步骤,它仍是只做了一件事情
- 判断函数是否只做了一件事情的另一方法是: 判断是否能再拆出来一个函数
- 只做一件事的函数无法被拆分为多个区段
每个函数一个抽象层级
- 为了确保函数只做一件事,我们需要确保函数中的语句都在一个抽象层级
- 一个函数中混合不同抽象层级容易产生困惑
自顶向下阅读代码
- 代码需要拥有自顶向下的顺序,这样做,在阅读代码时,就可根据抽象层级向下阅读
Switch语句
Switch
语句不可能只做一件事,不过仍可以确保每个Case
都在较低的抽象层级- 可以将
Switch
语句隐藏在抽象工厂方法的下面,不被任何使用的人看到
使用描述性名称
You know you are working on clean code when each routine turns out to be pretty much what you expected.
- 函数越小,做的事情越集中,那么为函数取一个具有描述性的名字就越容易
- 不要担心使用长名字。使用长的具有描述性的名字比使用短的费解的名字更好
- 不要害怕花费时间去起名字,好的名字也更有利代码重构
函数参数
- 最理想的函数参数个数是0个,其次是1个,再次是2个,避免使用3个参数,无论如何都不要使用3个以上的参数
- 确保函数执行结果是以返回值的形式出现,而不是更新到输入参数中
- 不要向函数中传递布尔值的参数,那是在表明你的函数不止做了一件事
- 如果可以的话,尽量减少函数参数的个数
- 如果函数需要大量的参数,是否考虑将其中一些参数封装为类
无副作用
- 避免函数承诺了只做一件事,但它却做了其他的事情
- 一定要避免将输入参数用作函数输出
分割指令和询问
- 函数应该执行操作或者进行判断,但是不应该都做
抛出异常比返回错误码更好
- 使用异常代替错误码,错误处理就可以从代码主流程分离,代码结构也会变得简单
- 抽离
try-except
代码块 - 错误处理也是
一件事
, 错误处理函数不应该做其他事。即try
是该函数的第一行,except/finally
代码块后也不应有任何内容
不要重复
- 重复会使代码变得臃肿,而且,增加了修改代码出错的可能
结构化编程
- 避免使用
goto
- 保持函数短小
如何写出这样的函数
- 没有人一开始就可以写出满足以上描述的代码
- 写代码和写论文一样,需要反复打磨,同时在打磨的时候遵守上面提到的规则
注释
- 注释的主要作用是弥补我们使用代码表达意图时的失败。注意,注释总是在表达失败
- 任何你想写注释时都应该考虑,是否可以使用更合适的代码来表明意图
- 不推荐使用注释的一个原因是,注释会撒谎。注释存在的越久,就和描述的代码越远,这也是因为程序员不能坚持维护注释
- 不准确的注释比没有注释更恐怖,只有代码才可以忠诚地告诉你它做了什么
注释不能美化代码
- 大部分情况下写注释的动机是,我们想对写的很差的代码进行描述
- 清晰,表达力强,注释少的代码远胜于混乱且复杂,注释多的代码
- 比起花费时间对混乱代码进行描述,更好的做法是花费时间去结局混乱
用代码来描述
- 用代码来解释意图,很多时候创建一个与注释描述的是同一事务的函数即可
好注释
- 最好的注释就是不写注释
- 版权等法律信息可作为注释
TODO
信息
坏注释
- 大多数的注释属于坏注释
对象和数据结构
- 面向过程,便于在不改动数据结构的前提下增加函数。面向对象,便于在不改动已有函数的前提下增加数据结构
- 面向过程难以添加新数据结构,因为必须改动所有函数。面向对象难以改动函数,因为必须改动所有数据结构
- 数据结构应该最好只拥有简单的共有变量,没有函数
- 对象拥有私有变量和公共函数
Demeter定律
- 模块不应了解它操作对象的内部结构。对象隐藏数据结构,暴露实现。对象不应通过存取器暴露它的内部结构
- 更准确来说,类
C
的方法f
只能调用以下对象的函数:C
- 由
f
创建的对象 - 作为参数传递给
f
的对象 C
的实体变量持有的对象
- 类
C
的方法f
不能调用任何由函数返回的对象的方法 - 以下代码就违反了
Demeter
定律: 它调用了函数getOptions
返回对象的getScratchDir
方法, 接着又调用了函数getScratchDir
返回对象的getAbsolutePath
方法
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath()
数据传递对象
- 典型的数据结构是一个有公有变量、没有函数的类,它对于数据库和网络访问非常有用
错误处理
- 如果错误处理搞乱了代码逻辑,那么它就是错误的做法
使用异常而非返回码
- 使用返回码最大的问题是搞乱了调用者代码,而且容易遗忘错误处理
使用不可控异常
- 可控异常可能会产生一个从软件最底端贯穿到最高端的异常链
不要返回null
值
- 任何时候都不应该返回
null
不要传递null
值
- 即,不要将
null
值作为参数传递给函数 - 返回
null
很不好,但是传递null
就更离谱了 - 无论任何时候,可能的话都不要传递
null
边界
我们不可避免需要第三方软件和程序包,所以我们需要将外来代码整洁地整合到我们自己的代码中
边界代码需要清晰地定义分界和期望测试用例
可以针对第三方包进行封装或者写适配器进行转换, 以便我们获取需要的功能
第三方代码
- 第三方包和程序框架的提供者为追求普适性,会尽可能让代码在多个环境中工作。但这样会可能导致代码在系统边界出现问题
- 第三方包可能会缺少很多对你的代码有用的限制,即提供了过多你本来不需要的功能
- 这时我们可以对第三方包进行封装,隐藏实现细节,并且屏蔽多余的功能
学习和浏览边界
- 在使用第三方包之前,我们可能需要写一些’学习性测试’来帮助我们理解需要的功能
学习性测试很重要
- 学习性测试没有任何成本,无论如何我们都要学习这个api,而学习性测试是获取这些知识的简单方法
使用尚不存在的代码
- 这是另一种类型的边界——分离已知和未知