toBeTheLight.github.io 荒原

《代码整洁之道》摘录与理解

2018-09-24
toBeTheLight

本书的主要内容是成为更好的程序员。如何在意代码。 以JS程序员的角度去读这本书,基本按照内容结构进行组织,会有要点省略。

第一章 整洁代码

  • 勒布朗法则:稍后等于永不
  • 程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法

整洁代码的一些定义:

  • 优雅和高效,逻辑直接了当。减少依赖关系,便于维护。一句某种分层战略完善错误处理代码。性能调至最优,不引诱别人做没规矩的优化导致更多的混乱。整洁的代码只做好一件事。—- 每个函数、每个类和每个模块都全神贯注于一件事,完全不受四周细节的干扰和污染。
  • 简单直接,如同优美的散文,从不隐藏设计者的意图,充满了干净利落的抽象和直接了当的控制语句。—- 应当讲述事实,只该包含必需之物。
  • 应当有单元测试和验收测试,使用有意义的命名,只提供一种而非多种做一件事的途径。只有尽量少的依赖关系,而且要明确的定义和提供清晰、尽量少的API。代码应通过其字面表达含义。—- 测试驱动开发。推崇小块代码。用人类可读的方式来写代码。
  • 整洁的代码总是看起来像是某位特别在意它的人写的。—- 如何在意代码,整洁代码就是作者着力照料的代码。
  • 能通过所有测试;没有重读代码;体现系统中的全部设计理念;包括尽量少的实体,比如类、方法、函数。—- 减少重复代码,提高表达力,提早构建简单抽象。
  • 如果每个例程(类似于函数)都让你感到深合己意,那就是整洁代码,如果例程让编程语言看起来像是专门为解决那个问题而存在,就可以称之为漂亮的代码。—- 如何让编程语言像是专为解决那个问题而存在是程序员的责任。

第二章 有意义的命名

以下建议是有交叉的。

  1. 名副其实:避免模糊度,命名应应与上下文关联,即体现它在这段代码中的作用和身份(date => cacheStepDate)。
  2. 避免误导:避免误导,避免特定名称,避免命名中包含错误的类型,避免拼写相近的命名。
  3. 做有意义的区分:不同的命名应能看出用法区别,避免以下几种命名
    • a1、a2、a3
    • getUser、getUserInfo,并不能看出有什么区别
  4. 使用读的出来的名字:避免自造(包括不恰当的简写)词,要让大家都认的出来。
  5. 使用可搜索的名称:应可检索,如常量应命名为某个变量进行使用。名称长短(或者复杂度)应与其作用域大小对应,如仅用于某个非业务函数内的变量可使用 o、p 等或尽量简短(没有检索需求),而此函数的命名则应表明其作用。
  6. 避免使用编码:避免将类型或作用域编进名称中。
  7. 避免思维映射:不应当让读者在脑中将你的名称翻译成他们熟知的名称。例如不合适的单字母命名,如不合适的简写。
  8. 类名:应为名词。
  9. 方法名:应为动词或动词短语。
  10. 别扮可爱:使用俗语或者你自己能理解的别称是不合适的。
  11. 每个概念对应一个词:即对某个概念的单词选用要统一。如请求都使用 fetch 而非有的地方用 fetch 有的地方用 get。
  12. 别用双关语:同样,在表示不同概念的时候不要用同样的词,如向数组中添加子项,根据添加方式的不同应选用不同的单词,而非都使用 add。
  13. 使用解决方案领域名词:使用计算机科学领域的名词命名而非程序针对的业务领域。
  14. 使用源自所涉及问题领域的名称:在第 13 条无法完全适用时遵循此条。
  15. 添加有意义的语境:在无法使用类、函数、或者命名空间来归纳一系列名称时,要考虑使用统一的命名前缀表明这些量的上下文(归属)。
  16. 不要添加没用的语境:避免命名冗余,如给程序里的每个名称都添加了同样的前缀,或者给某个方法的命名添加了不相关的语境前缀。

