# 代码整洁之道

# 参考文档

# 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 是依赖管理的手段,它将应用需要的依赖对象的创建权责从对象中拿出来,放在一个专注于此事的对象中,并通过依赖注入(赋值器)将依赖对象传递给应用。
  • 扩容:
    • 面向方面编程(AOP),Java 中三种方面和类似方面的机制:代理,纯 AOP 框架,AspectJ。
      • java 代理:适用于简单情况,如在单独对象或类中包装方法调用。代码量和复杂度是代理的两大弱点。
      • 纯 Java AOP 框架,如 Spring AOP、JBoss AOP。
      • AspectJ:提供将方面作为模块构造处理支持的 Java 扩展。
  • 最佳系统架构由模块化的关注面领域组成,每个关注面均用纯 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

# 讨论区

由于评论过多会影响页面最下方的导航,故将评论区做默认折叠处理。

点击查看评论区内容,渴望您的宝贵建议~
Last Updated: 6/10/2022, 3:54:48 PM