0%

【书摘】代码整洁之道

小处诚实非小事,神在细节之中。


习艺之要有二:知和行。你应当习得有关原则、模式和实践的知识,穷尽应知之事,并且要对其了如指掌,通过刻苦实践掌握它。

整洁代码

Bjarne Stroustrup: 我喜欢优雅和高效的代码,代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。

Grady Booch: 整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。

Dave Thomas: 整洁的代码应可由作者之外的开发者阅读和增补。它应有单元测试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽量少的 API。代码应通过其字面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达。

Michael Feathers: 我可以列出我留意到的整洁代码的所有特点,但其中有一条是根本性的。整洁的代码总是看起来像是某位特别在意它的人写的,几乎没有改进的余地。代码作者什么都想到了,如果你企图改进它,总会回到原点,赞叹某人留给你的代码——全心投入的某人留下的代码。

Ron Jeffries: 近年来,我开始研究贝克的简单代码规则,差不多也都琢磨透了。简单代码,依其重要顺序:能功过所有测试;没有重复代码;体现系统中的全部设计理念;包括尽量少的实体,比如类、方法、函数等

Ward Cunningham: 如果每个例程都让你感到深合己意,那就是整洁代码,如果代码让编程语言看起来像是专为解决那个问题而存在的,就可以称之为漂亮的代码。

光把代码写好可不够。必须时时保持代码整洁。我们都见过代码随时间流逝而腐坏。我们应当更积极地组织腐坏的发生。借用美国童子军一条简单的军规:

让营地比你来时更干净

有意义的命名

好名字的简单规则

  • 名副其实
  • 避免误导
    • 别用 accountList 来指一组帐号,除非它真的是 List 类型
    • 提防使用不同之处较小的名称
  • 做有意义的区分
  • 使用读得出来的名称
  • 使用可搜索的名称(单字母名称和数字常量很难在一大篇文字中找出来)
  • 避免使用编码
  • 避免使用思维映射
  • 类名不应当是动词
  • 方法名应当是动词或动词短语
  • 别扮可爱
  • 每个概念对应一个词
  • 别用双关语
  • 使用解决方案领域名称
  • 使用源自所涉问题领域的名称
  • 添加有意义的语境(实在不行可以添加前缀)
  • 不要添加没用的语境

函数

  • 短小
  • 只做一件事
  • 每个函数一个抽象层级
  • switch 语句
    • 如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍
  • 使用描述性的名称
  • 函数参数
    • 尽量避免三个或三个以上参数
  • 无副作用
  • 分隔指令与询问
  • 使用异常替代返回错误码
    • 错误处理代码就能从主路径代码中分离出来
    • 抽离 Try/Catch 代码块:最好把 try 和 catch 代码块的主题部分抽离出来,另外形成函数
    • 错误处理就是一件事
    • 返回错误码通常暗示有某个类或是枚举,定义了所有错误码,这样的类就是一块依赖磁铁(dependency magnet),其他许多类都得导入和使用它
  • 别重复自己
  • 结构化编程

每个系统都是使用某种领域特定语言搭建的,而这种语言是程序员设计来描述那个系统的。函数是语言的动词,类是名词。编程艺术是且一直就是语言设计的艺术。

大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用来讲那个故事。

注释

尽管有时也需要注释,我们也该多花心思尽量减少注释量

  • 注释不能美化糟糕的代码
  • 用代码来阐述
  • 好注释
    • 法律信息
    • 提供信息的注释
    • 对意图的解释
    • 阐释
    • 警示
    • TODO 注释
    • 放大
    • 公共 API 中的 Javadoc
  • 坏注释
    • 喃喃自语
    • 多余的注释
    • 误导性注释
    • 循规式注释
    • 日志式注释
    • 废话注释
    • 可怕的废话(Javadoc 也可能是废话)
    • 能用函数或变量时就别用注释
    • 位置标记
    • 括号后面的注释
    • 归属与署名(有源代码控制系统)
    • 注释掉的代码
    • HTML 注释
    • 非本地信息
    • 信息过多
    • 不明显的联系
    • 函数头
    • 非公共代码中的 Javadoc(八股)

格式

  • 向报纸学习
    • 名称应当简单且一目了然。名称本身应该足够告诉我们是否在正确的模块中。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直到找到源文件中最底层的函数和细节
    • 概念间垂直方向上的区隔
    • 垂直方向上的靠近
    • 垂直距离
    • 垂直顺序
  • 横向格式
    • 水平方向上的区隔与靠近
    • 水平对齐
    • 缩进
    • 空范围
  • 团队规则
    • 在团队中,个人需要满足团队

对象和数据结构

  • 数据抽象
  • 数据、对象的反对称性
  • 得墨忒耳律:模块不应了解它所操作对象的内部情形
    • 火车失事:连串的调用通常被认为是肮脏的风格
    • 混杂
    • 隐藏结构
  • 数据传送对象
    • 最为精炼的数据结构,是一个只有公共变量、没有函数的类。这种数据结构有时被称为数据传送对象,或 DTO(Data Transfer Objects)。DTO 是非常有用的结构,尤其是在与数据库通信、或解析套接字传递的消息之类的场景中

错误处理

错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法。

  • 使用异常而非返回码
  • 先写 Try-Catch-Finally 语句
  • 使用不可控异常(可控异常的代价就是违反开放/闭合原则)
  • 给出异常发生的环境说明(创建信息充分的错误消息,并和异常一起传递出去)
  • 依调用者需要定义异常类
  • 定义常规流程
  • 别返回 null 值
    • 返回 null 值,基本上是在给自己增加工作量,也是在给调用者添乱。只要有一处没检查 null 值,应用程序就会失控
  • 别传递 null 值

