《代码的整洁之道》读记

Later equals never!
稍后等于永不!


读因

读这本书之前做了一些功课,很多人反映,书是一本好书,无奈作者废话太多。。。
刚开始,我是不信的,但自己读下来才发现,古人诚不欺我啊。
来划一下重点吧,写的这些或者是作者所描述的重点,或者是我感触比较深的东西。



读感

终于把这本书啃完了。

虽然作者比较啰嗦点,但是收获还是很大的。

书中提到的,有些已经做到了;但有些不仅没做到,还是反面教材。

多规范一下自己的代码,毕竟对于我们来说,代码的清晰度、整洁度还是很重要的。

代码总要给别人看的,不要让自己的代码羞以示人。



片段

关于整洁的代码

代码的逻辑应该直截了当,让缺陷难以隐藏;尽量去减少依赖关系,从而便于维护;依据某种分层战略完善错误处理代码;性能调到最优,避免他人污染。

糟糕的代码会引发混乱,别人修改糟糕的代码时,往往会越改越烂。

每个函数、每个类、每个模块 都全神贯注去解决一件事。

代码应该通过其字面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达。

重要顺序:

  • 能通过所有测试
  • 没有重复代码
  • 体现系统中的全部设计理念
  • 包括尽量少的实体


关于命名

名副其实,见名知意

使用可以读的出来的、可以被搜索的名称

匈牙利命名法、去掉成员前缀

类名、对象名 应该是名词或名词短语; 方法名应该是动词或动词短语

不要抖机灵,每个概念对应一个词,不用双关语(add、insert)

分离解决方案领域和问题领域的概念,与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称

添加有意义的语境


关于函数

函数的第一规则是短小,第二条规则是更短小!

每个函数尽量只做一件事。

自顶向下阅读代码,向下规则。

函数参数,最理想为零,其次单参、双参、三参…(除非有足够特殊的理由,不要三个参数以上)

不要向函数传入布尔类型,因为这叫要求该函数不止做一件事

不要有副作用,比如让你洗个苹果,你别洗完了然后吃了它。

普遍而言,应避免使用输出参数。

分隔指令与询问,要么让它干什么,要么让它回答什么。


关于注释 (别给糟糕的代码加注释,重新写吧!)

代码会一直被维护更新,但是注释不一定。

注释不能改变根本问题,它不能优化糟糕的代码。

值得写的注释:

  • 版权及著作权声明等
  • 对你的意图的解释
  • 警示
  • TODO注释,为以后编写查找方便

废注释:

  • 没有规范化,过于局部的注释(需要纵览全文,才能知晓其意)
  • 多余的注释( getMaxNumber(num1, num2) ,还需要写这个函数是干啥的吗?)
  • 误导性注释
  • 循规式注释(例如:要求每个函数都要像API文档一样写一套注释来说明函数作用,参数意义。)
  • 日志式注释(之前不是说光维护代码,不维护注释吗?现在我维护注释,而且把每次修改的时间、内容都加上。有那时间干啥不好,100行的文件,80行注释日志?)
  • 归属与署名
  • 注释掉的代码(除了注释的人,其他人都不敢删的东西)
  • 信息过多,无条理


关于格式 (代码的格式是你代码的普通话,别让他说方言)

用空白行来区分你的模块。

关系应该密切的东西:

  • 变量声明,应该尽可能的靠近其使用位置
  • 实体变量,应该在类的顶部声明
  • 相关函数,函数A调用了函数B,应该让A和B放到一起,A尽可能的放在B的上面
  • 概念相关,概念相关的代码 应该放在一起,相关性与距离成正比

尽量让代码行短小,最好以80个字符为上限,但最多不要超过120

水平方向上的区隔:

  • 赋值操作周围加上空格
  • 不在函数名和左圆括号之间加空格
  • 逗号后加空格
  • 加减周围加空格,优先级高的乘除周围不加空格;当然,如果只有优先级相同的运算符,还是可以在周围加空格的


