代码整洁之道-读书笔记
《代码整洁之道》(《Clean Code》)是几乎每一个对编程境界有追求、有志于改善代码质量的编程者,都应该阅读的一本好书。
这本书提出了一个观点:
而且或许你会发现,若你一直保持输出整洁代码的习惯,长期来看,会让你的整体效率和代码质量大大提升。
一、糟糕代码,是如何产生的
最初的问题来了,我们都不喜欢坏代码,为何坏代码还总会产生?
抛开编程者本身技艺的问题,答案也许是想要快点完成任务,项目进度赶着时间,或许我们只是不耐烦再继续做这个需求,渴望它能早点结束。
很多时候,会有一堆事情压着你去完成,你觉得自己需要赶紧弄完手上的东西,好接着做下一件工作,所以,为了走捷径,为了特定实现功能而强加的糟糕的代码,就悄悄地诞生了。我们都曾经瞟一眼自己亲手造成的混乱,决定弃之不顾,走向新的一天,等有朝一日再来清理。
然而中国古训有云,“明日复明日,明日何其多”,勒布朗(LeBlanc)法则也表示,“稍后等于永不。”你会发现,因为各种各样的原因,很多时候你根本都不会(没时间)去整理之前的代码。所以,正如本书作者Robert C.Martin(Uncle Bob),在SD West 2007技术大会上所说的,“那堆“可以运行”的乱麻程序,就在我们的眼皮底下慢慢腐坏。”
我们知道,坏代码会污染环境,最后会坏掉整个项目。保持整洁的习惯,发现脏代码就要及时纠正。花时间保持代码代码整洁,不但有关效率,还有关项目的生存。
二、为什么好代码会很快变质?
为什么好代码会很快变质?一般情况下,需求一直在变更、需求变化违背了初期设计、进度太紧是导致好代码变质的诱因。
多数的产品经理想要好的代码,即便他们总是痴缠于进度,他们会奋力的维护进度和需求。而程序员们则当以同等的热情捍卫代码的整洁性和可扩展性。
举个栗子,如果你是医生,病人在请求给给他们做手术前别洗手,因为那会花太多时间,你会照办吗?本该是病人说了算,但医生却绝对应该拒绝遵从。为什么?因为医生比病人更了解疾病个感染的风险。医生如果按病人说的办,就是一种不专业的态度。
同理,程序员遵从不了解混乱代码风险的产品经理(策划)的意愿,都是不专业的做法。
三、程序员的基础价值谜题
程序员都面临着一种基础价值的谜题。有那么几年经验的开发者都知道,之前的混乱拖了自己的后腿,但开发者背负期限的压力,只好制造混乱。简言之,他们没花时间让自己做得更快。
而其实真正专业的人士明白,这道谜题第二部分说错了,制造混乱无助于赶上期限,混乱只会立刻拖慢你,叫你错过期限,赶上期限的唯一方法——做得更快的唯一方法——就是始终尽可能保持代码的整洁。
四、大师们眼中的整洁代码
到底什么是整洁的代码?有多少程序员,就有多少定义。 “大师级程序员把系统当故事来讲,而不是当做程序来写”。就让我们一起看看经验丰富的大师级程序们都是如何定义整洁代码的。
Bjarne Stroustrup ,C++语言之父, The C++ Programming Language(中译版《C++程序设计语言》)一书作者:
“我喜欢优雅和高效的代码,代码逻辑应直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;一句某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。”
Grady Booch,Object Orient Analysis and Design with Application(中译版《面向对象程序分析与设计》) 一书作者:
“整洁的代码简单直接,整洁的代码如优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。”
Michael Feathers,Working Effectively withLegacy programming(中译版《修改代码的艺术》)一书的作者:
“整洁的代码总是看起来想是某位特别在意它的人写的,几乎没有改进的余地,代码作者署名都想到了,如果你企图改进它,总会回到原点,赞叹某人留给你的代码——全心投入到某人留下的代码。”
Ron Jeffries,Extreme Programming Installed(中译版《极限编程实施》)以及Extreme Programming Adventures in (C#中译版《C#极限编程探险》)作者:
“减少重复的代码,提高表达力,提早构建简单抽象,这就是我写整洁代码的方法。”
五、编写代码的难度,取决于周边代码的阅读难度
编写代码的难度,取决于周边代码的阅读难度。何出此言?因为各种实践与统计表明,在项目里开发新功能的过程中,阅读之前代码与书写新的代码,花费的时间比例超过10:1,新写代码时,我们一直在读旧代码。既然比例如此之高,我们就应该让读的过程变得轻松,即便那会使得编写过程更难。
所以说,编写代码的难度,取决于周边代码的阅读难度,想要快速实现需求,想要快速完成任务,想要轻松的写代码,先让你书写的代码整洁易读吧。
六、让代码比你来时更干净
我们知道,光把代码写好可不够,必须时时保持代码整洁,我们都见过代码随着时间的流逝而腐坏。我们应该更积极地阻止腐坏的发生。
借用美国童子军的一条简单的军规,应用到我们的专业领域:
“让营地比你来时更干净。”
那么可以同样对编程领域这样说:
“让代码比你来时更干净。”
也就是说,如果我们每次签入时,代码都比签出时干净,那么代码就不会腐坏。这就是我们需要遵从的代码整洁之道。
有意义的命名-高质量代码的命名法则
1 名副其实
名副其实说起来貌似很简单,但真正做起来,似乎没那么容易。选个好名字要花一些时间,但其实选好名字之后省下来的时间,要比之前选名字时花掉的时间多得多。
我们一旦发现有更好的名称时,就应该换掉之前的旧名称,这样做读你代码的人(包括你自己),都会很开心。
一个好的变量、函数或类的名称应该已经几乎答复了所有的大问题。它应该告诉你,这个名称所代表的内容,为什么会存在,做了什么事情,应该如何用等。
如果一个名称需要注释来补充才能让大家明白其真正含义,那其实就不算是名副其实。(并不是说不需要注释,恰如其分的注释是程序员让自己代码锦上添花的好方法,关于注释的一些注意事项,稍后会有文章专门涉及。)
举个栗子:
以下的这句代码里的d就不算是个好命名。名称d什么都没说,它没引起我们对时间消逝的感觉,更别说单位是天了:
int d; // elapsed time in days||经过了几天时间
// 我们应该选择这样的指明了计量对象和计量单位的名称:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
2 避免造成误导
我们应该避免留下隐藏代码本意的错误线索,也应该避免使用与本意相悖的词。例如,别用accountList来指一组账号,除非它真的是List类型,用accountGroup、bunchOfAccounts,或者直接用accounts,都是更好的选择。
尽量提防长得太像的名称。想区分XYZControllerForEfficientHandlingOfStrings
和XYZControllerForEfficientStorageOfStrings
,会花费我们太多的时间。因为这两个词,实在太相似了。
以同样的方式拼写出同样的概念才是信息,拼写前后不一致就是误导。
3 尽量做有意义的区分
1.尽量避免使用数字系列命名(a1、a2…….aN)。这样的名称纯属误导,因为很多时候完全没有提供正确的信息,没有提供导向作者意图的线索。
2.废话是另一种没有意义的区分。如果我们有一个Product类,还有一个ProductInfo或ProductData类,那么他们的名称虽然不同,但意思却无区别。这里的Info、Data就像a、an和the一样,是意义含混的废话。
注意,只要体现出有意义的区分,使用a、the这样的前缀就没错。例如,将a用在域内变量,把the用于函数参数。
4 尽量使用读得出来的名称
我们要使用读得出来的名称。如果名称读不出来,讨论的时候就会不方便且很尴尬,甚至让旁人觉得很蠢。
例如,变量名称是beeceearrthreecee,讨论的时候读起来简直像没吃药。
5 尽量使用可搜索的名称
单字母和数字常量有个问题,就是很难再一大篇文字中找出来。
找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好找得多,而列表中也只剩下了体现我们意图的名称。
6 取名不要绕弯子
我们取名的时候要避免思维映射,不应当让读者在脑中把你的名称翻译为他们熟知的名称,也就是说取名不要绕弯子,而是要直白,直截了当。
在多数情况下,单字母不是个好的命名选择,除非是在作用域小、没有名称冲突的地方,比如循环。循环计数器自然有可能被命名为i,j或k(最好别用字母l),这是因为传统上我们惯用单字母名称做循环计数器。
程序员通常都是聪明人,聪明人有时会借助脑筋急转弯炫耀其聪明。而聪明程序员和专业程序员之间的区别在于,专业程序员了解,明确就是王道。专业的程序员善用其能,能编写出其他人能理解的代码。
7 类名尽量用名词
类名尽量用名词或名词短语,比如Customer, WikiPage,Account, 或 AddressParser。
类名最好不要是动词。
8 方法名尽量用动词
方法名尽量用动词或动词短语。比如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,强制使用这种命名手段。
9 每个概念对应一词,并一以贯之
我们需给每个概念选一个词,并且一以贯之。
例如,使用fetch、retrieve和get来给在多个类中的同种方法命名,你怎么记得住哪个类中是哪个方法呢?
同样,在同一堆代码中混用controller、manager,driver,就会令人困惑。DeviceManager和Protocol-Controller之间有何根本区别?为什么不全用controller或者manager?他们都是Driver吗?这就会让读者以为这两个对象是不同的类型,也分属不同的类。
所以,对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音。
10 通俗易懂
我们应尽力写出易于理解的变量名,把代码写得让别人能一目了然,而不必让人去非常费力地去揣摩其含义。我们想要那种大众化的作者尽责地写清楚的通俗易懂的畅销书风格,而不是那种学者学院风的晦涩论文写作风格。
11 尽情使用解决方案领域专业术语
记住,只有程序员才会读你写的代码。所以,尽管去用那些计算机科学(Computer Science,CS)领域的专业术语、算法名、模式名、数学术语。
对于熟悉访问者(Visitor)模式的程序来说,名称AccountVisitor富有意义。给技术性的事物取个恰如其分的技术性名称,通常就是最靠谱的做法。
12 添加有意义的语境
很少有名称是可以自我说明的。所以,我们需要用有良好命名的类,函数或名称空间来放置名称,给读者提供语境。若没能提供放置的地方,还可以给名称添加前缀。
举个栗子,假如我们有名为firstName、lastName、street、houseNumber、city、state和zipcode的变量。当他们搁一块儿的时候,的确是构成了一个地址。不过,假如只是在某个方法中看到一个孤零零的state呢?我们会推断这个变量是地址的一部分吗?
我们可以添加前缀addrFirstName、addrLastName、addrState等,以此提供语境。至少,读者可以知道这些变量是某个更大变量的一部分。当然,更好的方案是创建名为Address的类。这样,即便是编译器也会知道这些变量是隶属于某个更大的概念了。
另外,只要短名称足够好,对含义的表达足够清除,就要比长名称更合适。添加有意义的语境甚好,别给名称添加不必要的语境。
13 避免使用编码
使用编码 是一个不好的选择,把类型或者作用域添加的变量名中,无疑给变量名添加了额外的信息,增加了解码的负担。
string m_name;
应该避免这样的命名方式,下面python语言为例
m_list;
arr_list;
14 小结
起名是一门艺术,能够给一个变量起一个非常合适的名称,确实要花很多功夫。这个名字本身要有自我描述性,还不能有二义性,还要尽可能短,就这三点要求估计可以让人头脑发麻了。确实如果有一个好的名称,确实给后续维护减少不少的麻烦。所以在起名字这件事情上多花点功夫吧。
函数书写准则
《代码整洁之道》这本书提出了一个观点:代码质量与其整洁度成正比,干净的代码,既在质量上可靠,也为后期维护、升级奠定了良好基础。书中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。
1.短小
短小函数的第一规则。第二条规则还是要更短小。 每行都不应该有150个字符那么长,函数也不该有100行那么长。函数的缩进层级不该多于一层。
2.单一职责 只做一件事
只做一件事,如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。
判断函数是否不止做了一件事,还有就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。只做一件事的函数无法被合理地切分为多个区段。
设计模式中有单一职责原则,我们可以把这条原则理解为代码整洁之道中的函数单一职责原则。
3.函数参数尽可能的少
每个函数一个且仅有抽象层级:我们想要让代码拥有自顶向下的阅读顺序。
让代码读起来像是一系列自顶向下的 起头段落是保持抽象层级协调一致的有效技巧。把读代码想象成 读报纸。 总分结构, 或者总分总结构
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() // 这样来调用。
4.分隔指令与询问
函数要么做什么事(do
),要么回答什么事(boolean
),但二者不可兼得。
public boolean set(String attributer, String value);
用户调用
if (set("username","frank")) {//...
}
看到这个代码,首先想 是否能够成功设置username 为frank, 还是要问, username 是否已经之前被设置过为frank了呢? 对看代码的人会比较困惑。
所以 解决方案 就是 把做什么和是否成功分开。 防止发生混淆
if (exists("username")){
setAttribute("username","frank")
}
5.使用异常替代返回错误码
使用异常替代返回错误码:
- 使用异常替代错误码返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化;
- 抽离try/catch代码块:try/catch代码块丑陋不堪;最好把try和catch代码块的主体部分抽离出来,另外形成函数。
- 错误处理就是一件事:如果try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
- Error.java依赖磁铁:返回错误码通常暗示某处有个类或是枚举定义了所有错误码。使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署。
来看一个例子: 这里使用了很多的 嵌套的 if语句
这种很难让人理解,嵌套层级特别多,看起来很复杂,对于这种代码 最好直接抛出异常就可以,直接 使用 try 语句
然后捕获具体的异常就可以了。而不是一层,一层的进行判断,嵌套起来,之后就很难维护这样的代码。
# ... 省略
def fake_md(self):
if collection_id:
subscription_id = self.create_subscription(collection_id)
logger.info("successful: subscription create")
created_flag = self._ensure_subscription_created(subscription_id)
logger.info("successful: subscription flag ")
if created_flag:
confirm_flag = self._confirm_subscription()
if confirm_flag:
state_flag = self._ensure_subscription_state_created(subscription_id)
if state_flag:
logger.info("successful: subscription state create")
else:
raise ValueError("failure: subscription state create ")
else:
raise ValueError(
"failure: subscription can't receive message")
else:
raise ValueError("failure: subscription create failure")
else:
raise ValueError("create collection failure")
对于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)
}
6.别重复自己
Don’t repeat yourself
别重复自己:重复可能是软件中一邪恶的根源。
其实可以这样说,重复可能是软件中一切邪恶的根源,许多原则与实践规则都是为控制与消除重复而创建的。仔细想一想,面向对象编程是如何将代码集中到基类,从而避免了冗余的。而面向方面编程(Aspect Oriented Programming)、面向组件编程(Component Oriented Programming)多少也是消除重复的一种策略。这样看来,自子程序发明以来,软件开发领域的所有创新都是在不断尝试从源代码中消灭重复。
重复而啰嗦的代码,乃万恶之源,我们要尽力避免。
结构化编程:每个函数、函数中的每个代码块都应该有一个入口,一个出口,遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句。对于小函数,这些规则助益不大,只有在大函数中,这些规则才会有明显的好处。
8.如何写出这样的函数
如何写出这样的函数?
先想什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直到达到你心中的样子。
最好的情况要配上一套单元测试,覆盖每一行丑陋的代码。 然后开始打磨这些代码,分解函数,修改名称,消除重复。最后要保持测试可以顺利通过。 我并不是一开始就按照规则写函数。 我想没有人做得到。
9.小结
编程艺术是且一直就是语言设计的艺术。大师级程序员把系统当做故事来讲,而不是当做程序来写。如果你遵循这些规则,函数就会短小,有个好名字,而且被很好地归置。真正的目的在于讲述系统的故事,而你编写的函数必须干净利落的拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。程序员,其实是故事家。
代码的注释准则
”不要给糟糕的代码加注释 – 重新写吧。 " – Brain W.Kernighan
注释不能美化糟糕的代码
写注释的最常见的动机之一 就是 糟糕的代码存在。 其实带有少量注释而有表达力的代码,要比大量注释的零碎而复杂的代码像样得多。 与其花时间写注释,不如花时间清洁那堆糟糕的代码。
用代码来阐述
如果一段代码本身就具有表现力, 完全没有必要写注释。 从变量名或者函数名本身就具有自描述性,这样的代码就好一些。
好注释例子
-
法律信息:可指向一份标准许可或其他外部文档
-
提供信息的注释
-
对意图的解释
-
阐释
-
警示:例如为什么用某种设计模式
-
TODO 注释
-
放大
注释可以用来放大某种不合理之物的重要性。
-
公共API中的doc
坏注释例子
- 喃喃自语
- 多余注释
- 误导性注释
- 循轨式注释
- 日志式废话
- 废话注释:用整理代码的决心替代创造废话的冲动吧
- 可怕的废话
- 能用函数或变量时就别用注释
- 位置标记:///(少用,只在有价值的地方用)
- 括号后面的注释
- 归属与命名
- 注释掉的代码
- html注释
- 非本地信息
- 信息过多
- 不明显的联系
- 函数头
- 非公共API中的javadoc
代码格式准则
1 像报纸一样一目了然
想想那些阅读量巨大的报纸文章。你从上到下阅读。在顶部,你希望有个头条,告诉你故事主题,好让你决定是否要读下去。第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事细节。接着读下去,细节渐次增加,直至你了解所有的日期、名字、引语、说话及其他细节。
优秀的源文件也要像报纸文章一样。名称应当简单并且一目了然,名称本身应该足够告诉我们是否在正确的模块中。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。
2 恰如其分的注释
带有少量注释的整洁而有力的代码,比带有大量注释的零碎而复杂的代码更加优秀。
我们知道,注释是为代码服务的,注释的存在大多数原因是为了代码更加易读,但注释并不能美化糟糕的代码。
另外,注意一点。注释存在的时间越久,就会离其所描述的代码的意义越远,越来越变得全然错误,因为大多数程序员们不能坚持(或者因为忘了)去维护注释。
当然,教学性质的代码,多半是注释越详细越好。
3 合适的单文件行数
尽可能用几百行以内的单文件来构造出出色的系统,因为短文件通常比长文件更易于理解。当然,和之前的一些准则一样,只是提供大方向,并非不可违背。
例如,《代码整洁之道》第五章中提到的FitNess系统,就是由大多数为200行、最长500行的单个文件来构造出总长约5万行的出色系统。
4 合理地利用空白行
古诗中有留白,代码的书写中也要有适当的留白,也就是空白行。
在每个命名空间、类、函数之间,都需用空白行隔开(应该大家在学编程之初,就早有遵守)。这条极其简单的规则极大地影响到了代码的视觉外观。每个空白行都是一条线索,标识出新的独立概念。
其实,在往下读代码时,你会发现你的目光总停留于空白行之后的那一行。用空白行隔开每个命名空间、类、函数,代码的可读性会大大提升。
5 让紧密相关的代码相互靠近
如果说空白行隔开了概念,那么靠近的代码行则暗示了他们之间的紧密联系。所以,紧密相关的代码应该相互靠近。
来看下 这段代码作为 反例
public class ReporterConfig
{
/**
* The class name of the reporter listener
*/
private String m_className;
/**
* The properties of the reporter listener
*/
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property)
{
m_properties.add(property);
}
}
这段代码看起来也很简单。 但是这段代码看起来有什么问题呢?
首先 就是命名问题, 字段名 不应该带有编码信息。 m_className
,m_properties
这些命名就太好。 第二注释 隔离了类的 私有属性。 这些注释显得有些多余。
改动之后的代码:
public class ReporterConfig
{
private String className;
private List<Property> properties = new ArrayList<Property>();
public void addProperty(Property property)
{
properties.add(property);
}
}
6 基于关联的代码分布
关系密切的概念应该相互靠近。对于那些关系密切、放置于同一源文件中的概念,他们之间的区隔应该成为对相互的易懂度有多重要的衡量标准。应该避免迫使读者在源文件和类中跳来跳去。变量的声明应尽可能靠近其使用位置。
对于大多数短函数,函数中的本地变量应当在函数的顶部出现。
7 团队规则
每个程序员都有自己喜欢的格式规则,但如果在一个团队中工作,就是团队说了算。 一组开发者应该认同一中代码风格,每个成员应该遵守这个规则。
一个好的团队应当约定与遵从一套代码规范,并且每个成员都应当采用此风格。我们希望一个项目中的代码拥有相似甚至相同的风格,像默契无间的团队所完成的艺术品,而不是像一大票意见相左的个人所堆砌起来的残次品。
定制一套编码与格式风格不需要太多时间,但对整个团队代码风格统一性的提升,却是立竿见影的。
记住,好的软件系统是由一系列风格一致的代码文件组成的。尽量不要用各种不同的风格来构成一个项目的各个部分,这样会增加项目本身的复杂度与混乱程度。
8 小结 让代码不仅仅是能工作
代码的格式关乎沟通,而沟通是专业开发者的头等大事,所以良好代码的格式至关重要。
或许之前我们认为"让代码能工作"才是专业开发者的头等大事。然而,《代码整洁之道》这本书,希望我们能抛弃这个观点。
让代码能工作确实是代码存在的首要意义,但作为一名有追求的程序员,请你想一想,今天你编写的功能,极可能在下一版中被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。原始代码修改之后很久,其代码风格和可读性仍会影响到可维护性和扩展性。即便代码已不复存在,你的风格和律条仍影响深远。
当有人在阅读我们的代码时,我们希望他们能为其整洁性、一致性和优秀的细节处理而震惊。我们希望他们高高扬起眉毛,一路看下去,希望他们感受能到那些为之劳作的专业人士们的优秀职业素养。但若他们看到的只是一堆由酒醉的水手写出的鬼画符,那他们多半会得出结论——这个项目的其他部分应该也是混乱不堪的。
所以,各位,在开发过程中请不要仅仅是停留在"让代码可以工作"的层面,而更要注重自身输出代码的可维护性与扩展性。请做一个更有追求的程序员。
错误处理
错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法。
-
使用异常而非返回错误码:错误码搞乱了调用者代码
-
先写try-catch-finally语句:尝试编写强行抛出异常的测试,再往处理器中添加行为,使之满足测试要求。结果就是你要先构造try代码块的事务范围,而且也会帮助你维护好该范围的事务特征。
-
使用不可控异常:可控异常的代价就是违反开放/闭合原则
如果你在方法中抛出可控异常,而catch语句在三个层级之上,你就得在catch语句和抛出异常之间的每个方法签名中声明该异常
-
给出异常发生的环境说明:你抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所。在java中,你可以从任何异常里得到堆栈踪迹,然而堆栈踪迹却无法告诉你该失败操作的初衷。如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来。
[注释] 详细记录异常发生时,发生了什么,最好有详细的信息 -
依调用者需要定义异常类
-
定义常规流程:业务逻辑和错误处理代码之间就会有良好的间隔;你来处理特例,客户代码就不用应付异常行为了,异常行为被封装到特例对象中。
特例模式(Special Case Pattern):创建一个类或配置一个对象,用来处理特例。
-
别返回null值
[注释] 返回null 值会给调用者带来不方便,每次返回结果都要检查是否为null
别传递null值,不要给函数传递null 值
小结
将错误处理隔离对待,独立于主要逻辑之外,就能写出强固而整洁的代码,做到这一步,我们就能单独处理它。
整洁类的书写准则
1 合理地分布类中的代码
一般情况下,我们遵循变量列表在前,函数在后的原则。
类应该从一组变量列表开始。若有公有静态常量,应该最先出现,然后是私有静态变量,以及公有变量,私有变量。尽可能少的出现公有变量。
公共函数应该出现在变量列表之后。我们喜欢把由某个公共函数调用的私有工具函数紧跟在公共函数后面。
这样是符合自定向下的原则,让程序读起来像一篇报纸文章。
2 尽可能保持类的封装
我们喜欢保持变量和工具函数的私有性,但不执著于此。有时,我们需要用到protected变量或者工具,比如让测试可以访问到。然而,我们会尽可能使函数或变量保持私有,不对外暴露太多细节。放松封装,总是下策。
3 类应该短小
正如之前关于函数书写的论调。类的一条规则是短小,第二条规则还是要短小。
和函数一样,马上有个问题要出现,那就是,多小合适呢?
对于函数,我们通过计算代码行数来衡量大小,对于类,我们采用不同的衡量方法,那就是权责(responsibility)。
单一权责(Single Responsibility Principle,SRP)认为,类或模块应有且只有一条加以修改的理由。
4 合理提高类的内聚性
我们希望类的内聚性保持在较高的水平。
何为类的内聚性?类的内聚性就是类中变量与方法之间的依赖关系。类中方法操作的变量越多,就越黏聚到类上,就代表类的内聚性高。
类应该只有少量的实体变量,类中的每个方法都应该操作一个或者多个这种变量。通常而言,如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。一般来说,创建这种极大化的内聚类不可取,也不可能。
我们只希望内聚性保持在较高的水平。内聚性高,表示类中方法和变量相互依赖,相互结合成一个逻辑整体。
public class Stack
{
private int topOfStack = 0;
List<Integer> elements = new LinkedList<Integer>();
public int size()
{
return topOfStack;
}
public void push(int element)
{
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty
{
if (topOfStack == 0)
throw new PoppedWhenEmpty();
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}
这个类非常内聚,在三个方法中,仅有size()方法没有使用所有的两个变量。
注意,保持函数和参数短小的策略,有时候会导致为一组子集方法所用的实体变量增加。我们应该尝试将这些方法拆分到两个或者多个类中,让新的类更为内聚。
5 隔离修改
需求会改变,所以代码也会改变。在面向对象入门知识中我们学习到,具体类包含实现细节(代码),而抽象类则呈现概念。依赖于具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来隔离这些细节带来的影响。
把可能变化的部分抽象成接口,然后由子类进行实现,而不是直接实现具体的类。
设计模式本身的思想就是 把经常变化的部分和不经常变化的部分 分开, 这样 两者不应该相互影响。 通过接口通信, 不依赖具体的类,这样如果需要 子类有不同的变化,子类只需要 改动就可以,基类就不需要改变。 其实就是要依赖抽象而不应该依赖具体。
味道与并发-作者经验分享
代码修改的原因,每次修改代码,可以记录自己为啥要这样修改,总结原因, 给出读代码闻起来不舒服的味道。 于是就有了以下清单。
1 注释:
-
不恰当的信息:注释只应该描述有关代码和设计的技术性信息。
-
废弃的注释:过时、无关或不正确的注释就是废弃的注释。
-
冗余注释:注释应该谈及代码自身没提到的东西
-
糟糕的注释:值得编写的注释,也值得好好写。
-
注释掉的代码
2 环境:
- 需要多步才能实现的构建:构建系统应该是单步的小操作。
- 需要多步才能做到的测试:应当能够发出单个指令就可以运行全部单元测试。能够运行全部测试是如此基础和重要,应该快速、轻易和直截了当地做到。
3 函数
- 过多的参数,没参最好
- 输出参数:输出参数就违反直觉。如果函数非要修改什么东西的状态不可,就修改它所在对象的状态好了。
- 标识参数:布尔值参数大声宣告函数做了不止一件事。
- 死函数:永不被调用的方法应该丢弃。别害怕删除函数。
4 一般性问题
-
一个源文件中存在多种语言:理想的源文件包括且只包括一种语言,应该尽力减少源文件中额外语言的数量和范围。
-
明显的行为未被实现:最小惊异原则–函数或类应该实现其他程序员有理由期待的行为。
-
不正确的边界行为:别依赖直觉。追索每种边界条件,并编写测试。
-
忽视安全:关闭失败测试、告诉自己过后再处理,这和假装刷信用卡不用还钱一样坏。
-
重复:核心原则。每次看到重复代码,都代表遗漏了抽象。
-
在错误的抽象层级上的代码:只与细节实现有关的常量、变量或工具函数不应该在基类中出现。
-
基类依赖于派生类:例外情况是派生类数量严格固定。 将概念分解到基类和派生类的最普遍的原因就是 较高层级基类概念可以不依赖较低层级派生类概念。通常来说基类对派生类应该一无所知。
-
信息过多:设计良好的模块有着非常小的接口,让你能事半功倍。设计良好的接口并不提供许多需要依靠的函数,所以耦合度也较低。设计低劣的接口提供大量你必须调用的的函数,耦合度较高。隐藏你的数据。隐藏你的工具函数。隐藏你的常量和你的临时变量。不要创建拥有大量方法或大量实体变量的类。不要为子类创建大量受保护变量和函数。尽量保持接口紧凑。通过限制信息来控制耦合度。
-
死代码:死代码就是不执行的代码。
-
垂直分隔:变量和函数应该在靠近被使用的地方定义。
-
前后不一致:从一而终,小心选择约定,一旦选中,就小心持续遵循。
-
混淆视听: 没有实现的默认构造器
-
人为耦合:不互相依赖的东西不该耦合。
-
特性依恋:类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。
-
“选择算子”参数:“选择算子”参数只是一种避免把大函数切分为多个小函数的偷懒做法。选择算子不一定是boolean类型,可能是枚举元素、整数或任何一种用于选择函数行为的参数。
-
晦涩的意图:代码要尽可能具有表达力。
-
位置错误的权责:软件开发者做出的最重要决定之一就是在哪里放代码。(最小惊异原则)代码应该放在读者自然而然期待它所在的地方。
-
不恰当的静态方法:恰当的静态方法不应在单个实体上操作。
-
应当使用解释性变量:让程序可读的最有力的方法之一就是将计算过程打散成在用有意义的单词命名的变量中放置的中间值。
-
函数名称应该表达其行为:
-
理解算法:“可以工作”是不行的,必须知道解决方案是正确的。
-
应当把逻辑依赖改为物理依赖:依赖者模块不应对被依赖者模块有假定。
-
应当用多态替代if/else或switch/case: 在使用if/else或switch/case前,先考虑使用多态。
-
遵循标准约定:
-
用命名常量替代魔术数:
-
准确:
-
结构甚于约定:坚守结构甚于约定的设计决策。命名约定很好,但却次于强制性的结构。
-
封装条件:应该把解释了条件意图的函数抽离出来。
-
避免否定性条件:
-
函数只做一件事:
-
不要掩蔽时序耦合:通过创建时序队列暴露时序耦合。
-
应当封装边界条件:
-
函数应当只在一个抽象层级上:(拆分不同抽象层级是重构的最重要的功能之一)
-
应当在较高层级放置可配置数据
-
避免传递浏览:确保模块只了解其直接协作者。
5 Java 相关
- 通过使用通配符避免过长的导入清单:(这一项由IDE来实现)
- 不要继承常量:
- 常量 VS. 枚举 :优先用枚举
- 采用描述性名称:
- 名称应与抽象层级相符
- 尽可能使用标准命名法:
- 无歧义的名称:
- 避免编码:不应在名称中包括类型或作用范围信息。
- 名称应该说明副作用:名称应该说明函数、变量或类的一切信息。
6 测试
- 测试不足
- 使用覆盖率工具:覆盖率工具能汇报你测试策略中的缺口。
- 别略过小测试
- 被忽略的测试就是对不确定事物的疑问:需求不明确而不能确定某个行为细节,可以用注释掉的测试或者用@Ignore标记的测试来表达我们对于需求的疑问。
- 测试边界条件
- 全面测试相近的缺陷
- 测试失败的模式有启发性
- 测试覆盖率的模式有启发性
- 测试应该快速
总结
文中提到了很多观点 是帮助我构建更加简洁的代码,更加有维护性的代码。提出了很多建议,书中还有一些实战的内容,对java 一些库进行重构的部分,这里没有记录。 作者认为:
参考文档
浅墨_毛星云 原文链接 https://blog.csdn.net/poem_qianmo/article/details/52144086
浅墨_毛星云 原文链接 https://blog.csdn.net/poem_qianmo/article/details/52079996
浅墨_毛星云 原文链接 https://blog.csdn.net/poem_qianmo/article/details/52204224
浅墨_毛星云 原文链接 https://blog.csdn.net/poem_qianmo/article/details/52268975
浅墨_毛星云 原文链接 https://blog.csdn.net/poem_qianmo/article/details/52344732
KevinQ 原文链接 https://juejin.cn/post/6844903715464282119
代码整洁之道书籍 clean code