读了代码整洁之道,觉得这本书写的很好,所以就将里面自己觉得很经典的内容记录下来,作为自己以后写代码的标准和准则。同时也为那些曾经困惑过的人一点参考吧!

# 一、在正式开始之前,我们先思考几个几个问题:

# 1. 需求与代码哪个重要?

答:并不是所有的产品都能提出合理的需求,当你面对一个提出不合理需求的产品的时候,你需要坚持自己的原则,不能妥协。

# 2. 易读和易懂是一回事吗?

答:易读的代码和易懂的代码是有区别的,不是易读的代码就是易懂的代码。

# 3. 什么是测试驱动代码?

答:测试驱动代码,你写的代码要可以执行单元测试。如果你发现你的代码很难写单元测试,那么你就要思考你的代码是不是已经不整洁了,或者说已经乱成一团了。

# 4. 什么是简单的代码?

答:
1. 能通过所有测试
2. 没有重复代码
3. 体现系统中全部设计理念
4. 包含尽量少的实体,包括,类,函数,方法等

如果某段代码在程序设计中反复出现,就证明想法在代码中没有很好的体现出来。总之,不要重复代码,只做一件事,表达力,小规模抽象。

# 二、第二章 有意义的命名

# 1. 名副其实

变量,函数或类的名称应该已经答复了所有的大问题。它应该告诉你,它为什么会存在,它做什么事情,应该怎么用。如果名称需要注释来补充,那么就不算名副其实。

# 2. 避免误导

必须避免留下掩藏代码本意的错误线索。

# 3. 做有意义的区分

只要体现出有意义的区分,使用 a 和 the 这样的前缀就没有错。废话就是冗余。

# 4. 使用读得出来的名称

不要用傻乎乎的自造词,而不是恰当的英语词。

# 5. 使用可搜索的名称

使用便于搜索的名字。

# 6. 避免使用编码

把类型或者作用域编进名称里面,徒然增加了解码的负担。没理由要求每位新人都在弄清要应付的代码之外,还要再搞懂另一种编码” 语言”。

# 7. 避免思维映射

不应当让读者在脑中把你的名称翻译为他们熟知的名称,尤其是在经常出现在选择是使用问题领域术语还是解决方案领域术语的时候。

# 8. 类名或对象名最好使用名词或者名词短语

# 9. 方法名最好使用动词或者动词短语

# 10. 别扮可爱

命名一定要通俗易懂。

# 11. 每一个概念对应一个词

给每个抽象概念选择一个词,并一以贯之

# 12. 别用双关语词

避免将同一个词用于不同目的,遵循一词一义的原则

# 13. 使用解决方案领域名

# 14. 使用源自所涉领域名

# 15. 添加有意义的语境

在一个 bean 中定义变量名的时候,变量名一定是基于实际使用场景。

# 16. 不要添加没用的语境

# 三、第三章 函数

# 1. 短小

函数第一规则是要短小。第二条规则是还要更短小。

# 2. 只做一件事

  • 函数应该做一件事。做好这件事。只做一件事。如果可以从你的函数中还能拆分出一个函数,该函数不仅只是单纯地重新诠释其实现,那么你设计的函数就不是一个好的函数。
  • 函数中的区段,如果函数被切分了多个区段,这就证明该函数做的事情太多了。

# 3. 每个函数一个抽象层级

# 4.switch 语句

确保每个 switch 都埋藏在较低的抽象层级,而且永远不重复。

# 5. 使用描述性的名称

命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如:includeSetupPages。

# 6. 函数参数

  • 最理想的函数参数的数量是零个,其次是一,再次是二,应尽量避免三。有足够的理由才能用上三个以上的参数。
  • 如果函数看来需要两个,三个或者三个以上参数,就说明其中一些参数应该封装为类了。
  • 函数和函数的参数命名一般是动名词比较好。

# 7. 无副作用

# 8. 分割指令与查询

函数要么做什么事,要么回答什么事,但二者不可兼得

# 9. 使用异常替代返回错误码

  • 返回错误码的时候,就是在要求调用者立刻处理错误。
  • 如果使用异常替代返回错误码,错误处理代码就能从住路径代码中分离出来,得到简化。
  • 抽离 try/catch 代码块