关于对象与数据结构

不要将类内变量设置为私有,然后又添加赋值器和取值器,将它公之于众。

对象与数据结构之间的二分原理:

  • 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,
  • 面向对象代码便于在不改动既有函数的前提下添加新类
    反过来说就是:
  • 过程代码难以添加新数据结构,因为必须修改所有函数
  • 面向对象代码难以添加新函数,因为必须修改所有类

Demeter律,模块不应了解它所操作对象的内部情形。

最为精炼的数据结构,是一个只有公共变量、没有函数的类。


关于错误处理(当错误发生时,程序员有责任确保代码照常工作)

使用异常处理而非返回错误码

先写出 try-catch-finally语句

给出异常发生的环境说明,方便定位

依调用者需要定义异常类

不要返回、传递NULL值


关于边界(将其他代码整合到自己代码中)

使用类似Map的边界接口,就把它保留在类或近亲类中;避免从公共API中返回边界接口,或将边界接口作为参数传递给公共API。

学习性测试很有必要。


关于单元测试

TDD(测试驱动开发)三定律:

  • 在编写不能通过的单元测试前,不可编写生产代码
  • 只可编写刚好无法通过的单元测试,不能编译也算不通过
  • 只可编写刚好足以通过当前失败测试的生产代码

脏测试 等同于 没测试。

测试代码与生产代码一样重要,它需要被思考、被设计和被照料,它该像生产代码一样保持整洁。

整洁的测试的要素 - 可读性 !!!

整洁测试的五条规则 - FIRST

  • F:Fast,测试应该能够快速的运行。
  • I:Independent,测试应该相互独立。
  • R:Repeatable,测试应该可以在任何环境中重复通过。
  • S:Self-Validating,测试应该有布尔值输出。
  • T:Timely,测试应及时编写。


关于类

类应该由一组变量列表开始,公共静态常量优先于私有静态变量。

类应该通函数一样要短小。

类或模块应有且只有一条加以修改的理由。

单一全责:系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个全责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。

内聚:类应该只有少量实体变量。

既然修改会一直持续,那么就更应该对类加以组织,以降低修改的风险。

类应当依赖于抽象而不是依赖于具体细节。


关于系统

软件系统应将起始过程与之后的运行时逻辑分离开。 就比如我做一个玩家信息面板,在起始过程,需要创建很多Text、Image来存储玩家一些状态信息及玩家的形象。 但是,我用这个界面的时候,只需要改动里面的值、或者切换形象。
这时,就可以有两个函数,init来负责起始过程的创建;refresh来负责更新玩家的状态。(当然不能把所有具体实现都放在一个函数里,每个函数负责一个小模块是必要的)

软件系统与物理系统可以类比,它们的架构都可以递增式地增长,只要我们持续将关注面恰当的切分。

最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java(或其他语言)对象实现。不同领域之间用最不具有亲还行的方面或类方面工具整合起来。


关于迭代

Kent Beck关于简单设计的四条规则

  • 运行所有测试
  • 不可重复
  • 表达了程序员的意图
  • 尽可能减少类和方法的数量
    以上规则按其重要程度排列

遵循有关编写测试并持续运行测试的简单、明确的规则,系统就会更贴近OO低耦合度、高内聚度的目标。编写测试引致更好的设计。

测试消除了对清理代码就会破坏代码的恐惧,所以可以放心的去重构。

重复是良好系统设计的大敌,它代表着额外工作、额外风险和额外且不必要的复杂度。

增强表达力方法:

  • 选用好的名称
  • 保持函数和类的尺寸短小
  • 采用标准命名法
  • 编写良好的单元测试
  • 最重要的就是去尝试去做

尽可能减少类和方法的数量,这条规则优先级是最低的,要让步于测试、消除重复和增强表达力。


关于并发编程(对象是过程的抽象,线程是调度的抽象)

并发是一种解耦策略,它将目的时机分解开,而在单线程中,两者紧密耦合。

