书籍介绍:https://book.douban.com/subject/4199741/
整洁代码
整洁不会拖慢你的进度
程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法
混乱只会立刻拖慢你,叫你错过期限
赶上期限的唯一方法——做得快的唯一方法 ——就是始终尽可能保持代码整洁
何为好代码
代码逻辑应当直截了当,叫缺陷难以隐藏:
- 尽量减少依赖关系,使之便于维护
- 依据某种分层战略完善错误处理代码
- 性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来
整洁的代码只做好一件事,力求集中:
- 每个函数、每个类和每个模块都全神贯注于一事
- 整洁的代码应当明确地展现出要解决问题的张力
- 它应当将这种张力推至高潮,以某种显而易见的方案解决问题和张力
- 代码应当讲述事实,不引人猜测
- 它只该包含必需之物。读者应当感受到我们的果断决绝
- 它应当有单元测试和验收测试。它使用有意义的命名
- 它只提供一种而非多种做一件事的途径
- 它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽量少的API
- 代码应通过其字面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达
消除重复和提高表达力:
如果同一段代码反复出现,就表示某种想法未在代码中得到良好的体现
尽力去找出到底那是什么,然后再尽力更清晰地表达出来
检查对象或方法是否想做的事太多。如果对象功能太多,最好是切分为两个或多个对象
如果方法功能太多,我总是使用抽取手段(Extract Method)重构之
- 从而得到一个能较为清晰地说明自身功能的方法,以及另外数个说明如何实现这些功能的方法
消除重复和提高表达力让我在整洁代码方面获益良多,只要铭记这两点,改进脏代码时就会大有不同
你不会为整洁代码所震惊:
- 你无需花太多力气:那代码就是深合你意
- 它明确、简单、有力:每个模块都为下一个模块做好准备
- 每个模块都告诉你下一个模块会是怎样的,整洁的程序好到你根本不会注意到它,设计者把它做得像一切其他设计般简单
语言是冥顽不化的:
- 是程序员让语言显得简单
实际上,作者有责任与读者做良好沟通:
- 下次你写代码的时候,记得自己是作者,要为评判你工作的读者写代码
有意义的命名
名副其实
一个好的变量、函数或类的名称应该已经几乎答复了所有的大问题
它应该告诉你,这个名称所代表的内容,为什么会存在,做了什么事情,应该如何用等
如果一个名称需要注释来补充才能让大家明白其真正含义,那其实就不算是名副其实
举个栗子:
以下的这句代码里的d就不算是个好命名
- 名称d什么都没说,它没引起我们对时间消逝的感觉,更别说单位是天了
int d; // elapsed time in days||经过了几天时间
// 我们应该选择这样的指明了计量对象和计量单位的名称:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
避免造成误导
我们应该避免留下隐藏代码本意的错误线索,也应该避免使用与本意相悖的词
例如,别用accountList来指一组账号
- 除非它真的是List类型,用accountGroup、bunchOfAccounts,或者直接用accounts,都是更好的选择
尽量提防长得太像的名称:
想区分
XYZControllerForEfficientHandlingOfStrings
和XYZControllerForEfficientStorageOfStrings
- 会花费我们太多的时间
因为这两个词,实在太相似了
以同样的方式拼写出同样的概念才是信息,拼写前后不一致就是误导
尽量做有意义的区分
尽量避免使用数字系列命名
(a1、a2…….aN)
:
- 这样的名称纯属误导,因为很多时候完全没有提供正确的信息,没有提供导向作者意图的线索
废话是另一种没有意义的区分:
- 如果我们有一个Product类,还有一个ProductInfo或ProductData类,那么他们的名称虽然不同,但意思却无区别
- 这里的Info、Data就像a、an和the一样,是意义含混的废话
注意,只要体现出有意义的区分,使用a、the这样的前缀就没错
- 例如,将a用在域内变量,把the用于函数参数
尽量使用读得出来的名称
我们要使用读得出来的名称
如果名称读不出来,讨论的时候就会不方便且很尴尬,甚至让旁人觉得很蠢
- 例如,变量名称是
beeceearrthreecee
,讨论的时候读起来简直像没吃药
尽量使用可搜索的名称
单字母和数字常量有个问题,就是很难再一大篇文字中找出来
- 找
MAX_CLASSED_PER_STUDENT
很容易,但想找数字7,就很麻烦同样,字母e也不是个便于搜索的好变量名。因为作为英文中最常用的字母,在每个程序、每段代码中都有可能出现
名称长短应与其作用域大小相对应,若变量或常量可能在代码中多处使用,应赋予其以便于搜索的名称
举个栗子,比较如下两段代码:
for (int j=0; j<34; j++)
{
s += (t[j]*4)/5;
}
和
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j < NUMBER_OF_TASKS; j++)
{
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}
按整洁代码的要求来评判,第一段代码会让读者不知所云,第二段代码比第一段好太多
- 第二段代码中,sum并非特别有用的名称,但至少他搜索得到
采用能表达意图的名称,貌似拉长了函数代码,但要想想看,
WORK_DAYS_PER_WEEK
要比数字5好找得多
- 而列表中也只剩下了体现我们意图的名称
取名不要绕弯子
我们取名的时候要避免思维映射,不应当让读者在脑中把你的名称翻译为他们熟知的名称
- 也就是说取名不要绕弯子,而是要直白,直截了当
在多数情况下,单字母不是个好的命名选择,除非是在作用域小、没有名称冲突的地方,比如循环
- 循环计数器自然有可能被命名为i,j或k(最好别用字母l),这是因为传统上我们惯用单字母名称做循环计数器
程序员通常都是聪明人,聪明人有时会借助脑筋急转弯炫耀其聪明
而聪明程序员和专业程序员之间的区别在于,专业程序员了解,明确就是王道
专业的程序员善用其能,能编写出其他人能理解的代码
类名尽量用名词
类名尽量用名词或名词短语,比如Customer,WikiPage,Account 或 AddressParser
类名最好不要是动词
方法名尽量用动词
方法名尽量用动词或动词短语
- 比如
postPayment,deletePage
或者save属性访问器、修改器和断言应该根据其value来命名,并根据标准加上get、set和is前缀
举个栗子,这里的getName、setName等命名都很OK
string name = employee.getName();
customer.setName("mike");
if (paycheck.isPosted())...
而重载构造器时,使用描述了参数的静态工厂方法名
Complex fulcrumPoint =Complex.FromRealNumber(666.0);
通常好于:
Complex fulcrumPoint = new Complex(666.0);
我们也可以考虑将相应的构造器设置为
private
,强制使用这种命名手段
每个概念对应一词,并一以贯之
我们需给每个概念选一个词,并且一以贯之
- 例如,使用
fetch、retrieve
和get来给在多个类中的同种方法命名,你怎么记得住哪个类中是哪个方法呢?同样,在同一堆代码中混用
controller、manager,driver
,就会令人困惑DeviceManager和Protocol-Controller之间有何根本区别?
为什么不全用controller或者manager?他们都是Driver吗?
- 这就会让读者以为这两个对象是不同的类型,也分属不同的类
所以,对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音
通俗易懂
我们应尽力写出易于理解的变量名,把代码写得让别人能一目了然,而不必让人去非常费力地去揣摩其含义
我们想要那种大众化的作者尽责地写清楚的通俗易懂的畅销书风格
- 而不是那种学者学院风的晦涩论文写作风格
尽情使用解决方案领域专业术语
记住,只有程序员才会读你写的代码
- 所以,尽管去用那些计算机科学(Computer Science,CS)领域的专业术语、算法名、模式名、数学术语
对于熟悉访问者(Visitor)模式的程序来说,名称
AccountVisitor
富有意义给技术性的事物取个恰如其分的技术性名称,通常就是最靠谱的做法
添加有意义的语境
很少有名称是可以自我说明的
- 所以,我们需要用有良好命名的类,函数或名称空间来放置名称,给读者提供语境
若没能提供放置的地方,还可以给名称添加前缀
举个栗子,假如我们有名为
firstName、lastName、street、houseNumber、city、state
和zipcode的变量
当他们搁一块儿的时候,的确是构成了一个地址
不过,假如只是在某个方法中看到一个孤零零的state呢?
我们会推断这个变量是地址的一部分吗?
我们可以添加前缀addrFirstName、addrLastName、addrState等,以此提供语境
- 至少,读者可以知道这些变量是某个更大变量的一部分
当然,更好的方案是创建名为Address的类
这样,即便是编译器也会知道这些变量是隶属于某个更大的概念了
- 另外,只要短名称足够好,对含义的表达足够清除,就要比长名称更合适
添加有意义的语境甚好,别给名称添加不必要的语境
避免使用编码
使用编码 是一个不好的选择,把类型或者作用域添加的变量名中,无疑给变量名添加了额外的信息,增加了解码的负担
string m_name;
应该避免这样的命名方式,下面
python
语言为例:
m_list;
arr_list;
函数书写准则
短小
短小函数的第一规则
第二条规则还是要更短小
- 每行都不应该有150个字符那么长,函数也不该有100行那么长
函数的缩进层级不该多于一层
单一职责 只做一件事
只做一件事,如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事
判断函数是否不止做了一件事,还有就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现
- 只做一件事的函数无法被合理地切分为多个区段。
设计模式中有单一职责原则,我们可以把这条原则理解为代码整洁之道中的函数单一职责原则
函数参数尽可能的少
每个函数一个且仅有抽象层级:
- 我们想要让代码拥有自顶向下的阅读顺序
- 让代码读起来像是一系列自顶向下的 起头段落是保持抽象层级协调一致的有效技巧
- 把读代码想象成读报纸
- 总分结构,或者总分总结构
switch语句:
- 多态–将switch语句埋到抽象工厂底下,在系统的其他部分看不到,就还能容忍
- 可以使用抽象工厂来进行改进
函数名称使用:描述性名称
testableHtml 改为
SetupTeardownIncluder.render
,好的名称价值怎么好评都不为过如果每个例程都让你感动深合已意,那就是简洁代码
函数参数:
- 参数越少越好,参数越少越便于理解
一元函数的普遍形式:
- 函数名称应能区分出来
问关于参数的问题,如
boolean fileExist("myFile");
将参数转换为其他什么东西,在输出之:
- 如
InputStream fileOpen("myFile");
事件:
- 有输入参数而无输出参数,程序将函数看做一个事件,使用该参数修改系统状态
标识参数丑陋不堪,即如果标识为true将会这样做,标识为false将会这样做:
- 向函数传入布尔值简直是骇人听闻的做法
如果出现了参数是Boolean 值,就要思考一下,函数是否只做了一件事情
- 反复问自己 ,这个函数是不是做了一件事,或者承担了一项职责
二元函数:
- 可以把某个参数转换成当前类的成员变量,从而无需再传递它
- 当然有一些有两个参数更加合适
Point(x,y)
这种就比较合理三元函数:
- 排序,琢磨,忽略的问题都会加倍体现,写三元函数之前一定要想清楚
参数列表:
- 一个数量可变的参数等同于一个参数
函数命名应与参数形成动词/名词对,如
writeField(name)
无副作用:
- 函数承诺只做一件事,但还是会做其他被藏起来的事,例如时序性耦合
输出参数:
- 面向对象语言中对输出参数的大部分需求已经消失了
- 应避免使用输出参数,如果函数必须要修改某种状态,就修改所属对象的状态吧
看下这个例子:
appendFooter(s)
看到这个 首先就会 想把 s 添加什么东西,或者它把什么东西加到s 后面?
- s 是输入 还是输出参数呢?
public void appendFooter(StringBuffer report);
看了函数的签名,我们大概明白了。
所以 在面向对象语言中,最好的做法
report.appendFooter() // 这样来调用。
分隔指令与询问
函数要么做什么事(
do
),要么回答什么事(boolean
),但二者不可兼得
public boolean set(String attributer, String value);
用户调用:
if (set("username","frank")) {//...
}
看到这个代码,首先想 是否能够成功设置username 为frank, 还是要问,username 是否已经之前被设置过为frank了呢?
对看代码的人会比较困惑
所以 解决方案 就是 把做什么和是否成功分开
- 防止发生混淆
if (exists("username")){
setAttribute("username","frank")
}
使用异常替代返回错误码
使用异常替代错误码返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化
抽离
try/catch
代码块:
try/catch
代码块丑陋不堪最好把try和catch代码块的主体部分抽离出来,另外形成函数
错误处理就是一件事:
- 如果try在某个函数中存在,它就该是这个函数的第一个单词
- 而且在catch/finally代码块后面也不该有其他内容
Error.java
依赖磁铁:
返回错误码通常暗示某处有个类或是枚举定义了所有错误码
使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署
使用异常的好处,可以避免嵌套太深的层级
- 代码可以写起来更加简化
来看一个例子:
- 这里使用了很多的 嵌套的 if语句 这种很难让人理解,嵌套层级特别多,看起来很复杂
- 对于这种代码 最好直接抛出异常就可以,直接 使用
try
语句 然后捕获具体的异常就可以了而不是一层,一层的进行判断,嵌套起来,之后就很难维护这样的代码
对于try 代码块 丑陋不堪, 它们会搞乱代码结构,把错误处理和正常流程混为一谈
最好把 try 和 except 的主体部分抽离出来 单独形成函数
- 这样以后维护也会方便一点,看起来代码清晰,代码结构简单
public void delete(Page page){
try{
deletePageAndAllReferences(page);
}catch(Exception e){
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deltePage(page);
registry.deleteReference(page.name);
configKeys.deleteKeys(page.name.makeKey());
}
public void logError(Exception e ){
logger.log(e.getMessage)
}
别重复自己
别重复自己:重复可能是软件中一邪恶的根源
- 其实可以这样说,重复可能是软件中一切邪恶的根源,许多原则与实践规则都是为控制与消除重复而创建的
仔细想一想,面向对象编程是如何将代码集中到基类,从而避免了冗余的
而面向方面编程(Aspect Oriented Programming)
面向组件编程(Component Oriented Programming)多少也是消除重复的一种策略
- 这样看来,自子程序发明以来,软件开发领域的所有创新都是在不断尝试从源代码中消灭重复
重复而啰嗦的代码,乃万恶之源,我们要尽力避免
结构化编程:
- 每个函数、函数中的每个代码块都应该有一个入口,一个出口,遵循这些规则
- 意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句
- 对于小函数,这些规则助益不大,只有在大函数中,这些规则才会有明显的好处
如何写出这样的函数
先想什么就写什么,然后再打磨它
- 初稿也许粗陋无序,你就斟酌推敲,直到达到你心中的样子
最好的情况要配上一套单元测试,覆盖每一行丑陋的代码
然后开始打磨这些代码,分解函数,修改名称,消除重复。最后要保持测试可以顺利通过
我并不是一开始就按照规则写函数。 我想没有人做得到
代码注释准则
注释不能美化糟糕的代码
写注释的最常见的动机之一:
- 就是 糟糕的代码存在
其实带有少量注释而有表达力的代码,要比大量注释的零碎而复杂的代码像样得多
与其花时间写注释,不如花时间清洁那堆糟糕的代码
用代码来阐述
如果一段代码本身就具有表现力,完全没有必要写注释
从变量名或者函数名本身就具有自描述性,这样的代码就好一些
好注释例子
法律信息:
- 可指向一份标准许可或其他外部文档
提供信息的注释
对意图的解释
阐释
警示:
- 例如为什么用某种设计模式
TODO 注释
放大
- 注释可以用来放大某种不合理之物的重要性
公共API中的doc
坏注释例子
喃喃自语
多余注释
误导性注释
循轨式注释
日志式废话
废话注释:
- 用整理代码的决心替代创造废话的冲动吧
可怕的废话
能用函数或变量时就别用注释
位置标记:
- ///(少用,只在有价值的地方用)
括号后面的注释
归属与命名
注释掉的代码
html注释
非本地信息
信息过多
不明显的联系
函数头
非公共API中的javadoc
代码格式准则
像报纸一样一目了然
想想那些阅读量巨大的报纸文章
- 你从上到下阅读
在顶部,你希望有个头条,告诉你故事主题,好让你决定是否要读下去
- 第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事细节
接着读下去,细节渐次增加,直至你了解所有的日期、名字、引语、说话及其他细节
优秀的源文件也要像报纸文章一样。名称应当简单并且一目了然,名称本身应该足够告诉我们是否在正确的模块中
- 源文件最顶部应该给出高层次概念和算法
细节应该往下渐次展开,直至找到源文件中最底层的函数和细节
恰如其分的注释
带有少量注释的整洁而有力的代码,比带有大量注释的零碎而复杂的代码更加优秀
- 我们知道,注释是为代码服务的,注释的存在大多数原因是为了代码更加易读,但注释并不能美化糟糕的代码
另外,注意一点。
- 注释存在的时间越久,就会离其所描述的代码的意义越远,越来越变得全然错误
- 因为大多数程序员们不能坚持(或者因为忘了)去维护注释
当然,教学性质的代码,多半是注释越详细越好
合适的单文件行数
尽可能用几百行以内的单文件来构造出出色的系统,因为短文件通常比长文件更易于理解
- 当然,和之前的一些准则一样,只是提供大方向,并非不可违背
例如,《代码整洁之道》第五章中提到的FitNess系统
- 就是由大多数为200行、最长500行的单个文件来构造出总长约5万行的出色系统
合理地利用空白行
古诗中有留白,代码的书写中也要有适当的留白,也就是空白行
- 在每个命名空间、类、函数之间,都需用空白行隔开(应该大家在学编程之初,就早有遵守)
这条极其简单的规则极大地影响到了代码的视觉外观
- 每个空白行都是一条线索,标识出新的独立概念
其实,在往下读代码时,你会发现你的目光总停留于空白行之后的那一行
- 用空白行隔开每个命名空间、类、函数,代码的可读性会大大提升
让紧密相关的代码相互靠近
如果说空白行隔开了概念,那么靠近的代码行则暗示了他们之间的紧密联系
- 所以,紧密相关的代码应该相互靠近。
基于关联的代码分布
关系密切的概念应该相互靠近
对于那些关系密切、放置于同一源文件中的概念,他们之间的区隔应该成为对相互的易懂度有多重要的衡量标准
应该避免迫使读者在源文件和类中跳来跳去
- 变量的声明应尽可能靠近其使用位置
对于大多数短函数,函数中的本地变量应当在函数的顶部出现
团队规则
每个程序员都有自己喜欢的格式规则,但如果在一个团队中工作,就是团队说了算
一组开发者应该认同一中代码风格,每个成员应该遵守这个规则
一个好的团队应当约定与遵从一套代码规范,并且每个成员都应当采用此风格
我们希望一个项目中的代码拥有相似甚至相同的风格,像默契无间的团队所完成的艺术品
- 而不是像一大票意见相左的个人所堆砌起来的残次品
定制一套编码与格式风格不需要太多时间,但对整个团队代码风格统一性的提升,却是立竿见影的
记住,好的软件系统是由一系列风格一致的代码文件组成的
- 尽量不要用各种不同的风格来构成一个项目的各个部分,这样会增加项目本身的复杂度与混乱程度
错误处理
错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法
使用异常而非返回错误码:
- 错误码搞乱了调用者代码
先写
try-catch-finally
语句:
- 尝试编写强行抛出异常的测试,再往处理器中添加行为,使之满足测试要求
- 结果就是你要先构造
try
代码块的事务范围,而且也会帮助你维护好该范围的事务特征使用不可控异常:
- 可控异常的代价就是违反开放/闭合原则
如果你在方法中抛出可控异常,而catch语句在三个层级之上
- 你就得在catch语句和抛出异常之间的每个方法签名中声明该异常
给出异常发生的环境说明:
- 你抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所
- 在java中,你可以从任何异常里得到堆栈踪迹,然而堆栈踪迹却无法告诉你该失败操作的初衷
- 如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来
详细记录异常发生时,发生了什么,最好有详细的信息
依调用者需要定义异常类
定义常规流程:
- 业务逻辑和错误处理代码之间就会有良好的间隔
- 你来处理特例,客户代码就不用应付异常行为了,异常行为被封装到特例对象中
特例模式(
Special Case Pattern
):
- 创建一个类或配置一个对象,用来处理特例
别返回null值:
- 返回null 值会给调用者带来不方便,每次返回结果都要检查是否为null
别传递null值,不要给函数传递null 值