# 10. 别重复自己

# 11. 结构化编程

# 四、第四章 注释

# 1. 如果你代码写的足够好的话,可以让人一看就懂,那么你就不需要再写注释。如果你的代码需要注释,那么你就需要想想是不是你的表达水平有问题。

# 2. 真正好的注释是想办法不用写注释,那么什么样的注释需要写呢?

  • 法律信息
  • 提供信息的注释
  • 提供意图的解释
  • 阐释
  • 警告
  • TODO 注释
  • TODO 是一种程序要认为应该要做的,但是由于某种原因没有做的
  • 放大,放大某些看似不合理的地方

# 五、第六章 对象和数据结构

# 1. 得墨忒定律

模块不应该了解他所操作对象的内部情形。对象隐藏数据曝露操作,这就意味着对象不应该通过存取器曝露其内部结构。

# 六、第七章 错误处理

# 1. 将业务逻辑和出错处理一定要隔离开,但是并不是所有的情况都试用,在不适用的情况下我们可以创建一个配置对象将特殊情况给予返回。

# 2. 异常处理不能过多的曝露实现细节,主要提现在异常抛出的栈信息上。

# 七、第八章 边界

# 1. 整洁的边界,边界上的代码要清晰的分割和定义了期望的测试。应该避免我们的代码过多地了解第三方代码中的特定信息。

# 八、第九章 单元测试

# 1.TDD 三定律

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

# 2. 整洁测试三要素

  • 可读性
  • 可读性
  • 可读性

# 3. 整洁测试的五条原则:

  • 快速,测试应该够快
  • 独立,测试应该相互独立
  • 可重复,测试应当可在任何环境中重复通过。
  • 自足验证,测试应该有布尔值输出,无论测试是成功还是失败,不应该人工通过 log 来确认测试是否成功或者失败。
  • 及时,测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。如果在编写生产代码之后编写测试,你会发现生产代码难以测试。你可能会认为某些生产代码本身难以测试。你可能不会去设计可测试的代码。

# 九、第十章 类

# 1. 单一权责原则,类或者模块应有且只有一条加以修改的理由。

# 2. 内聚,如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。内聚性高,意味着类中的方法和变量相互依赖,相互结合成一个逻辑整体。

# 十、第十一章 系统

# 1. 将系统的构造与使用分开

软件系统应将起始过程和启始过程之后的运行时逻辑分离开,在起始过程中构建应用对象,也会存在相互缠结的依赖关系。(延迟初始化可以加快程序运行速度)

# 1.1 分解 main

将程序的构造放到 main,并使其与运行时逻辑分开,main 只管构造并将构造后的数据对象发送给各个应用,或者被各个应用使用。但是,不参与运行时的任何逻辑控制。

# 1.2 工厂模式

使用该模式可以将系统构造和系统运行时逻辑分开。

# 1.3 依赖注入(需要强化理解)

# 1.4 扩容

“一开始就做对系统” 纯属神话。反之,我们应该只去实现今天的用户故事,然后重构,明天再扩展系统,实现新用户的故事。这就是迭代和增量敏捷的精髓所在。测试驱动开发,重构以及他们打造出的整洁的代码。

# 1.5 测试驱动系统架构

最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯编程语言对象实现。不同的领域之间用最不具有侵害性的方面或类方面工具结合起来。这种架构就能测试驱动,就像代码一样。(需要关注面向切片编程 (POJO))

# 1.6 优化决策

模块化和关注面切分成就了分散化管理和决策。在巨大的系统中,不管是一座城市或是一个软件项目,无人能做所有的决策。最好是授权给最有资格的人。拥有模块化关注面的 POJO 系统提供的敏捷能力,允许我们基于最新的知识做出优化的,时机刚好的决策。决策的复杂性降低了。

# 十一、第十二章 迭进

# 1. 通过迭进设计达到代码整洁的目的:

通过这几个原则可以让你的设计变简单(DIP,SRP)

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

# 2. 简单设计规则 1: 运行所有测试