解耦目的与时机可以显著的改进程序的吞吐量结构

一些迷思与误解

  • 并发总能改进系能。 并发有时能改进性能,但只在多个线程或处理器之间能分享大量等待时间的时候管用。
  • 编写并发程序无需修改设计。 并发算法的设计有可能与单线程系统的设计极不相同,解耦目的与时机往往对系统结构产生巨大的影响。
  • 在采用Web或EJB容器的时候,理解并发问题并不重要。 只有了解容器的运作,才可以对其产生的并发问题更好的解决。

关于并发编程的中肯理解

  • 并发会在性能和编写额外代码上增加一些开销
  • 正确的并发是复杂的,即便对于简单的问题也是如此
    -并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待
  • 并发常常需要对设计策略的根本性修改

防御并发代码问题的原则与技巧

  • 单一权责原则(SRP)
    分离并发相关代码与其他代码
  • 限制数据作用域
    谨记数据封装,严格限制对可能被共享的数据的访问
  • 使用数据复本 从多个线程收集所有复本的结果,并在单个线程中合并这些结果
  • 线程应尽可能的独立 尝试将数据分解到可被独立线程(可能在不同的处理器上)操作的独立子集

一些基础定义

  • 限定资源
    并发环境中有着固定尺寸或数量的资源。
  • 互斥
    每一时刻仅有一个线程能访问共享数据或共享资源。
  • 线程饥饿
    一个或一组线程在很长时间内或永久被禁止。
  • 死锁
    两个或多个线程互相等待执行结束。
  • 活锁
    执行次序一致的线程,每个都想要起步,但发现其他线程已开始。

一些执行模型

  • 生产者-消费者模型
    一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。
  • 读者-作者模型
    当存在一个主要为读者线程提供信息源,但只偶尔被作者线程更新的共享资源,吞吐量就会是个问题。增加吞吐量,会导致线程饥饿和过时信息的累计。更新会影响吞吐量。协调读者线程,不去读作者线程正在更新的信息(反之亦然),这是一个辛苦的平衡工作。作者线程倾向于长期锁定许多读者线程,从而导致吞吐量问题。
  • 经典的哲学家 一群哲学家环坐在圆桌旁。每个哲学家的左手边放了一把叉子。桌面中央摆着一大碗意大利面。每个哲学家在吃饭的时候都要拿起叉子吃饭。但除非手上有两把叉子,否则没法进食。如果左边或右边的哲学家已经取用一把叉子,中间这位就需要等到别人吃完,放回叉子。每位哲学家吃完后,就将两把叉子放回桌面,直到下次吃饭。

避免使用一个共享对象的多个方法。当不得不使用时,写代码需要注意的方法:

  • 基于客户端的锁定
    客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码。
  • 基于服务端的锁定
    在服务端创建锁定服务端的方法,调用所有方法,然后解锁。让客户端调用新方法。
  • 适配服务端
    创建执行锁定的中间层。这是一种基于服务端锁定的例子,但不修改原始服务端代码。

尽可能减小同步区域。

尽早考虑关闭问题,尽早令其工作正常。

编写测试,测试线程代码。

关于测试代码的建议:

  • 将伪失败看做可能的线程问题
  • 先使非线程代码可工作
  • 编写可插拔的线程代码
  • 编写可调整的线程代码
  • 运行多于处理器数量的线程
  • 在不同平台上运行
  • 调整代码并强迫错误发生


关于逐步改进

要编写整洁代码,必须先写肮脏代码,然后清理它。
所以,不要害怕写的肮脏,只要去清理,就可以写出整洁的代码。
但是,一定要去清理它

在改进程序过程中,要保持系统始终可以运行。

进度可以重订,需求可以重新定义,团队动态可以修正,但糟糕的代码只是一直腐败发酵,无情的拖后腿。


味道与启发