第三章 函数

  1. 短小:if 语句、else 语句、while 语句其中的代码块应该只有一行,大抵应该是一个函数调用语句,这样可以配合函数命名增加可阅读性。
  2. 只做一件事:如果函数只是做了该函数名下同一抽象层上的步骤,那么该函数是只做了一件事。结合第三点。
  3. 每个函数一个抽象层级:程序就像是一系列 TO 起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续 TO 起头段落(即函数内局部的一系列代码是否可归纳为了一个目的的操作)。
  4. switch 语句:如果 switch 或 if/else 这种判断语句中直接包含业务逻辑的话,后续的业务扩充可能会影响已有的业务代码的运转,那么可以考虑使用返回不同实例的方式代替业务代码的直接调用,而将这种判断封装在更低层的抽象中。
  5. 使用描述性的名称:等同第一章有意义的命名部分。
  6. 函数参数:减少参数的数量。通过避免标志量,超长数量的参数进行封装等方式。
  7. 无副作用:函数名应能反应函数内容的真实操作,避免“隐瞒”
  8. 分隔指令与询问:指令为操作,询问为判断。即将判断和之后要做的操作分开,而不是直接操作再由其返回判断结果。
  9. 使用异常替代返回错误码:避免在指令中返回状态的一种实现方式,可使用 try catch 统一处理。
  10. 避免重复自己:对使用两次及以上的逻辑和代码进行抽离。
  11. 结构化编程:遵循一定的范式。
  12. 可以写长长的代码,但是别忘了重构并吸取经验。

第四章 注释

好的代码不需要注释。注释是弥补代码表达意图失败的方式。当然不合适的注释也可能产生误导、错误等。

  1. 注释不能梅花糟糕的代码,不如花时间重写。
  2. 用代码来阐述。
  3. 好注释:法律信息;对意图的解释;阐释:解释无法修改的晦涩难懂的函数或返回值;警示;TODO;放大(强调某段代码的合理性,类似 4);文档。
  4. 坏注释:喃喃自语:别人看不懂的注释;多余的注释;误导性注释;循规式注释(并不是所有的函数都需要文档注释);日志式注释;废话注释;能用函数或变量时就别写注释;位置信息(标记代码的特殊注释);括号后的注释(区分代码分层的注释,这时要做的是缩短函数,做抽离);归属与署名(现在依赖代码管理工具);注释掉的代码(为什么没删除,会给人造成困惑);本地信息(注释的内容应紧贴他所描述的代码)。

第五章 格式

就是字面意义的书写格式

  1. 垂直格式:
    1. 向报纸学习:名称简单且一目了然,顶部给出高层次概念和算法,细节向下依次展开。
    2. 垂直方向上的区隔:使用空白行分割功能独立的代码,或类型相近的代码。
    3. 垂直距离:
      • 变量声明应尽可能的靠近其使用位置。
      • 相关函数:有调用关系的函数应该放在一起,且调用者应该在被调用者的上面(函数必须先声明的除外)。
      • 概念相关的代码也应该放在一起。
  2. 横向格式
    1. 空格的使用,如区分运算中的要素 a + b
    2. 水平对齐:对其一组赋值语句的右侧值是没有什么用的。
    3. 缩进:目的是为了方便洞悉文件的结构,立即辨别出有哪些组成。
  3. 团队规则:团队风格大于个人

第六章 对象和数据结构

对象把数据隐藏在抽象之后,思考设计意图,直接提供实现功能的方法。数据结构暴漏其数据,没有提供有意义的函数。 这一章并没有太看懂。

  1. 数据抽象:将数据结构隐藏在接口之下,只对外暴漏功能的实现,而不需要使用者关心你是依靠怎么的数据结构实现的。
  2. 数据(直接操作数据)、对象(对数据结构进行封装提供方法)的反对称性:面向对象的代码想要添加新的功能,需要修改所有的对象实现。添加新的数据结构只需要再实现一个新的对象;面向过程的代码添加新的功能则只需要多加一步判断即可。而添加新的数据结构,不仅要增加一个新的结构的定义,还需要修改所有功能对新的数据结构做兼容。这就是面向对象和面向过程的反对称性,选取合适的实现方式比较重要。
  3. 德墨忒尔律(迪米特法则):一个对象应当对其他对象有尽可能少的了解。即 a 的方法 b 应该只操作 a 的方法属性,或者由 b 创建的数据,或者传入 b 的数据。避免 ctx.query().toLocal().render() 这样就需要持续的关注每个方法返回的是什么,会增加耦合度,前方方法返回值变更后会引起比较大的问题。
  4. 数据传送对象:是只有公共变量没有函数的类。