编写的测试越多就越能持续走向编写较易测试的代码,紧耦合的代码很难编写测试。遵循有关编写测试并持续运行测试的简单,明确的规则,系统就会更贴近 OO 低耦合度,高内聚度的目标。编写测试引致更好的设计。

# 3. 简单设计规则 2-4:重构

在重构过程中,可以应用有关优秀软件设计的一切知识。提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称,如此等等。这也是简单设计原则后三条规则的地方:消除重复,保证表达力,尽可能减少类和方法的数量。

# 4. 不可重复

# 5. 表达力

# 6. 尽可能少的类和方法

# 十二、第十三章 并发编程

# 1. 为什么要并发

并发是一种解耦策略。它帮我们把做什么和何时做分解开。

# 2. 并发的防御原则

# 13.1 单一权责原则(SRP)

并发代码应该从其他代码中分离出来,因为,并发代码相比于其他的代码是很复杂的存在。但是,并发实现细节常常直接嵌入到其他生产代码中。那么下面几个问题需要考虑,

  • 并发相关代码由自己的开发,修改和调优生命周期
  • 开发相关代码由自己要对付的挑战,和非并发相关代码不同,而且往往更为困难
  • 即便没有周边应用程序增加负担,写得不好的并发代码可能的出错方式数量已经足具挑战性。

建议:分离并发相关代码与其他代码。

# 13.2 限制数据作用域

两个线程修改共享对象的同一字段时,可能相互干扰,导致未预期的行为。解决方案之一是采用 synchronized 关键字在代码中保护一块使用共享对象的临界区。限制临界区的数量很重要。更新共享数据的地方越多,就越可能出错。

建议:谨记数据封装,严格限制对可能被共享的数据的访问。

# 13.3 使用数据副本

避免共享数据的好方法之一就是一开始就避免共享数据。在某些情况下,有可能复制对象并以只读方式对待。在另外一种情况下,有可能复制对象,从多个线程收集所有副本的结果,并在单个线程中合并这些结果。

# 13.4 线程尽可能地独立

让每个线程在自己的世界中存在,不与其他线程共享数据。每个线程处理一个客户端请求,从不共享的源头接纳所有请求数据,存储为本地变量。

建议:尝试将数据分解到可被独立线程操作的独立子集。

# 13.5 了解执行模型

# 13.5.1 生产者---消费者模型

一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。

# 13.5.2 读者---作者模型

当存在一个主要为读者线程提供信息源,但只偶尔被作者线程更新的共享资源,吞吐量就会是个问题。增加吞吐量,会导致线程饥饿和过时信息的积累。更新会影响吞吐量。协调读者线程,不去读作者线程正在更新的消息,这是一种辛苦的平衡工作。作者线程倾向于长期锁定许多读者线程,从而导致吞吐量问题。

挑战之处在于平衡读者线程和作者线程的需求,实现正确的操作,提供合理的吞吐量,避免线程饥饿。

# 13.5.3 哲学家问题(故事内容不在这里描述)

从哲学家问题可以反映出编写并发经常遇到的问题,死锁,活锁,吞吐量和效率降低等问题。那么学习这些基础问题的解决算法可以帮助你解决大多数的并发问题,因为大多的并发问题都是上述问题的变种。

# 13.6 警惕同步方法之间的依赖

同步方法之间的依赖会导致并发代码中的狡猾缺陷。Java 语言有 synchronized 概念,可以用来保护单个方法。然而,如果在同一共享类中由多个同步方法,系统就可能写得不太正确了。

建议:避免使用一个共享对象的多个方法。如果有时必须使用一个共享对象的多个方法。这种情况发生时,有3中写对代码的手段:

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

# 13.7 保持同步区域微小

应该尽可能少地设计临界区,如果有请尽可能减小同步区域。

# 13.8 编写线程需要注意的几点:

# (1). 将伪失败看作可能的线程问题。线程代码中的缺陷可能在一千或者一百万次执行中才会显现一次。所以,不要将系统错误归咎于偶发事件。

# (2). 先使非线程代码可工作。确保线程之外的代码可以工作。不要同时追踪非线程缺陷和线程缺陷。确保代码在线程之外可以工作。

