《代码整洁之道》读书笔记

提升代码能力(•̀⌄•́) 可惜是比较针对java,我又不懂java,只能借鉴一下思想了。
                              —— By Jihan


主题:如何编写好的代码
类别:计算机->实用性->提升代码编写能力
概要:如何定义优秀的代码,如何编写优秀的代码,给出示例具体分析。

前言

写过有几年的代码了,最近在代码架构设计,函数设计,类设计等方面总是不那么通透,期望寻找一些较好的理论和原则,写出更加优雅的代码。期望阅读完本书后,能有一些代码设计编写方面的体会和收获,给出一些理念和原则。

怎样区分好和差代码

程序员的职业操守

应当自己为自己的code负责,应尽力写出整洁优雅的代码,当有狗屎需求来,或者要求效率赶工的时候,应当提出自己的反对观点,指出其后续缺陷影响。要是领导还是让那么干,就屎上堆屎吧,堆不动了就溜了吧。😎

怎么样判断是否是好代码

让人改无所改,没有可优化的余地,尝试改了一圈,还是回到原点。

一些好代码的特性

  1. 能通过所有测试
  2. 没有重复的代码
  3. 体现设计中的全部思想理念,而且尽量简单的体现。
  4. 包括尽量少的实体,比如方法,类,函数等。(主要体现系统架构和逻辑,就是系统只做某一类事情,而不是某一件事情)
  5. 写代码的过程中,读写的比例是10:1,因此让你的代码更易读,可以帮助你自己,也能帮助他人
  6. 时时保持代码的整洁。每次修改代码时,或许你只需要清除无用的代码,修改变量名,清除一点重复的代码……

良好的命名

  1. 良好的命名能够帮助你很多,一旦发现好的命名,就要替换旧的命名。好的命名,能一眼看出你想干什么,你代表什么。
  2. 避免误导性的命名,比如fp, socket等,或者语言特定称呼,比如xxxList,除非真的是list类型,不然最好用其他命名。
  3. 做有意义的区分,而不是一些含义相同,拼写不同的命名。比如productInfo和productData
  4. 不要自己造单词,或者不可读单词。比如什么gmssssasl这种乱七八糟的
  5. 避免使用一些编码和数字,而是用一些宏定义替代它。
  6. 类名和变量名,都应该是名词;函数都应该是动词。
  7. 同一个概念应该对应同一个词汇。

函数

  1. 函数应该做一件事情,做好这一件事情。并且只做这一件事情。
  2. 判断函数是否做一件事情的方法,是函数是否还能拆分,从而改变其抽象层级。
  3. 每个函数中应当只包含一个抽象层级。
  4. 使用表述性词汇,即能描述出函数的功能。而且越精简的函数越好命名。
  5. 尽量少的传递参数,如果超过了三个以上的参数,就需要考虑用类封装了。
  6. 标识参数异常丑陋,因为这表明函数不止做一件事情。
  7. 函数尽量不要暗含其他操作。如果有最好进行说明

注释

  1. 优美易读的代码不需要注释。
  2. 有用的注释:
    1. 法律信息
    2. 提供一些信息,比如wiki链接
    3. 提供意图,为何这么干
    4. 进行一些补充说明
    5. 一些警告,比如前置条件一类
    6. TODO
  3. 避免一些误导性注释,以及一些废话。
  4. 避免循环性注释,也就是每个函数都加什么参数说明这种注释。
  5. 注释也要讲求精炼。

格式

良好的格式更易于阅读。

  1. 适当的缩进和换行
  2. 被调用的函数写在更下面,以方便顺序阅读。C这种有语言限制的除外。
  3. 水平对齐,更方便查看。
  4. 团队应当保持统一的编码风格。

对象和数据结构

  1. 面向过程的代码,便于在已有的数据结构下添加新的函数, 这是以数据结构为中心。 面向对象,便于在已有的函数下添加新的类,以函数为中心,也就是接口为中心。
  2. 什么时候使用面向对象,什么时候使用面向过程,都是值得考虑。而混用是否导致混乱和不必要,也需要进行考虑。比如go中封装的db库,即把它当成一个对象,来操作其方法,又直接获取其子成员包含方法。即没有把对象内部细节屏蔽,又使用了对象的封装,以求实现屏蔽细节。矛盾,需要细细思考。TODO
  3. 对象暴露行为,隐藏数据。数据结构暴露数据,没有什么行为。在合适的地方使用合适的结构。(个人理解,如果提供一种能力,那么则可以使用对象。如果是提供一种状态,则可以使用数据结构。对外提供能力,对内传输数据。向上提供接口,向下读写数据。)