第七章 错误处理

写出既整洁又强固的代码—-优雅的错误处理。错误处理不应该搞乱代码逻辑。

  1. 使用异常而非返回码:在3.9提到过此条。使用异常可以隔离错误处理和业务逻辑,业务流程中不需要对错误进行关注。
  2. 先写 try-Catch-Finally 语句:能帮助定义代码的用户应该期待什么。
  3. 使用不可控异常:即不要使用 throw 异常(应该是一些语言需要对函数内可能通过 throw 抛出的异常进行提前声明,那么这样就需要从异常抛出函数逐层声明至处理函数)。
  4. 给出异常发生的环境说明:在错误信息中添加失败的操作和失败类型。
  5. 依调用者需要定义异常类:对第三方 API 进行封装,减少依赖。
  6. 定义常规流程:有时候我们会在 catch 中做 try 中失败的业务逻辑,这时,业务逻辑就会被隔离在两块范围,可以对 try 中业务代码做处理,失败时返回“特例”,继续进行业务运算,避免 try catch 的使用。
  7. 别返回 null 值:返回 null 值时我们一般做判断,那么这个时候可以尝试返回一个复合业务数据的“特例”,如正常返回数组,无值时可返回空数组继续后续的数组运算。
  8. 别传递 null 值:其实是指传入不合规的参数会引起异常,而且处理不合规的参数需要做大量的错误处理。

第八章 边界

可控和不可控的边缘。

  1. 使用第三方代码:将第三方代码接口进行封装,而不直接对外暴漏使用,或将其作为参数传递。
  2. 浏览和学习边界:使用针对功能为第三方代码编写测试的方式来进行学习。
  3. 学习性测试的好处:8.2 的方式即为学习性测试,此种方式一来可以精准匹配我们对所需 API 的理解,二来可以及时检测 API 更新引起的变化,三来可以方便迁移和升级。
  4. 使用尚不存在的接口:对正在被提供的接口,先写个 mock 方法,再在功能实现后,把代码放进去。
  5. 整洁的边界:边界上会发生的一件事就是改变,整洁的边界会将改变引起的修改难度降低。我们应避免过多的了解代码中的特定信息,使用适配器等方式,将第三方接口转换成能控制的接口。

第九章 单元测试

  1. TDD 三定律:测试驱动开发:
    1. 在测试代码之前不要写任何业务代码。
    2. 只编写能恰好体现一个失败情况的测试代码。
    3. 只编写恰好能通过测试的代码。
  2. 保持测试整洁:脏测试等于没测试,测试代码需要跟随生产代码的演进而修改,所以测试代码的整洁也很重要。失去了测试代码也就失去了测试代码对业务代码易维护、扩展、复用的驱动。
  3. 整洁的测试:除了遵循整洁代码的要素外,测试代码要遵循构造(测试数据)–操作(测试数据)–检验(操作结果)模式。
  4. 每个测试一个断言:每个测试只测试一个概念,每个测试只有一个断言,测试中的函数名应符合 given-when-then,即极其语义化。
  5. F.I.R.S.T.:几个要素单词缩写
    1. 快速(运行)
    2. 独立:各个测试独立无相互依赖
    3. 可重复:不受环境影响
    4. 自足验证:测试应输出布尔值,即不依赖外物可直接观察测试结果
    5. 及时:在业务代码之前

第十章 类

我们可以把类理解为封装。

  1. 类应该短小:非代码行数上的短小,而是权责上的短小,如果命名无法精确的描述其权责,这个类可能就大了。
    1. 单一权责原则:类和模块应有且只有一个引起修改的理由。如果有则还可以继续拆解为更小的类。
    2. 内聚:类的方法只与类的属性进行操作,很少与外界接触。类的属性被每一个方法使用。
  2. 为了修改而组织:面向修改编程。在如 JAVA 语言中,我们可以通过抽象(抽象和接口,即只提供概念而无具体实现,由继承其的子类去实现具体逻辑)的方式,对不确定性进行隔离。同时也引出了一个概念,基于抽象编程———依赖倒置原则。

Content