:::info ℹ️ 书籍摘录 from《代码整洁之道 CleanCode: A handbook of Agile Software Craftmanship》 :::

试问什么是整洁的代码?

面向「」写代码。

WIP

命名

  • 名副其实,减少缩写
  • 避免误导,比如 o 和 0
  • 做有意义的区分,一看就能够懂意思
  • 使用可搜索的名称,单一工程下尽量去除重复命名
  • 尽量避免使用没有意义的编码,比如成员前缀等等
    • I 前缀的滥用就是反例
  • 类和实例使用名词,方法使用动词或者动词短语
  • 一个概念对应一个词
  • 使用领域解决方案的专用词汇
    • 设计模式
    • CS 领域相关词汇
  • 使用涉及要解决特定的领域问题的专用词汇
  • 增加有意义的修饰词,以提供语境

函数

  • 短小、简单
  • 职责单一,只做一件事情(看起来容易,很多时候贯彻落实却很难)
  • 每个函数都是不同层次的抽象
  • 函数参数数量最小化原则,少即是多
    • 单参函数,双参函数尽量作为常态使用
    • 长函数参数,应该 对象化
  • 杜绝标志性参数,典型的就是 boolean
  • 函数尽量减少「副作用」,理想的函数应该是纯函数
  • 函数内部尽量使用「异常」来代替错误码
  • 尝试将 try-catch 在调用层隔离,不要在函数体内实现
  • 永远别尝试重复自己,消灭重复!
  • 不断 重构 的心

注释

  • 代码即注释
    • 较多的注释并不意味着一定就好,因为很多时候没有及时更新的注释具有「欺骗性」
    • 注释并不能「美化」糟糕的代码,反而会引入理解成本问题
  • 值得「注释」的地方
    • 法律信息
    • 提供信息的注释 & 阐释(换一种语言描述助于理解)
    • 对意图的解释(Why)
    • 警告 WARN
    • 待办 TODO
    • 放大结构,利用注释来放大不合理的代码
    • 公共 API 值得类似 JavaDoc 的方式提供完备的注释体系
  • 不值得或者糟糕的「注释」
    • 描述代码实现的注释(How)即废话
    • 纯粹的废话注释,比如:/* Class Constructor */
    • 无关的内容(喃喃自语)
    • 私有 API 无关的正式 Doc 或者高频变化的实现的正式 Doc
    • 日志式注释(增加各种版本信息 bla … bla … bla …)
    • 位置标记(分隔符)大部分不建议做,除非函数体内确乎需要隔离,但这有悖于函数最小化的原则
    • 归属签名(意义不大)
    • 注释掉的代码一定不要留,除非有特定明确意图保留的说明
    • 信息不要过多,过多信息可以外链出去 @see or @link

格式

用好 Linter 和 Formatter

  • 代码格式在当代多人协同的团队中显得非常重要
  • 垂直格式
    • 单个文件 300 ~ 500 行或许比较合适(统计给出)
    • 合理使用空白符或者分隔符切分内容(设计上叫做留白)
    • 垂直对齐
    • 结构体内顺序做适当设计,比如 privatepublic 成员顺序
  • 横向格式
    • 120 字符左右比较适合现代显示器 🖥
    • 水平对齐、缩进
    • 分号的使用
  • 建立团队的规则,并用工具保证的同时做卡口

对象和数据结构

  • 对数据对象,务必做好封装和抽象,面向接口、getter / setter 设计。
    • DTO(Java 中数据传送对象)就是一个典型。
  • 面向过程 & 面向对象的非对称性:过程式代码难以添加新的数据结构,因为必须修改所有关联函数;面向对象式的代码难以添加新的函数,因为必须修改所有关联的类。
  • The Laws of Demeter:模块不应该了解它所操作对象的内部情形(细节)。