# (3). 编写可插拔的线程代码。编写在数个环境下运行的线程代码

  • 1. 单线程与多线程在执行时不同的情况
  • 2. 线程代码与实物或测试替身互动
  • 3. 用运行速度,缓慢和有变动的测试替身执行
  • 4. 将测试配置为能运行一定数量的迭代

建议:编写可插拔的线程代码,这样就能在不同的配置环境下运行。

# (4). 编写可调整的线程代码

在系统运行时允许线程发生变动。允许线程依据吞吐量和使用率自我调整。

# (5). 运行多余处理器数量的线程

任务交换越频繁,越有可能找到错过临界区或导致死锁的代码。

# (6). 在不同平台上运行

# (7). 调整代码并强迫错误发生

# 十三、第 14 章 逐步改进

代码并不是一次就能写的很干净的,需要我们不断的迭代和优化。

# 十四、第 17 章 味道与启发

# 1. 注释

# (1). 不恰当的注释信息

让注释传达本该更好地在源代码控制系统,问题追踪系统或任何其他记录系统中保存的信息,是不恰当的。例如,修改历史记录只会用大量过时而无趣的文本搞乱源代码文件。注释只应该描述有关代码和设计的技术性信息。

# (2). 废弃的注释

过时,无关或不正确的注释就是废弃的注释。

# (3). 冗余注释

如果注释描述的是某种充分自我描述了的东西,那么注释就是多余的。注释应该谈及代码自身没有提到的东西。

# (4). 注释要写就要写最好的注释,别画蛇添足

# (5). 注释掉的代码,看到注释掉的代码就删。

# 2. 环境

# (1). 需要多步才能实现的构建

构建系统应该是单步的小操作。不应该从源代码控制系统中一小点一小点签出代码。不应该需要一系列神秘指令或环境依赖脚本来构建单个元素。不应该四出寻找额外的小 jar,xml 文件和其他系统所需的杂物。你应该能够用单个命令签出系统,并单个指令构建它。

# (2). 需要多步才能做到测试

你应该能够用单个指令就可以运行全部单元测试。能够运行全部测试是如此基础和重要,应该快速,轻易和直接了当地做到。

# 3. 函数

  • 函数的参数应该少。
  • 输出参数违反直觉。如果非要修改,那就修改函数所在对象好了。
  • 标示参数,布尔值参数宣告函数做了不止一件事,应该消灭掉,即函数只做一件事。
  • 永不被调用的方法应该丢弃。

# 4. 一般性问题

(1). 如果代码需要有多种语言,那么应该尽力减少源文件中额外语言的数量和范围。

(2). 明显的行为未被实现,如果明显的行为未被实现,读者和用户就不能再依靠他们对函数名称的直觉。他们不再信任原作者,不得不阅读代码细节。

(3). 不正确的边界行为,追索每种边界条件,编写测试。

(4). 忽视安全

(5). 重复,尽可能找到并消除重复。

(6). 在错误的抽象层级上的代码,创建分离较高层级一般性概念与较低层级细节概念的抽象模型,这很重要。

(7). 基类不可多度依赖于派生类,基类应该对派生类一无所知。

(8). 信息过多,设计良好的模块有着非常小的接口,让你事半功倍。设计良好的接口并不提供许多需要依靠的函数,所以耦合度比较低。

优秀的软件开发人员学会限制类或模块中暴露的接口数量。类中的方法越少越好。函数知道的变量越少越好。类拥有的实体变量越少越好。隐藏你的数据。隐藏你的工具函数。隐藏你的常量和你的临时变量。不要创建拥有大量方法或大量实体变量的类。不要为子类创建大量受保护变量和函数。尽力保持接口紧凑。通过限制信息来控制耦合度。

(9). 死代码一定要删除。

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

(11). 前后不一致,从一而终即一旦选中,就小心持续遵循。如果在特定函数中用了 response 的变量来持有 HttpServletResponse 对象,则在其他用到 HttpServletResponse 对象的函数中也用同样的变量名。这样会让你的代码更易阅读。