整洁代码是可读的,但也要强固。可读与强固。可读与强固并不冲突。如果将错误处理隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码。做到这一步,我们就能单独处理它,也极大地提升了代码的可维护性。

边界

  • 使用边界接口
    • 如果你私用类似 Map 这样的边界接口,就把它保留在类或近亲类中。避免从公共 API 中返回边界接口,或将边界接口作为参数传递给公共 API
  • 浏览和学习边界
  • 学习性测试的好处不只是免费
  • 使用尚不存在的代码
  • 整洁的边界
    • 边界上会发生有趣的事。改动是其中之一。有良好的软件设计,无需重大投入和重写即可修改

单元测试

  • TDD 三定律
    • 在编写不能通过的单元测试前,不可编写生产代码
    • 只可编写刚好无法通过的单元测试,不能编译也算不通过
    • 只可编写刚好足以通过当前失败测试的生产代码
  • 保持测试整洁
    • 测试代码和生产代码一样重要
  • 整洁的测试
    • 三要素:可读性、可读性和可读性。
    • 明确、简洁,还有足够的表达力
  • 每个测试一个断言
  • FIRST 原则
    • 快速 Fast
    • 独立 Independent
    • 可重复 Repeatable
    • 自足验证 Self-Validating
    • 及时 Timely

  • 类的组织
    • 从一组变量列表开始。如果有公共静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。很少会有公共变量。
    • 公共函数应跟在变量列表之后。我们喜欢把某个公共函数调用的私有工具函数紧随在该公共函数后面。这符合了自顶向下原则
  • 类应该短小
    • 单一权责原则(SRP):类或模块应有且仅有一条加以修改的理由
    • 系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为
    • 内聚:类应该只有少量实体变量。类中的每个方法都应该操作一个或多个这种变量
    • 保持内聚性就会得到许多短小的类
  • 为了修改而组织

系统

  • 将系统的构造与使用分开
    • 分解 main
    • 工厂
    • 依赖注入
  • 扩容
    • 软件系统与物理系统可以类比。它们的架构都可以递增式地增长,只要我们持续将关注面恰当地切分
  • 测试驱动系统架构
  • 优化决策
  • 明智使用添加了可论证价值的标准
  • 系统需要领域特定语言

系统也应该是整洁的。侵害性架构会湮灭领域逻辑,冲击敏捷能力。当领域逻辑受到困扰,质量也就堪忧,因为缺陷更易隐藏,用户故事更难实现。当敏捷能力受到损害时,生产力会降低,TDD 的好处也遗失殆尽。

迭进

  • 简单设计规则 1:运行所有测试
  • 简单设计规则 2~4:重构
  • 不可重复
  • 表达力
  • 尽可能少的类和方法

并发编程

对象是过程的抽象,线程是调度的抽象

编写整洁的并发程序很难——非常难。并发是一种解耦策略。它帮助我们把做什么(目的)和何时(时机)做分解开。在单线程应用中,目的与时机紧密耦合,很多时候只要查看堆栈追踪即可断定应用程序的状态。

解耦目的与时机能明显地改进应用程序的吞吐量和结构。从结构的角度看,应用程序看起来更像是许多台协同工作的计算机,而不是一个大循环。系统因此会更易于被理解,给出了许多切分关注面的有力手段。

并发防御原则

  • 单一权责原则:建议分离并发相关代码与其他代码
  • 限制数据作用域:谨记数据封装;严格限制对可能被共享的数据的访问
  • 使用数据副本
  • 线程应尽可能地独立

其他需要注意的

  • 警惕同步方法之间的依赖
  • 保持同步区域微小

测试线程代码的方法:

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

味道与启发

以下这些都是不好的,需要避免

  • 注释
    • 不恰当的信息
    • 废弃的注释
    • 冗余注释
    • 糟糕的注释
    • 注释掉的代码
  • 环境
    • 需要多步才能实现的构建
    • 需要多步才能做到的测试
  • 函数
    • 过多的参数
    • 输出参数
    • 标识参数
    • 死函数
  • 一般性问题
    • 一个源文件中存在多个语言
    • 明显的行为未被实现
    • 不正确的边界行为
    • 忽视安全
    • 重复
    • 在错误的抽象层级上的代码
    • 基类依赖于派生类
    • 信息过多
    • 死代码
    • 垂直分隔
    • 前后不一致
    • 混淆视听
    • 人为耦合
    • 特性依赖
    • 选择算子参数
    • 晦涩的意图
    • 位置错误的权责
    • 不恰当的静态方法

应该做的

  • 使用解释性变量
  • 函数名称应该表达其行为
  • 理解算法
  • 把逻辑依赖改为物理依赖
  • 用多态替代 If/Else 或 Switch/Case
  • 遵循标准约定
  • 用命名常量代替 Magic Number
  • 准确
  • 接哦古甚于约定
  • 封装条件
  • 避免否定性条件
  • 函数只该做一件事
  • 掩蔽时序耦合
  • 别随意
  • 封装边界条件
  • 函数应该只在一个抽象层级上
  • 在较高层级放置科配置数据
  • 避免传递浏览

Java 技巧

  • 通过使用通配符避免过长的导入清单
  • 不要继承常量
  • 用枚举代替常量

名称

  • 采用描述性名称
  • 名称应与抽象层级相符
  • 尽可能使用标准命名法
  • 五歧义的名称
  • 为较大做用范围选用较长名称
  • 避免编码(前缀等没必要)
  • 名称应该说明副作用

测试

  • 测试不足
  • 使用覆盖率工具
  • 别略过小测试
  • 被忽略的测试是对不确定事物的疑问
  • 测试边界条件
  • 全面测试相近的缺陷
  • 测试失败的模式有启发性
  • 测试覆盖率的模式有启发性
  • 测试应该快速