第一章 整洁代码
什么是整洁代码?
整洁的代码应当代码正确,简单直接,应有单元测试和验收测试,清晰易读,只做一件事,尽量少的依赖关系;
“如果每个例程都让你感到深含己意,那就是整洁代码”。
第二章 有意义的命名
2.1 名副其实
如果名称需要注释来补充,那就不算时名副其实。
2.2 避免误导
避免留下掩藏代码本意的错误线索;避免使用与本意相悖的词。
如:sco 不能做变量名,它是UNIX平台专有名称。
2.3 做有意义的区分
如果名称相异,其意思应该不同。
如:accountData与account在变量上没有区别,不应该以其表达不同的意思。
2.4 使用读的出来的名称
名称应该是恰当的英文单词而不是自造词。
2.5 使用可搜索的名称
如:用MAX_CLASSES_PER_STUDENT代替数字7。
2.6 避免使用编码
避免把类型和作用域编进名称里面;
不必使用m_前缀来标明成员变量;
2.7 避免思维映射
不应当让读者在脑中把你的名称翻译为他们熟知的名称。
如:循环计数器避免使用字母l
2.8 类名和方法名
类名和对象名应该是名词或名词短语;
方法名应当是动词或动词短语。
2.9 每个概念对应一个词
给每个抽象概念选一个词,并且一以贯之。
2.10 别用双关
避免将同一单词用于不同的目的。
2.11 使用解决方案领域名称
尽量用计算机科学术语、算法名、模式名、数学术语。
如果不能用程序员熟悉的术语来命名,就采用所涉问题领域的名称来命名。
添加有意义的语境。
第三章 函数
函数是所有程序中的第一组代码。
3.1 短小
函数的第一规则是要短小。20行封顶最佳,每个函数都依序把你带到下一个函数。
If语句、else语句、while语句等,其中的代码块应该只有一行。函数的缩进层级不该多于一层或两层。
3.2 只做一件事
函数应该做一件事。做好这件事,只做这一件事。
如果函数只是做了函数名下同一抽象层的步骤,则函数还是做了一件事。
判断函数是否不止做了一件事的方法:看该函数是否还能拆出一个函数,该函数不仅只是单纯的重新诠释其实现。
3.3 每个函数一个抽象层级
自顶向下读代码:向下规则。让每个函数后面都跟着位于下一抽象层的函数。
3.4 switch语句
当无法避免使用switch语句时,应确保每个switch都埋藏在较低的抽象层,而且永不重复。
3.5 使用描述性的名称
“如果每个例程都让你感到深含己意,那就是整洁代码”。
长而具有描述性的名称,要比短而令人费解的名称好;长而具有描述性的名称,要比描述性的长注释好。
尝试不同的名称,找到最具描述性的那一个。
选择描述性的名称能理清你关于模块的设计思路。
命名方式要保持一致。
3.6 函数参数
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。
标识参数实则是表明该函数不止做一件事。eg:
void check(boolean flag){...}
两个、三个或三个以上参数,就应该将其封装。
可以使用参数列表代替多参数。eg:
void monad(Integer... args); void dyad(String name,Integer args); void triad(String name, int count,Integer... args);
3.7 使用异常替代返回错误码
抽离try/catch代码块。把try和catch代码块的主体部分抽离出来,另外形成函数。
错误处理就是一件事。
错误码应该是一个枚举类。
3.8 结构化编程
每个函数、函数中的每个代码块都应该有一个入口、一个出口。
只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,甚至还比单入单出原则更具表达力。
好的代码并不是一开始就必须要遵循这些规则,而是在不断打磨中,分解函数、修改名称、消除重复中逐渐形成的。
第四章 注释
真正的注释是你想办法不去写的注释。
4.1 注释不能美化糟糕的代码
与其花时间编写解释糟糕的注释,不如花时间清洁代码。
4.2 用代码来阐述
有时代码不足以解释其行为,因此只需要创建一个描述与注释所言同一事物的函数即可。
4.3 提供信息的注释
有时用注释来提供基本的信息。
注释可以用来放大某种看来不合理之物的重要性。
4.4 坏注释
多余的注释、误导性注释、循规式注释、日志式注释、废话注释、注释的代码、注释信息过多、不明显的联系。
第五章 格式
5.1 格式的目的
代码格式不可忽略,代码格式关乎沟通,而沟通是专业开发者的头等大事。
5.2 垂直格式
5.2.1 像报纸学习
内容的布局可以参考报纸的不知布局
5.2.2 概念间垂直方向上的区隔
每行展现一个表达式或一个字句,每组代码行展示一条完整的思路,这些思路用空白行区隔开来。
5.2.3 垂直方向上的靠近
紧密相关的代码应该相互靠近。
5.2.4 垂直距离
变量申明应尽可能靠近其使用位置;实体变量应该在类的顶部声明;相关函数应该尽可能放在一起;概念相关的代码应该放在一起;被调用的函数应该放在执行调用的函数下面。
5.3 横向格式
水平方向上的区隔与靠近;水平对其;缩进
5.4 团队规则
在团队中应该使用团队里的规则。
第六章 对象和数据结构
6.1 数据抽象
不暴露数据细节,更愿意以抽象形态表达数据。
6.2 数据、对象的反对称性
过程式代码难以添加新数据结构,因为必须修改所有的函数。面向对象代码难以添加新函数,因为必须修改所有类。
6.3 得墨忒耳律
模块不应了解它所操作对象的内部情形。对象隐藏数据,暴露操作。
6.4 数据传送对象(DTO)
DTO是非常有用的数据结构,尤其是在与数据库通信、或解析套间字传递的消息场景中,是将原始数据转换为数据库的翻译过程。
第七章 错误处理
7.1 使用异常而非返回码
7.2 先写try-catch-finally 语句
尝试编写强行抛出异常的测试,即构造try代码块的事务范围,再往处理器中添加行为。
7.3 使用不可控异常
可控异常的代价是违反了开闭原则。
7.4 给出异常发生的环境说明
抛出的每个异常都应该提供足够的环境说明,以便判断错误的来源处所。传递足够的信息给catch块,并记录下来。
7.5 调用者需要定义异常类
如果你想要捕获某个异常,并且放过其他异常,就是用不同的遗产类。
7.6 定义常规流程
使用特例模式(SPECIAL CASE PATTERN),创建一个类或配置一个对象,用来处理特例,将异常行为封装到特例对象中。
// 不要返回 null
class MissingCustomer implements Customer {
public Long getId() {
return null;
}
}
// 不要返回特殊值
class MissingCustomer implements Customer {
public Long getId() {
return 0;
}
}
————————————————改造如下:
class MissingCustomer implements Customer {
public Long getId() throws MissingCustomerException {
throw new MissingCustomerException();
}
}
————————————————
7.7 别返回null值
与其每一行都要检查是否与null值,不如用新方法将其打包这个方法,在新方法中抛出异常或返回特例对象。
7.8 别传递null值
除非API要求你传null值,否则要尽量传递null值作为参数。
第八章 边界
8.1 使用第三方代码
通常情况在使用第三方代码时,应用自己逻辑将其封装,避免暴露其他接口影响本身的逻辑。避免从公共API中返回边界接口,或将边界接口作为参数传递给公共API。
8.2 浏览和学习边界
学习性测试:编写测试来遍历和理解第三方代码。
学习性测试毫无成本。
学习性测试不仅仅免费。
学习性测试确保第三方程序包按照我们想要的方式工作。
8.3 使用尚不存在的代码
将已知和未知代码分隔开的边界。可以采用适配器模式
8.4 整洁的边界
边界上的代码需要清晰的分割和定义了期望的测试。
第九章 单元测试
9.1 TDD三定律
定律一:在编写不能通过的单元测试前,不可编写生产代码
定律二:只可编写刚好无法通过的单元测试,不能编译也算不通过
定律三:只可编写刚好足以通过当前失败测试的生产代码。
9.2 保持测试整洁
测试代码和生产代码一样重要。
测试带来一切好处,单元测试让你的代码可扩展、可维护、可复用。
9.3 整洁的测试
整洁测试三要素:可读性、可读性、可读性。(明确、整洁和足够的表达力)
面向特定领域的测试语言。
9.4 每个测试一个断言
符合given-when-then约定
每个测试函数只测一个概念。
9.5 F.I.R.S.T
整洁的测试还应遵循5项规则:
快速(FAST) 测试应足够快
独立(Independent) 测试应当相互独立
可重复(Repeatable) 测试应当可在任何环境中重复通过
自足验证(Self-Validating) 测试应该有Boolean值输出
即使(Timely) 测试应及时编写
第十章 类
10.1 类的组织
类应当从一组变量列表开始。如果有公共静态变量,应当先出现。然后再是私有静态变量,以及私有实体变量。符合自定向下的原则。
10.2 类应该短小
无法为某个类名以精确的名称,这个类就长了。
类应满足一下规则:
10.2.1 单一权责原则
类和模块应有且只有一条加以修改的理由。
10.2.2 内聚
类应该只有少量的实体变量。
10.2.3 保持内聚性就会得到许多短小的类
10.3 为了修改而组织
隔离修改。 具体类包含实现细节(代码),而抽象类则只呈现概念。
第十一章 系统
11.1 将系统的构造与使用分开
软件系统应将启始过程和启始过程之后的运行逻辑分开,在启始过程中构建应有对象,也会存在相互缠结的依赖关系。
分解main。 控制流程。
工厂: 有时程序也需要负责确定何时创建对象。
使用依赖注入和控制反转可以实现分离构造与使用。
11.2 扩容
迭代和增量敏捷的精髓即是,我们应该只去实现今天的用户故事,然后重构,明天在扩展系统、实现新的用户故事。
测试驱动开发、重构以及它们打造出的整洁代码,在代码层面保证了这个过程的实现。
11.3 Java代理
Java代理适用于简单的情况,例如在单独的对象或类中包装方法调用。JDK 提供的动态代理仅能与接口协同工作。
11.4 测试驱动系统架构
最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java(或其他语言)对象实现。不同的领域之间用最不具有侵害性的方面或类方面工具整合起来。这种架构能测试驱动,就像代码一样。
模块化和关注面切分成了分散化管理和决策。
第十二章 跌进
12.1 通过跌进设计达到整洁目的
遵循以下规则,设计就能变得“简单”:
1)运行所有的测试
2)不可重复
3)表达了程序员的意图
4)尽可能减少类和方法的数量
第十三章 并发编程
“对象是过程的抽象。线程是调度的抽象。”
13.1 并发是一种解耦策略
并发会在性能和编写额外代码上增加一些开销;
正确的并发是复杂的,即便对于简单的问题也是如此;
并发常常需要对设计策略的根本性修改。
13.2 并发防御原则
单一权责原则:方法/类/组件应当只有一个修改理由。
建议:分离并发相关代码与其他代码。
限制数据作用域:限制临界区的数量(LOCK)
建议:谨记数据封装;严格限制对可能被共享的数据的访问。
使用数据副本
线程应尽可能地独立:每个线程处理一个请求,不共享源头的数据。
建议:尝试将数据分解到可被独立线程(可能在不同的处理器上)操作的独立子集。
13.3 了解Java库
使用类库提供的线程安全集群;
使用executor框架(executor framework)执行无关任务;
尽可能使用非锁定解决方案;
主要线程不安全的类。
13.4 保持同步区域微小
锁会带来延迟和额外开销,应尽可能少的设计临界区。
13.5 测试线程代码
编写有潜力暴露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。建议如下:
将伪失败看作可能的线程问题;
先使非线程代码可工作;
编写可插拔的线程代码;
运行多与处理器数量的线程;
在不同平台上运行;
调整代码并强迫错误发生。
第十四章 味道与启发
14.1 注释
C1:不恰当的注释
注释只应该描述有关代码和设计的技术性信息。
C2:废弃的注释
过时、无关或不正确的注释就是废弃的注释。
C3: 冗余的注释
如果注释描述的是某种充分自我描述了的东西,那么注释就是多余的。
i++; // increment i
C4:糟糕的注释
如果要编写一条注释,就花时间保证写出最好的注释。字斟句酌。
C5:注释掉的代码
不要保留注释掉的代码。
14.2 环境
E1: 构建系统时应当能够用单个命令签出系统,并用单个指令构建它。
E2: 能够通过单个指令就可以运行全部的单元测试。
14.3 函数
F1:函数的参数应该少。最多不超过三个。
F2:不应该输出参数
F3:不应出现类似布尔值的标识参数。
F4:永不被调用的代码不应该保留。
14.4 一般性问题
G1:理想的源文件应该只包括一种语言。
G2:函数或类应该实现其他程序员有理由期待的行为。在函数或类中应该返回的值域、相关的逻辑实现都要包括。
G3:别依赖直觉。追索每种边界条件,并编写测试。
G4:不要忽视编译器的警告。
G5:重复的代码即遗漏了抽象,应该重复代码通过一些设计模式进行消除和提炼。
G6:所有较低层级概念放在派生类中,所有较高层级概念放在基类中。
G7:较高层级基类概念不依赖可以不依赖于较低层级派生类概念。
G8:类中的方法越少越好,函数知道的变量越少越好,类拥有的实体变量越少越好。
G9:不要写或删除系统中不执行的代码(死代码)。
G10:变量和函数应该靠近被使用的地方定义。
G11:对变量命名时,保持前后一致。(对HttpServeletResponse 命名为respose时,其他地方的HttpServeletResponse 也要相应的叫response)
G12:不要写没有意义的变量、注释和不调用的函数等。
G13:不互相依赖的东西不该耦合。
G14:类中的方法只应对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。
G15:将选择算子参数函数(参数为Boolean或枚举等)切分为多个小函数。
G16:代码要尽可能具有表达力。
G17:清楚代码放置的位置。
G18:善用静态方法(静态函数不应有多态的行为)
G19:让程序可读的最有力的方法之一就是将计算过程打散成在用有意义的单词命名的变量中放置中间值。
G20:函数的名称应该表达其行为。
G21:理解函数的算法,然后重构函数,得到某种整洁而足具表达力、清楚呈现如何工作。
G22:把逻辑依赖改为物理依赖。
G23:用多态代替IF/ELSE或Switch/Case
G24:遵循团队中基于行业规范通用的一套编码标准。
G25:用命名常量代替魔术数。
G26:在代码中做决定时,确认自己足够准确。
G27:坚守结构基于约定的设计决策。
G28:把解释了条件的函数抽离出来。
将if (timer.hasExpired() && !timer.isRecurrent()) 改造为 if (shouldBeDeleted(timer))
G29:避免否定性条件
将if (!buffer.shouldNotCompact() ) 改造为 if (buffer.shouldCompact() )
G30:函数只做一件事。
G31:掩蔽时序的耦合。(通过函数的返回作为下一个函数的参数,可以保证其有序性)
G32:构建代码需要理由,而且理由应当与代码结构相契合。
G33:封装边界条件
G34:函数应该只在一个抽象层级上。
G35:在较高层级放置可配置数据。
G36:避免传递浏览。(在A和B相互协作时,不应出现a.getB().getC().doSomething())
14.5 Java相关规范
J1:通过使用通配符避免过长的的导入清单。(当引入很多类时,且在一个包中,可以使用 import package.* ;)
J2:不要继承常量。
J3:使用枚举代替静态常量。
14.6 名称
N1:命名三思而行,采用描述性名称。
N2:名称应与抽象层级相符。
N3:尽可能使用标准命名法
N4:名称应无歧义
N5:作用范围较大时应选用较长的名称
N6:不应在名称中包括类型或作用范围信息。如m_,f或vis_前缀的名称。
N7:名称应该说明函数、变量或类的一些信息,不应用名称掩蔽副作用。
14.7 测试
T1:测试应覆盖所有的条件
T2:使用覆盖率工具能更容易找到测试不足的模块、类和函数。
T3:不要忽略小测试
T4:可以用注释掉的测试或者用@Ignore标记的测试来表达我们对与需求的疑问。
T5:注意测试边界条件
T6:当函数中出现一个缺陷时,应全面测试哪个函数。
T7:可以通过找到测试用例失败的模式来诊断问题所在。
T8:竭尽所能让测试足够块。