错误处理

  • 使用异常而非错误码,具备直观性。
  • 对于可能存在异常的代码,先编写 try-catch-finally 语句,便于自身理解逻辑,定义结构范围。
  • 给出异常发生的环境信息,比如 stack-trace
  • 异常类一定是服务于调用者的,设计的时候务必按照调用者的需要进行设计。
  • 谨慎对待 null 值,最好不要返回或者传递 null 值

边界

创造整洁的边界。

  • 谨慎使用第三方的代码,对于三方代码抛出的类型,我们应该进行封装,不要在自己的模块中对其它模块产生直接依赖,否则三方的代码升级或者变更会导致一系列链式反应。
  • 使用「学习性测试」来调用第三方代码,增进对三方代码的理解。
  • 对于黑盒式的代码,使用适配器模式(Adapter)做好隔离,实现整洁的边界。

单元测试

  • TDD 三定律:
    • 在编写不能通过的单元测试前,不可编写任意代码
    • 之可编写刚好无法通过的单元测试,不能编译也算不能够通过
    • 之可编写刚好足以通过当前失败测试的代码
  • 保持测试代码的整洁度,它一样重要,不能够被当做二等公民来对待。
  • 对于特定的复杂度的项目,我们可以使用 DSL 编写测试。
  • 尽量做到一个测试,一个断言,「单一职责,如是而已」。
  • 每个测试函数也应该尽量做到测试一个概念。

  • 成员类型的组织顺序需要有所讲究。
  • 类应该短小(SRP),持续重构,防止类变得臃肿无助。
  • 高内聚、低耦合。
  • 为方便于修改和扩展而设计(IoC),最好能够实现类之间的正交关系。

:::info ℹ️ 类,这块,在《重构》这本书里面,提到了大量的最佳实践,值得深入。 :::

系统

  • 更高的抽象等级,产生组件、模块等更高维度的抽象概念。
  • 充分使用构建对象的设计模式
    • builder
    • factory
  • 用好依赖注入,便于大型项目的依赖管理
  • 考虑扩容的情形
  • 考虑代理模式
  • 使用 AOP 编程进行模块化
  • DSL 的使用可以简化部分复杂的设计,让领域专家能够和领域特定的语言打配合,简化设计和实现
    • css & html 都是典范
  • 适度采用测试驱动的系统架构(TDD 在系统层的实践)

迭进

  • 运行所有的测试
  • 重构
  • 消除重复
  • 增强表达力
  • 减少类和方法

并发编程

对象是对过程的抽象,线程是对并发的抽象。 —— James O Coplien

  • 能够减少使用并发的场景就尽量避免,毕竟编写整洁的编发程序很难,非常难。
  • 并发的防御原则
    • 单一职责原则 SRP:将并发的逻辑和业务的实现细节分离
    • 限制数据作用域:Java 中提供了 synchornized 关键字来在代码中保护一块使用共享对象的临界区(critical section)。
    • 使用数据副本:线程自治自己的副本,后使用单线程进行数据合并。
    • 线程应该尽可能独立:减少和其它线程的通信和耦合。
  • 典型的 case
    • java.util.concurrent 包中的线程安全群集
    • 生产者和消费者模型:一个或者多个生产者线程创建默写工作,并放置于缓存队列中。一个或者多个消费者线程从该队列中获取并完成这些工作。
    • 读者 - 作者模型:平衡读者和作者的 I/O & 吞吐量,避免饥饿。锁模型会在这里产生重要的功效。
    • 哲学家宴席问题:死锁问题,参考业界成熟算法,做好规避。
  • 警惕同步方法之间的依赖,并保持同步区域微小。
  • 测试多线程的关注要素:
    • 将「不可能失败」的失败,往往可以归结到线程问题上。
    • 使用非线程代码依旧可以 work 的原则。
    • 进一步:编写可插拔的线程代码。
    • 编写可调整的线程代码。比如线程数、吞吐量可以自适应调整。
    • 支持跨平台(可移植性问题)。
    • 尝试使用硬编码和自动化。