第一章 整洁代码
我们都曾经说过有朝一日再回头清理代码。当然,在那些日子里,我们都没听过勒布朗(LeBlanc)法则:稍后等于永不(Later equals never)。
程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法。
混乱只会立刻拖慢你,叫你错过期限。赶上期限的唯一方法---做得快的唯一方法---就是始终尽可能保持代码整洁。
破窗理论
。窗户破损了的建筑让人觉得似乎无人照管。于是别人也再不关心。他们放任窗户继续破损。最终自己也参加破坏活动,在外墙上涂鸦,任垃圾堆积。一扇破损的窗户开辟了大厦走向倾颓的道路。
整洁的代码只做好一件事
。(
每个函数、每个类和每个模块都全神贯注于一事,完全不受四周细节的干扰和污染
)
读与写花费时间的比例超过10:1
。写新代码时,我们一直在读旧代码。
既然比例如此之高,我们就想让读的过程变得轻松,即便那会使得编写过程更难。没可能光写不读,所以使之易读实际也使之易写。
不读周边代码的话就没法写代码。编写代码的难度,取决于读周边代码的难度。要想干得快、要想早点做完、要想轻松写代码,先让代码易读吧。
美国童子军一条简单的军规,应用到我们的专业领域:
让营地比你来时更干净。
-
我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。
--
Bjarne Stroustrup,C++语言发明者,C++ Programming Language(中译版《C++程序设计语言》)一书作者。
-
整洁的代码应可由作者之外的开发者阅读和增补。它应当有单元测试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽量少的API。代码应通过其字面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达。
-- "老大"Dave Thomas,OTI公司创始人,Eclipse战略教父。
-
整洁代码就是作者着力照料的代码。有人曾花时间让它保持简单有序。他们适当地关注到了细节。他们 在意 过。
-
简单代码,依其重要顺序:
1. 能通过所有测试;
2.
没有重复代码;
3.
提高表达力
;
4.
提早构建简单抽象
;
5.
体现系统中的全部设计理念;
6. 包括尽量少的实体,比如类、方法、函数等。
-
如果每个例程都让你感到 深合己意 ,那就是整洁代码。如果代码让编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代码。
-
漂亮的代码让编程语言像是 专为解决那个问题 而存在!
心得:
-
考虑项目进度与需求,从经理的角度看待问题
第二章 有意义的命名
命名规则:
1.名副其实
-
变量、函数或类的名称应该已经答复了所有的大问题。
-
只要简单改一下名称,就能轻易知道发生了什么。
int d; //消逝的时间,以日计
修改为:
int elapsedTimeInDays;
-
无法第一时间理解代码的问题不在于代码的简洁度,而是在于代码的模糊度:即上下文在代码中未被明确体现的程度。
-
选个好名字需要花时间,但省下的时间比花掉的多。注意命名,一旦有好的命名,就换掉旧的。
2.避免误导
-
程序员必须应当避免使用与本意相悖的词。
-
专有名称、关键字(如list)对程序员有特殊意义。尽量避免命名。
例:别用accountList来指称一组账号,除非它真的是List类型。如果包纳账号的容器并非真是个List,就会引起错误的判断。所以,用accountGroup或bunchOfAccounts,甚至直接用accounts都会好一些。避免使用有歧义的字母与数字。
-
提防使用不同之处较小(不明显)的名称。
-
以同样的方式拼写出同样的概念才是信息。拼写前后不一致就是误导。
3.做有意义的区分
-
如果名称必须相异,其意思也应该不同。
-
命名要有实质意义上的区分。
-
以数字系列命名(a1、a2,……aN)纯属误导,完全没有提供正确信息;没有提供导向作者意图的线索。
不规范示例:
Public static void copyChars(char a1[],char a2[]){
for(int i=0;i<a1.length;i++){
a2[i]=a1[i];
}
}
-
要区分名称,就要以 读者能鉴别不同之处的方式来区分。
-
废话是另一种没意义的区分。
例:假设你有一个Product类。如果还有一个ProductInfo或ProductData类,它们的名称虽然不同,意思却无区别。Info和Data就像a、an和the一样,是意义含混的废话。
-
废话都是冗余。
例:Variable一词永远不应当出现在变量名中。Table一词永远不应当出现在表名中。NameString会比Name好吗?难道Name会是一个浮点数不成?
4.
使用读得出来的名称
-
单词能读得出来。
5.
使用可搜索的名称
-
长名称胜于短名称,搜得到的名称胜于用自造编码代写就的名称。
-
单个字母或者数字常量是很难在一大堆文章中找出来。
-
窃以为单字母名称仅用于短方法中的本地变量。名称长短应与其作用域大小相对应。
for (int j=0; j<34; j++) {
s += (t[j]*4)/5;
}
修改为:
int realDaysPerIdealDay = 4;
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;
}
6.避免使用编码
-
把类型或作用域编进名称里面,徒然增加了解码的负担。带编码的名称通常也不便发音,容易打错。
-
人们会很快学会无视前缀(或后缀),只看到名称中有意义的部分
7.
避免思维映射
-
不应当让读者在脑中把你的名称翻译为他们熟知的名称。
-
读者必须在脑中将变量名映射为真实概念。
-
明确是王道,编写其他人能理解的代码 。
8.类名
-
类名和对象名应该是名词或名词短语,如Customer、WikiPage、Account和AddressParser。
-
避免使用Manager、Processor、Data或Info这样的类名。
-
类名不应当是动词。
9.方法名
-
方法名应当是动词或动词短语,如postPayment、deletePage或save。
-
属性访问器、修改器和断言应该根据其值命名,并依Javabean标准 加上get、set和is前缀。
10.别扮可爱
-
名称解释够准确无误;别用花哨的名字。
-
言到意到。意到言到。
11.每个概念对应一个词
-
给每个抽象概念对应一个词,并且一以贯之。
-
切记同时使用多个词对应一个概念(例:切记同时出现controller、manager、driver等)
12.别用双关语
-
避免将同一单词用于不同目的。同一术语用于不同概念,基本上就是双关语了。
13.
使用解决方案领域名称
-
只有程序员才会读你的代码。所以,尽管用那些计算机科学(Computer Science,CS)术语、算法名、模式名、数学术语。
14.
使用源自所涉问题领域的名称
-
如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。
-
优秀的程序员和设计师,其工作之一就是分离 解决方案领域 和 问题领域 的概念。
-
与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称。
15.
添加有意义的语境
-
你需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境。
-
如果没这么做,给名称添加前缀 就是最后一招了。
16.
不要添加没用的语境
-
只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。
-
正确是命名的要点。
-
设若有一个名为"加油站豪华版"(Gas Station Deluxe)的应用,在其中给每个类添加GSD前缀就不是什么好点子。
17.最后的话
-
取好名字最难的地方在于需要良好的描述技巧和共有文化背景。
心得:
-
通过看代码的名称,就能知道代码具体要干什么
-
状态量(STATUS_VALUE, FLAGGED)大写
第三章 函数
1.短小
-
函数的第一规则是要短小。第二条规则是还要更短小。
2.只做一件事
-
函数应该做一件事。做好这件事。只做这一件事。
3.每个函数一个抽象层级
-
自顶向下读代码:向下规则
-
程序就像是一系列TO起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续TO起头段落。
4.使用描述性的名称
-
命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。
5.函数参数
-
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。
-
有足够特殊的理由才能用三个以上参数(多参数函数)-所以无论如何也不要这么做。
-
如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
6.使用异常替代返回错误码
-
Try/catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外形成函数。这意味着如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
}
-
使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署。
7.如何写出这样的函数
-
写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。
-
我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。 然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。 最后,遵循本章列出的规则,我组装好这些函数。 我并不从一开始就按照规则写函数。我想没人做得到。
-
不过永远别忘记,真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。
第四章 注释
-
优秀的代码不需要注释,本身就能说明问题。
-
唯一真正好的注释是想办法不去写注释。
-
好的注释比不上干净的代码。
// Check to see if the employee is eligible for full benefits
if (( employee.flags & HOURLY_FLAG ) && ( employee.age > 65 ))
修改后:
if ( employee.isEligibleForFullBenefits() )
-
很多时候,简单到只需要创建一个描述与注释所言同一事物的函数即可,用代码解释大部分的意图。
-
如果决定写注释,就要花必要的时间确保写出最好的注释。
第五章 格式
-
选用一套管理代码格式的简单规则,然后贯彻这些规则
-
源文件要像报纸那样。名称应当简单且一目了然,名称本身应该足够告诉我们是否在正确的模块中,源文件最顶部应该给出最高层次概念和算法,细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。
-
紧密相关的代码应该相互靠近。
-
变量声明应该尽可能靠近其使用位置。
-
实体变量应该在类的顶部声明。
-
相关函数,应该把它们放到一起,并且调用函数应该尽可能放在被调用函数上面。
-
概念相关的代码应该放到一起,相关性越强,彼此之间的距离就该越短,相关性可能来自执行相似操作的一组函数。
-
根据运算符优先级格式化代码:
-
优先级高的不加空格,优先级低的添加空格。
private static double determinant(double a, double b, double c) {
return b*b - 4*a*c
}
-
保持循环体为空时的格式缩进。
第六章 对象和数据结构
-
过程式代码(使用数据结构的代码)便于在不该懂既有数据结构的前提下添加新函数;面向对象代码便于在不改动既有函数的前提下添加新类。
-
过程式代码难以添加新数据结构,因为必须修改所有函数;面向对象代码难以添加新函数,因为必须修改所有类。
-
得墨忒耳律:模块不应该了解它所操作对象的内部情形。
-
类C的方法f只应该调用以下对象的方法:
- C;
- 由f创建的对象;
- 作为参数传递给f的对象;
- 由C的实体变量持有的对象。
- 方法不应调用由任何函数返回的对象的方法。即:只和朋友谈话,不与陌生人谈话。
第七章 错误处理
-
如果错误处理搞乱了代码逻辑,就是错误的做法。
-
遇到错误时,最好抛出一个异常。
-
尝试编写强行抛出异常的测试,再往处理器中添加行为,使之满足测试要求。结果就是要先构造try代码块的事务范围,维护该范围的事务特征。
-
在应用程序中定义异常类时,最重要考虑的时它们 如何被捕获。
-
特例模式:创建一个类或配置一个对象,用来处理特例。客户代码就不用应付异常行为了,异常行为被封装到特例对象中。
-
别返回null值。更别传递null值。
List<Employee> employees = getEmployees();
if(employees != null) { // employees可能为null
for(Employee e : employees) {
totalPay += e.getPay();
}
}
修改后:
public List<Employee> getEmployees(){
.....
return Collections.emptyList(); // Java collection.empty() 返回一个预定义不可变列表,避免NullPointException出现
}
List<Employee> employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}
-
如果在调用第三方API中可能返回null值的方法,可以用新方法打包返回null值的方法,在新方法中抛出异常或返回特例对象。
第八章 边界
-
将外来的代码干净利落的整合进自己的代码中。
-
如果使用类似Map这样的边界接口,就把它保留在类或近亲类中。避免从公共API中返回边界接口,或将边界接口作为参数传递给公共API。
Map sensors = new HashMap();
Sensors s = (Sensor)sensors.get(sensorId); // 强制类型转换
修改为:
Map<Sensor> sensors = new HashMap<Sensor>(); // 泛型
Sensor s = sensor.get(sensorId);
修改为:
public class Sensors{
private Map sensors = new HashMap();
public Sensor getById(String id){
return (Sensor) sensors.get(id); // 转换与类型管理在Sensors类内部处理
}
}
-
边界上的代码需要清晰的分割和定义了期望的测试。
第九章 单元测试
TDD三定律:
在编写不能通过的单元测试前,不可编写生产代码。 只可编写刚好无法通过的单元测试,不能编译也算不通过。 只可编写刚好足以通过当前失败测试的生产代码。
-
测试代码和生产代码一样重要。它需要被思考、被设计和被照料,该像生产代码一般保持整洁。
-
整洁的测试三要素:可读性、可读性、可读性。
-
每个测试函数只测试一个概念。
整洁的测试遵循5条规则:
快速,测试应该够快。 独立,测试应该相互独立。应该可以单独运行每个测试,以及任何顺序运行测试。 可重复,测试应当可在任何环境中重复通过。应该能够在生产环境、测试环境中运行测试。 自足验证,测试应该有布尔值输出。 及时,测试应及时编写,单元测试应该恰好在使其通过的生产代码之前编写。
第十章 类
-
类应该短小;
-
类的名称应该描述其权责(方法的数量);
-
类长短的标准:能否为某个类命以精确的名称;(包括含义模糊的词:processor、manager、super)
-
单一权责原则:类或模块应该有且只有一条加以修改的理由。
-
系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。
保持内聚性就会得到许多短小的类:
当把含有许多变量的大函数拆解成单独的函数,要拆解的代码使用了该函数声明中的多个变量,是否必须将这多个变量都作为参数传到新函数中?完全没必要!只要将多个变量提升为类的实体变量,完全无需传递任何变量就能拆解代码,应该很容易将函数拆分成为小块。
-
通过降低连接度,类就遵循了依赖倒置原则:即类应当依赖于抽象(接口)而不是依赖于具体细节。
第十一章 系统
-
软件系统应将启动过程和启动过程之后的运行时逻辑分离开,在启动过程中构建应用对象,也会存在相互缠结的依赖关系。
-
将构造与使用分开的方法之一就将全面构造过程搬迁到main或main的模块中。
-
使用抽象工厂模式让应用自行控制并创建对象,但构造的细节却隔离于应用程序代码之外。
-
依赖注入,控制反转实现分流构造与使用。
第十二章 跌进
-
跌进设计:
-
运行所有测试;
-
不可重复;
-
表达了程序员的意图;
-
尽可能减少类和方法的数量。
-
以上规则按其重要程度排列。
-
测试消除了对清理代码就会破坏代码的恐惧。
-
应用简单设计后三条规则:
-
消除重复、保证表达力、尽可能减少类和方法的数量。
-
第十三章 并发编程
-
并发会在性能和编写额外代码上增加一些开销;
-
正确的并发是复杂的,即便对于简单的问题也是如此;
-
并发缺陷并非总能重视,所以常被看作偶发事件而忽略,未被当作真的缺陷看待;
-
并发常常需要对设计策略的根本性修改。
-
并发防御原则
-
单一权责原则:建议:分离并发相关代码与其他代码。
-
限制数据作用域:(防止多线程共享对象的同一字段互相干扰,synchronized保护一块使用共享对象的临界区)谨记数据封装;严格限制对可能被共享的数据的访问。
-
使用数据复本:避免共享数据的方法之一就是开始就避免共享数据。
-
线程尽可能独立:尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集(不与其他线程共享数据)。
-
-
建议:检读可用的类。对于Java掌握java.util.concurrent, java.util.concurrent.atomic 和 java.util.concuttent.locks。
-
执行模型:学习这些基础算法,理解其解决方案。
-
生产者-消费者模型;
-
读者-作者模型;
-
宴席哲学家模型;
-
-
测试线程代码:
-
编写有潜力暴露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。
-
建议:
- 将伪失败看作可能的线程问题;-- 不要将系统错误归咎于偶发事件。
- 先是非线程代码可工作;-- 不要同事追踪非线程缺陷和线程缺陷。确保代码在线程之外可工作。
- 编写可插拔的线程代码;-- 编写可在数个配置环境下运行的线程代码。
- 编写可调整的线程代码;-- 在不同配置环境下检测系统性能,允许线程数量可调整与线程变动,允许线程根据吞吐量和系统使用率自我调整。
- 运行多于处理器数量的线程;-- 防止频繁切换任务导致死锁。
- 在不同的平台上运行;-- 今早并经常的在所有目标平台上允许线程代码。
- 调整代码并强迫错误发生;-- 捕捉线程中罕见的错误,采用硬编码与自动化的方法装置代码,改变代码执行顺序。
- 如果花点时间装置代码,能极大的提升发现错误代码的机会。
- 只要采用了整洁的做法,做对的可能性就有翻天覆地的提高。
第十四章 逐步改进
-
要编写整洁代码,必须先写肮脏代码,然后清理它。
-
避免程序在改进时无法恢复,采用测试驱动开发的规程,保证系统始终能运行。
-
解决之道就说保持代码持续整洁和简单,用不让腐坏有机会开始。