# 代码整洁之道
# 参考文档
# Later equals never,稍后等于永不
- 童子军军规:让营地比你来时更干净。
- 不要因为一些原因而留下糟糕的代码。
- 糟糕的代码会使得今后的开发和维护更加的困难,浪费自己时间,也同样浪费别人的时间。
# 第 1 章 整洁代码
代码是必要的,时刻都要保持整洁的代码。
什么是整洁的代码?
优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。
起码要做到以下几点:
- 能通过所有的测试。
- 没有重复代码。
- 体现系统中的全部设计理念。
- 包含尽量少的实体、比如类、方法、函数等。
即整洁的代码需要做到:
- 职责明确,没有多余
- 减少依赖,便于维护
- 高效
# 第 2 章 有意义的命名
名副其实。
一个好的命名可以省去大量的时间。
命名一定要做到,词要达意!
说起来很简单。选个好名字需要花时间,但省下的时间比花掉的多。
注意命名,一旦有好的命名,就换掉旧的。
示例:
int d;// 消失的时间,以日计。 int elapsedTimeInDays;
1
2
避免思维映射
- 例如循环中的 i、j、k 等单字母名称不是个好选择,读者必须在脑中将它映射为真实概念。最好用 filter、map 等方法代替 for循环。
避免误导
- 比如不是 List 类型,就不要用个 accountList 来命名,这样形成误导。
做有意的区分
- Variable 一词 永远不应当出现在变量名中。
- Table 一词永远不应当出现在表名中。
- NameString 会比 Name 好吗,难道 Name 会是一个浮点数不成。
- 如有一个 Customer 的类,有又一个 CustomerObject 的类。
类名应该是名词或短语
- 像 Customer,Account,避免使用 Manager,Processor,Data 或者 Info 这样的类名。
- 类名不应当是动词。
- 方法名应该是动词或动词短语。
- 如 postPayment,deletePage 或 Save,属性访问、修改和断言应该根据其值来命名,并加上 get,set,is 这些前缀。
每个概念对应一个词
- 在一堆代码中有 Controller,又有 manager,driver 就会令人困惑。
好的名字相当于为代码写了一段有用的注释。
# 第 3 章 函数
Function should do one thing. They should do it well. They should do it only.
函数只应该做一件事情,把一件事情做好,而且只由它来做这一件事情。
一个好的函数应该满足以下规则:
- 短小
- 函数的第一规则是要短小。
- 第二条规则是还要更短小。
- 例如 if、else、while 等语句,其中的代码应该只有一行。该行大抵应该是一个函数调用语句。因为块内的函数拥有较具体说明性的名称,从而增加了文档上的价值。
- 只做一件事,做好这件事
- 无副作用
- 确保函数功能就像函数名描述的一样,不要做函数名描述以外的事情。应该为起一个更能描述函数功能的函数名。
- 参数
- 尽量少的函数参数。
- 有两个参数的函数要比一元函数的难懂。
- 如果需要三个或者三个以上的参数应该封装成类了。
- 最理想的参数数量是零。
- 不要重复一段代码
- 如果一段相同的代码出现了两次,想一想是不是应该抽取方法了。
如何写出好函数:
- 分解函数,抽象分层
- 一个好的命名
- 消除重复代码
# 第 4 章 注释
Comments Do Not Make Up for Bad Code.
注释不是对劣质代码的补救。
- 与其花时间编写解释糟糕的代码注释,不如花时间清洁糟糕的代码。
- TODO 注释虽好,但也要定期查看,删除不再需要的。
- 警示,告诉别人要注意这个方法之类的。
- 放大,有的代码可能看着有点多余,但编码者当时是有他自己的考虑,这个时候需要注释下这个代码的重要性。避免后面被优化掉。
- 注释掉的代码。
- 不要的代码应该立即删掉。(有 VCS 可以找回)
# 第 5 章 格式
# 垂直格式
- 函数与函数之间留空行
- 变量声明
- 变量声明应该尽可能靠近其使用位置。
- 因为函数很短,本地变量应该在函数的顶部出现。
- 实体变量应该在内的顶部,相当于我们的 field 字段,会被使用的多。
- 变量声明应该尽可能靠近其使用位置。
- 相关函数
- 若某个函数调用了另一个函数,就应该把它们放到一起,而且调用者应该尽可能放在被调用者上面。这样程序就有个自然顺序。
- 概念相关的代码应该放到一起
- 相关性越强,彼此之间的距离就该越短。
# 横向格式
- 一行的长度不要太长
- 赋值语句两端留空
a = b;
- 不在函数名和左括号间加空格
- 因为函数与其参数密切相关。
- 缩进
- 要体现层级的概念。
# 第 6 章 对象和数据结构
- 数据抽象
- 隐藏实现关乎抽象,类并不简单地取值器和赋值器将变量推向外间,而是暴露抽象接口,以便用户无需了解数据的实现就能擦做数据本体。
- 数据、对象的反对称性
- 对象把数据隐藏于抽象之后,暴露操作数据的函数。数据结构暴露其数据,没有提供有意义的函数。回头再读一遍,留意这两种定义的本质。他们是对立的,这种差异貌似渺小,但却有深远的意义。对象与数据结构的二分原理:
- 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。
- 反过来讲也说得通: 过程式代码难以添加新的数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。
- 对象把数据隐藏于抽象之后,暴露操作数据的函数。数据结构暴露其数据,没有提供有意义的函数。回头再读一遍,留意这两种定义的本质。他们是对立的,这种差异貌似渺小,但却有深远的意义。对象与数据结构的二分原理:
- 迪米特法则
- 模块不应了解它所操作对象的内部情形。对象隐藏数据,暴露操作。者意味着对象不应通过存取其暴露其内部结构,因为这样更像是暴露而非隐藏其内部结构。
- 数据传送对象
- 最为精练的数据结构,是一个只有公共变量,没有函数的类。这种数据结构有时被称为数据传送对象,或 DTO。
# 第 7 章 错误处理
- 使用异常而非返回码。
- 在编写可能抛出异常的代码时,先写
try-Catch-Finally
语句。 - 使用不可控异常:C 使用可控异常的代价是:违反“开放/闭合原则”。对于一般性应用开发,其依赖成本要高于收益。
- 给出异常发生的环境说明:应创建信息充分的错误消息,并和异常一起传递出去,以便判断错误的来源和处所。
- 依调用者需要定义异常类(看异常如何被捕获):如打包调用 API 确保返回通用异常类型。
- 定义常规流程:创建一个类或配置一个对象,用来处理特例。
- try catch 语句块的范围不要太大,这样不利于对异常的分析。
- 别返回 null 值,这样可以减少调用者的防御性检测。
- 与其返回 null,不如抛出异常,或是返回特例对象。
- 别传递 null 值,传递 null 就要求被调用函数需要一系列防御性检测,也就意味着程序有更大可能出错。
# 第 8 章 边界
- 使用第三方代码
- 使用第三方代码时,如果有边界接口,可将其保留在类或近亲中,避免从公共 API 中返回边界接口,或者将其边界接口作为参数传给公共 API。
- 浏览和学习边界
- 在利用第三方程序包时,没有测试第三方代码的职责,但为要使用的第三方代码编写测试,可能最符合我们的利益。不要在生成代码中实验新东西,而是编写测试来遍览和理解第三方代码。
- 学习性测试的好处不只是免费
- 学习性测试是一种精确试验,帮助我们增进对 api 的理解。当第三方程序包发布了新版本,我们可以运行学习性测验,看看程序包的行为有没有改变。学习性测试确保第三方程序包按照我们想要的方式工作。一旦整合进来,就不能保证第三方代码总与我们的需要兼容。如果第三方程序包的修改与测试不兼容,我们也能马上发现。
- 整洁的边界
- 在使用我们控制不了的代码时,必须加倍小心,确保未来的修改代价不会太大。边界上的代码需要清晰的分隔和定义了期望的测试。
# 第 9 章 单元测试
- TDD 三定律
- 定律一,在编写不能通过的单元测试前,不可编写生成代码。
- 定律二,只可编写刚好无法通过的单元测试,不能编译也不算通过。
- 定律三,只可编写刚好足以通过当前失败测试的生成代码。
- 保持测试整洁
- 可读性,可读性,可读性。
- 如何做到可读,那就是要保证测试代码同其他代码一样,明确,简洁,足具表达力,这也是这本书一直强调的事情。
- 测试代码和生产代码一样重要。需要被思考,设计和照料。应该和生产代码一样整洁。
- 每个测试一个断言
- 在尽可能减少每个概念的断言数量的同时,最好能做到每个测试函数中只测试一个概念。
- F.I.R.S.T 原则
- 快速 (Fast) 测试应该够快。
- 独立 (Independent) 测试应该相互独立。
- 可重复 (Repeatable) 测试应当可在任何环境中重复通过。
- 自足验证 (Self-Validating) 测试应该有布尔值输出。
- 及时 (Timely) 测试应及时编写。
# 第 10 章 类
- 类的组织
- 遵循标准的 JAVA 约定,类应该从一组变量列表开始。
- 变量顺序:
- 公共静态常量
- 私有静态变量
- 私有实体变量
- 很少有公共变量,公共函数应该在变量列表后面。公共函数调用的私有工具函数紧随在该公共函数的后面。
- 类应该短小
- 单一权责原则 (SRP)
- 类或模块应有且只有一条加以修改的理由。系统应该有许多短小的类而不是巨大的类组成。
- 内聚
- 内聚性高:类中的方法和变量相互依赖、互相结合成一个逻辑整体。
- 当类丧失了内聚性,就需要拆分它。将大函数拆分为许多小函数,往往也是将类拆分为许多个小类的时机。程序会更有组织,也会拥有更为透明的结构。
- 为了修改而组织
- 在整洁的系统中,对类进行组织,以降低修改的风险(通过扩展而非修改现有的代码来添加新的特性)。
- 隔离修改
- 遵循依赖倒置原则(DIP),应该依赖于抽象而非具体细节。
# 第 11 章 系统
Complexity kills. It sucks the life out of developers, it makes products difficult to plan, build and test.
复杂要人命,它消磨开发者的生命,让产品难于规划、构建和测试。
- 将构造和使用分开的方法:
- 分解 main,将系统中的全部构造过程搬迁到 main 或者 main 模块中。
- main 函数创建对象,再将对象传递给应用程序,应用程序只管使用,对构造一无所知。
- 如果应用程序需要负责确定何时创建对象,可以创建抽象工厂,让应用程序控制实体创建的时机。
- 依赖注入,控制反转 IoC 是依赖管理的手段,它将应用需要的依赖对象的创建权责从对象中拿出来,放在一个专注于此事的对象中,并通过依赖注入(赋值器)将依赖对象传递给应用。
- 分解 main,将系统中的全部构造过程搬迁到 main 或者 main 模块中。
- 扩容:
- 面向方面编程(AOP),Java 中三种方面和类似方面的机制:代理,纯 AOP 框架,AspectJ。
- java 代理:适用于简单情况,如在单独对象或类中包装方法调用。代码量和复杂度是代理的两大弱点。
- 纯 Java AOP 框架,如 Spring AOP、JBoss AOP。
- AspectJ:提供将方面作为模块构造处理支持的 Java 扩展。
- 面向方面编程(AOP),Java 中三种方面和类似方面的机制:代理,纯 AOP 框架,AspectJ。
- 最佳系统架构由模块化的关注面领域组成,每个关注面均用纯 Java(或其他语言)对象实现。不同领域之间用最不具有侵害性的方面或类方面工具整合起来。这种架构能测试驱动,就像代码一样。
- 拥有模块化关注面的 POJO 系统提供的敏捷能力,允许我们基于最新的知识做出优化的、时机刚好的决策。决策的复杂性也降低了。
- 领域特定语言(DSL)允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用 POJO 来表达。
# 第 12 章 迭进
简单设计规则
运行所有测试
- 不可测试的系统不可验证,不可验证的系统,绝不能部署。
不可重复
通过抽取或是模板方法整合重复代码。
示例:
int size(); bool isEmpty();
1
2这两个方法可以分别实现,但可以在
isEmpty
中使用size
消除重复。bool isEmpty() { return size() == 0; }
1
2
3
表达力
- 选用好的名称来表达。
尽可能少的类
# 第 13 章 并发编程
- 为什么要并发
- 并发是一种解耦策略,它帮助我们把做什么(目的)和何时(时机)做分解开。
- 解耦目的与时机能明显地改进应用程序的吞吐量和结构。
- 单线程程序许多时间花在等待 web 套接字 I/O 结束上面,通过采用同时访问多个站点的多线程算法,就能改进性能。
- 常见的迷思和误解
- 并发总能改进性能
- 只在多个线程或处理器之间能分享大量等待时间的时候管用。
- 编写并发程序无需修改设计
- 可能与单线程系统的设计极不相同。
- 在采用 web 或 ejb 容器时,理解并发问题并不重要。
- 并发总能改进性能
- 有关编写并发软件的中肯的说法
- 并发会在性能和编写额外代码上增加一些开销。
- 正确的并发是复杂的,即使对于简单的问题也是如此。
- 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待。
- 并发常常需要对设计策略的根本性修改。
- 并发防御原则
- 单一权责原则
- 限制数据作用域
- 使用数据副本
- 线程应尽可能独立
- 了解 Java 库
- 使用类库提供的线程安全群集
- 使用 executor 框架(executor framework)执行无关任务
- 尽可能使用非锁定解决方案
- 有几个类并不是线程安全的
- 了解执行模型
- 警惕同步方法之间的依赖
- 保持同步区域微小
- 很维编写正确的关闭代码
- 平静关闭很难做到,常见问题与死锁有关,线程一直等待永远不会到来的信号。
- 建议:尽早考虑关闭问题,尽早令其工作正常。
- 测试线程代码
- 建议:编写有潜力曝露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败。
- 将伪失败看作可能的线程问题
- 线程代码导致“不可能失败的”失败,不要将系统错误归咎于偶发事件。
- 先使非线程代码可工作
- 不要同时追踪非线程缺陷和线程缺陷,确保代码在线程之外可工作
- 编写可插拔的线程代码,能在不同的配置环境下运行。
- 编写可调整的线程代码
- 允许线程依据吞吐量和系统使用率自我调整。
- 运行多于处理器数量的线程
- 任务交换越频繁,越有可能找到错过临界区域导致死锁的代码。
- 在不同平台上运行
- 尽早并经常地在所有目标平台上运行线程代码。
- 装置试错代码
- 增加对
Object.wait()
、Object.sleep()
、Object.yield()
、Object.priority()
等方法的调用,改变代码执行顺序,硬编码或自动化。
- 增加对
# 第 14 章 逐步改进
# 第 15 章 JUnit 内幕
# 第 16 章 重构 SerialDate
# 第 17 章 味道与启发
# 附录 A 并发编程 II
# 讨论区
由于评论过多会影响页面最下方的导航,故将评论区做默认折叠处理。