(12). 混淆视听,没用的变量或者函数直接删掉,以免造成不必要的干扰。

(13). 人为耦合,不互相依赖的东西不该耦合。一般来说,人为耦合是指两个没有直接目的之间的模块的耦合。其根源是将变量,常量或函数不恰当地放在临时方便的位置。

(14). 特性依恋,类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。当方法通过某个其他对象的访问器和修改器来操作该对象内部数据,则它就依恋于该对象所属类的范围。

(15). 选择算子参数,尽量不要把 boolean 作为函数参数

(16). 晦涩意图,代码要尽可能具有表达力。

(17). 位置错误的权责,我们可以采用最小惊异原则来帮助我们即代码应该放在读者自然而然期待它所在的地方。

(18). 不恰当的静态方法,对于没有机会打算让它有多态行为的函数可以作为动态函数,否则可选用静态函数。

(19). 使用解释性变量,命名要有意义。

(20). 函数名称应该表达其行为

(21). 理解算法

(22). 把逻辑依赖改为物理依赖:

  • 1. 逻辑依赖:原始数据和业务逻辑之间的依赖关系。例如:定义的常量和业务逻辑存在着依赖关系或者说被捆绑在了一起,即当业务逻辑发生变化或者存在其他可能性的时候且定义的常量无法满足导致常量需要修改。
  • 2. 物理依赖:函数方法和业务逻辑之间的依赖关系。例如:为了解决逻辑依赖的问题,我们可以通过构造一个方法将数据和业务隔离。

(23). 用多态替代 if/elseswitch/case

(24). 遵循标准约定

(25). 用命名常量替代魔术数,魔术数不仅仅指的是数字,也泛指任何不能自我描述的符号。

(26). 准确,在可以用 List 的时候,非要把变量声明为 ArrayList 就过分拘束了。在代码中做决定时,确认自己足够准确。明确自己为何要这么做,如果遇到异常情况如何处理。别懒得理会决定的准确性。代码中的含糊和不准确性要么是意见不同的结果,要么源于懒惰。无论原因是什么,都要消除。

(27). 结构甚于约定,坚守结构甚于约定的设计决策。

(28). 封装条件,如果没有 if 或 while 语句的上下文,布尔逻辑就难以理解。应该把解释了条件意图的函数抽离出来。

例如:

if(shouldBeDeleted(timer))

要好于

if(timer.hasExpired() && !timer.isRecurrent())

(29). 避免否定性条件,尽可能将条件表示为肯定形式。

(30). 函数只该做一件事

(31). 掩蔽时序耦合,排列函数参数,好让它们被调用的次序显而易见。

(32). 别随意,构建代码需要理由,而且理由应于代码结构相契合。

(33). 封装边界条件,把处理边界条件的代码集中到一处,不要散落于代码中。

(34). 函数应该只在一个抽象层级上,函数中的语句应该在同意抽象级上,该层级应该是函数名所示操作的下一层。

(35). 在较高层级放置可配置数据,如果你有个已知并在较高抽象层级的默认常量或配置值,不要将它埋藏到较低层级的函数中。把它作为较高层级函数调用较低层级函数时的一个参数。位于较高层级的配置性常量易于修改。它们向下贯穿应用程序。应用程序的较低层级并不拥有这些常量的值。

(36). 避免传递浏览,编写害羞代码。

(37). 不要继承常量

# 17.5 测试

  1. 使用覆盖率工具,覆盖率工具能汇报你测试策略中的缺口。使用测试覆盖率工具能更容易地找到不足的模块,类和函数。
  2. 别略过小测试
  3. 被忽略的测试就是对不确定事物的疑问。
  4. 测试边界条件
  5. 全面测试相近的缺陷,缺陷趋向于扎堆。
  6. 测试失败的模式有启发性。
  7. 测试覆盖率的模式有启发性,查看被或未被以通过的测试执行的代码,往往能发现失败的测试为何失败的线索。
  8. 测试应该快速。

来自:代码整洁之道内容概要

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Metal-Heart 微信支付

微信支付

Metal-Heart 支付宝

支付宝

Metal-Heart 贝宝

贝宝