《代码整洁之道》读书笔记
2005年,Elisabeth递给我一条绿色腕带,上面写着Test Obsessed沉迷测试的字样,我高兴地带上。我发现自己无法取下腕带,不仅是因为腕带很紧,而且那也是精神上的紧箍咒。那腕带就是我职业道德的宣告,也是我承诺尽己所能写出最好代码的提示。写代码时,我用余光瞟见它。它一直提醒我,我做了写出整洁代码的承诺。
1 怎样才算整洁?
1.1 花时间保持代码的整洁,不但有关效率,还有关生存。
1.2 本该是病人说了算;但医生却绝对应该拒绝遵从。为什么?因为医生比病人更了解疾病。医生如果按病人说的办,就是一种不专业的态度。同理,程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法。
1.3 写整洁代码需要遵循大量的小技巧,贯彻刻苦习得的整洁感。这种代码感就是关键所在。有些人生而有之。有些人费点劲才能得到。
2 变量
2.1 变量、常量、方法、类的命名,一旦发现更好的名称,就换掉旧的。
2.2 变量名称的长短应与其作用域大小相对应。单字母名称仅用于短方法中的本地变量,在代码多处使用的变量或常量,应赋予便于搜索的名称。
2.3 关于名称的编码:Fortran语言要求首字母体现出类型,导致了编码的产生。BASIC早期版本只允许使用一个字母加上一位数字。匈牙利语标记法将这种态势愈演愈烈。那时候编译器并不做类型检查,程序员需要匈牙利语标记法来帮助自己记住类型。现代编程语言具有更丰富的类型系统,编译机也记得并强制使用类型。Java程序员不需要类型编码,对象都是强类型。
3 方法
3.1 函数的第一规则是要短小,第二规则还要更短小。函数应该做一件事。做好这件事。只做这一件事。
3.2 我看惯了Swing程序中长度数以里计的函数。但这个程序中每个函数都只有两行、三行或者四行。每个函数都一目了然。每个函数都只说一件事。而且,每个函数都依序把你带到下一个函数。这就是函数应该达到的短小程度!
3.3 问题在于很难知道那件该做的事是什么。如果函数只是做了该函数名下同一抽象层上的步骤,则该函数还是只做了一件事。
3.4 大师级程序员把系统当做故事来讲,而不是当做程序来写。
4 注释
4.1 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。注释是一种失败。如果你发现自己需要写注释,再想想看是否有办法翻盘,用代码来表达。每次用代码表达,你都该夸奖一下自己。
5 对象和数据结构
5.1 我们不想其他人依赖这些私有变量。我们还想在心血来潮时能自由修改其类型或实现。那么为什么还有那么多程序员给对象自动添加赋值器和取值器,将私有变量公之于众,如同它们根本就是公共变量一般呢?
5.2 要以最好的方式呈现某个对象包含的数据,需要做严肃的思考。傻乐着乱加赋值器和取值器是最坏的选择。
5.3 对象把数据隐藏于抽象之后,暴露操作数据的函数。数据结构暴露其数据,不提供有意义的函数。
5.4 对象与数据结构之间的二分原理:过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。
5.5 得墨忒耳率:对象不该通过存取器暴露其内部结构。以下代码被称为火车失事。
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
6 异常处理
6.1 在很久以前,许多语言都不支持异常。你要么设置一个错误标识,要么返回给调用者检查的错误码。
public void sendShutDown() { DeviceHandle handle = getHandle(DEV1); if (handle != DeviceHandle.INVALID) { retrieveDeviceRecord(handle); if (record.getStatus() != DEVICE_SUSPENDED) { pauseDevice(handle); clearDeviceWorkQueue(handle); closeDevice(handle); } else { logger.log("Device suspended. Unable to shut down"); } } else { logger.log("Invalid handle for : " + DEV1.toString()); } }
6.2 Try/catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外形成函数。
public void sendShutDown() { try { tryToShutDown(); } catch (DeviceShutDownError e) { logger.log(e) } } private void tryToShutDown() { DeviceHandle handle = getHandle(DEV1); DeviceRecord record = retrieveDeviceRecord(handle); pauseDevice(handle); clearDeviceWorkQueue(handle); closeDevice(handle); } private DeviceHandle getHandle(DeviceID id) { ... throw new DeviceShutDownError("Invalid handle for:" + id.toString()); ... }
6.3 使用不可控异常。可控异常的代价是违反开闭原则。如果你在方法中抛出了可控异常,而catch语句在三个层级之上,你就得在catch语句和抛出异常处之间的每个方法签名中声明该异常。
6.4 遵循前面的建议,在业务逻辑和错误处理代码之间就会有良好的区隔。大量代码会开始变得像是整洁而简朴的算法。不过有时你也许不愿这样做,把错误检测推到了程序的边缘地带。有种手法叫特例模式,创建一个类用来处理特例。
7 边界
7.1 第三方程序包和框架提供者追求普适性,这样就能在多个环境中工作,吸引广泛的用户。而使用者想要满足特定需求的接口。这种张力会导致系统在边界上出现问题。
7.2 不要在生产代码中试验新东西,而是编写测试来浏览和理解第三方代码。Jim Newkirk把这叫做学习型测试。
8 单元测试
8.1 测试必须随生产代码的演进而修改。测试越脏就越难修改。测试代码越纠结,你就越可能花更多时间塞进新测试。修改生产代码后,旧测试就会开始失败。
8.2 无论架构多有扩展性,无论设计划分得有多好,没有了测试,你就很难改动,因为你担忧改动会引入不可预知的缺陷。
8.3 测试应该够快。如果测试运行缓慢,你就不会想要频繁地运行它。
8.4 你应该可以单独运行每个测试,以及以任何顺序运行测试。
8.5 测试应当可以在任何环境中重复通过。你应该能够在生产环境、质检环境中运行测试,也能够在无网络的列车上用笔记本电脑运行测试。如果测试不能在任意环境中重复,你就总会有个解释其失败的借口。
8.6 无论通过还是失败,你都不应该查看日志文件来确认测试是否通过。如果测试不能自足验证,运行测试就需要更长的手工操作时间。
9 类
9.1 通常而言,方法操作的变量越多,就越黏聚到类上。如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。内聚性高,意味着类中的方法和变量相互依赖,互相结合成一个逻辑整体。
9.2 你想要拆出来的代码使用了该函数中声明的4个变量,是否必须将这4个变量作为参数传递到新函数中呢?没必要!只要将4个变量提升为类的实体变量,完全无需传递任何变量。可惜这也意味着类丧失了内聚性,因为堆积了越来越多只为少量函数共享而存在的实体变量。等一下!如果有些函数想要共享某些变量,为什么不让它们拥有自己的类呢?
10 系统
10.1 将全部构造过程搬迁至main或者被称为main的模块中,设计系统的其余部分时,假设所有对象都已正确设置。
10.2 控制反转将第二权责从对象中拿出来,转移到另一个专注于此的对象中,从而遵循了单一权责原则。
11 并发编程
11.1 Web应用的Servlet标准模式。这类系统运行于Web或EJB容器的保护伞下,容器为你部分地处理并发问题。当有Web请求时,servlet就会异步执行。程序员无需管理所有的请求。实际上,Web容器提供的解耦手段离完美还差得远。