注释

  • 不恰当的信息
    让注释传达本该更好地在源代码控制系统、问题追踪系统或任何其他记录系统中保存的信息,是不恰当的。
  • 废弃的注释
    过时、无关或不正确的注释就是废弃的注释。
  • 冗余注释
    如果描述的是某种充分自我描述了的东西,那么注释就是多余的。
  • 糟糕的注释
    值得编写的注释,也值得好好写。不要画蛇添足,要保持整洁。
  • 注释掉的代码
    看到注释掉的代码,就删除它!
    源代码控制系统还会记得他,让注释的人回去找。
    什么?不用源代码控制系统?
    好吧,你已经不需要读这本书了。

环境

  • 需要多步才能实现的构建
    构建系统应该是单步的小操作。
    不应该从源代码控制系统中一点点签出代码;
    不应该需要一系列神秘指令或环境依赖脚本来构建单个元素;
    不应该四处寻找额外小JAR、XML文件和其他杂物;
    应该能用单个命令签出系统,并用单个指令构建它。
  • 需要多步才能做到的测试
    应该能发出单个指令就可以运行全部单元测试。

函数

  • 过多的参数
    函数的参数量应该少,三个以上绝对不可容忍。
  • 输出参数
    输出参数违反直觉,读者期望参数用于输入而非输出。
  • 标识参数
    布尔值参数等同于宣告该函数做了不知一件事,应该消灭。
  • 死函数
    永不被调用的方法应该被丢弃。
    不要怕删除,源代码控制系统会帮你记住它。

一般性问题

  • 一个源文件中存在多种语言
    理想的源文件包括且只包括一种语言,
    现实中,应该尽力减少源文件中额外语言的数量和范围。
  • 明显的行为未被实现
    遵循”最小惊异原则”,函数或类应该实现其他程序员有理由期待的行为。
  • 不正确的边界行为
    不要让代码只是能工作,应该追索每种边界条件,并编写测试。
  • 忽视安全
    关闭某些编译器警告,可能有助于构建;但更存在无穷无尽的调试风险。
  • 重复
    本书最重要的规则之一
    尽可能找到并消除重复。
  • 在错误的抽象层级上的代码
    创建分离较高层级一般性概念与较低层级细节概念的抽象模型。
  • 基类依赖于派生类
    将概念分解到基类和派生类的最普遍的原因是较高层级基类概念可以不依赖于较低层级派生类概念。
  • 信息过多
    设计良好的模块有着非常小的接口,让你事半功倍。
    设计低劣的模块有着广阔、深入的接口,让你事倍功半。
    设计良好的接口并不提供许多需要依靠的函数,所以耦合度也较低。
    设计低劣的接口提供大量必须调用的函数,耦合度较高。
  • 死代码
    死代码就是不执行的代码,可以在
    • 不会发生的条件语句中
    • 从不抛出异常的try语句的catch块中
    • 在永久不会发生的switch/case条件中

找到这些代码。
然后,埋葬它!

垂直分隔
变量和函数应该在被靠近使用的地方定义。
私有函数应该刚好在其首次被使用的位置下面定义。

前后不一致
最小惊异原则,小心选择约定,一旦选中,就应该持续的遵循。

使用解释性变量名、函数名、类名

把逻辑依赖改为物理依赖
依赖者模块不应对被依赖者模块有假定,应该明确的询问候着全部信息。

用多态替代 if/else 或 switch/case

用命名常量替代魔术数

封装条件、边界

函数只做一件事

关于类

  • import package.*; 比80行的导入语句好看多了
  • 不要继承常量

名称

  • 采用描述性的名称
  • 名称应与抽象层级相符
  • 尽可能用标准命名法
  • 无歧义的名称
  • 为较大作用范围选用较长名称
  • 避免编码
  • 名称应该说明副作用

测试

  • 多测试
  • 使用覆盖率工具
  • 别略过小测试
  • 被忽略的测试就是对不确定事物的疑问
  • 测试边界条件
  • 全面测试相近的缺陷
  • 测试应该快速