边界

  1. 如何应对第三方接口的修改?
    1. 在第三方接口上进行封装。但是其能力会降低?
    2. 编写相关测试代码,测试第三方程序。以及相应的更新文档。以确定新版本的改动。(风险只能降低,不能避免)
  2. 对于还不存在的代码,我们可以先定义其行为和交互的数据。给一个统一的接口,代码来了直接装填就能用。
  3. 编写代码前,我们应当清晰的定义出软件的边界,以及哪些地方可扩展。对于可扩展的地方要小心谨慎对待,系统对接的地方也是如此,以便以后尽量少的改动我们的代码。

单元测试

  1. 测试应当随着代码的开发跟进开发
  2. 太久不维护测试代码,会导致测试代码失效,导致修改代码的故障率增加,以至于不想不敢去改原始代码。比如我写的AAD模块。
  3. 测试代码覆盖的越全面,你就能更加轻易的修改功能代码。
  4. 测试代码要求简单、精悍、足具表达力。
  5. 一类测试应当有一个断言。
  6. 测试应当是一类作为一个测试,而测试数据应当想办法独立,以保障最大的覆盖面。
  7. 整洁的测试还遵循以下5条规则:
    1. 快速(Fast)测试应该够快。测试应该能快速运行。测试运行缓慢,你就不会想要频繁地运行它。如果你不频繁运行测试,就不能尽早发现问题,也无法轻易修正,从而也不能轻而易举地清理代码。最终,代码就会腐坏。
    2. 独立(Independent)测试应该相互独立。某个测试不应为下一个测试设定条件。你应该可以单独运行每个测试,及以任何顺序运行测试。当测试互相依赖时,头一个没通过就会导致一连串的测试失败,使问题诊断变得困难,隐藏了下级错误。
    3. 可重复(Repeatable)测试应当可在任何环境中重复通过。你应该能够在生产环境、质检环境中运行测试,也能够在无网络的列车上用笔记本电脑运行测试。如果测试不能在任意环境中重复,你就总会有个解释其失败的接口。当环境条件不具备时,你也会无法运行测试。
    4. 自足验证(Self-Validating)测试应该有布尔值输出。无论是通过或失败,你不应该查看日志文件来确认测试是否通过。你不应该手工对比两个不同文本文件来确认测试是否通过。如果测试不能自足验证,对失败的判断就会变得依赖主观,而运行测试也需要更长的手工操作时间。
    5. 及时(Timely)测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。如果在编写生产代码之后编写测试,代码难以测试。你可能会认为某些生产代码本身难以测试。你可能不会去设计可测试的代码。

  1. 类应当尽量短小,一个类不要承载过多的公共函数和变量。
  2. 一个类应当遵循单一原则,只有一个修改原因。
  3. 应当对类加以组织,减少修改带来的影响。

系统

  1. 系统的构建和运行应当分离,意思是系统架构和实际运行的功能应当分离。
  2. 主系统在执行的时候,默认实际运行的功能已经是ok的,主程序不依赖任何实际运行的功能。
  3. 依赖注入,一种强大的机制可以实现分离构造与使用。在依赖管理情景中,对象不应负责实体化对自身的依赖。反之,它应当将这份权责移交给其他“有权力”的机制,从而实现控制的反转。
  4. 扩容是系统必须考虑的事情之一。但是无法一次性规划好系统,只需要持续将关注面进行切分。个人觉得,netfilter的架构是扩展性很好的架构。
  5. 模块化和关注面切分成就了分散化管理和决策。

迭代

Kent Beck关于简单设计的四条规则:

  1. 运行所有测试
    1. 不可测试的系统,不可验证,不可验证的系统不能上线
    2. 良好的测试能促使进一步完善代码。
  2. 不要有重复
  3. 表达程序员意图
  4. 尽可能的减少方法和类
    良好的遵循以上四条规则,能够更好的帮助你重构某个不合理的模块,迭代增加新的功能。

并发编程

并发是一种时机上的解耦

  1. 并发并不总能提高性能。
  2. 并发可能伴随很大程度上的系统改造。
  3. 并发可能需要对系统有更加深入的理解。
  4. 并发开发建议:
    1. 分离并发代码和其他代码
    2. 严谨控制数据的作用域
    3. 尽量使用副本。比如内存池和线程池。
    4. 线程应当尽可能独立。
    5. 详细了解基本的并发之间的基础模型:读写模型,生产者消费者模型,哲学家圆桌模型。
    6. 明确读写锁,死锁,互斥,线程饥饿等概念。
    7. 警惕同步方法之间的依赖,保持同步区域的微小。
    8. 考虑子线程死锁后如何退出线程。TODO
    9. 偶发性错误可能是多线程之间隐藏的难易发现的错误,也要重视。尝试稳定复现。
    10. 编写可插拔的线程代码:可控线程数量,可控线程运行速度,可配置程序运行环境。

结语

总的来说,在有一定开发经验的基础上来阅读本书,是很有帮助的,其中最让我受益的是重新理解了测试的重要性,以及一些使代码整洁的基本原则。
可惜了是java为主的代码示例,让我有些看不懂,或者感触不深,毕竟毫无java开发经验。

-------------本文结束感谢您的阅读-------------