Fork me on GitHub

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

刚进入无二的时候,就被强烈推荐过这本《代码整洁之道》,一直想着去看去看,也终于是没看。前几天又被老大提起这事,想着也是该看一看,就趁一些段段续续的零散时间看完了这本广受美誉的书。

看完掩卷,也的确觉得这本书非常不错,虽然书里面示例代码都是用Java写的,也有一些内容是单纯关于Java的,但有些内容,不管是什么编程语言,是前端还是后端,看完都会有一些感悟和收获。

毕竟,不管什么语言,保持代码的高度精炼和整洁,都是非常有必要性。或者可以更严格的说,整洁的代码不一定是好代码,但好代码一定是整洁的。

这篇博客,是我在阅读的过程中,个人总结和记录的一些比较值得思考和遵循的一些内容,也算做是读书笔记吧。

命名

命名,是编程过程中最常见的事情。但掌握好的命名方式和手法并不是一件容易的事情。
对于命名来说,最基本的原则是易读,易理解,易维护,尽可能的提高名称的准确性。
书中有大有小的提到了以下这些注意事项:

  • 名称要体现它本身要表达的意思或作者的意图,避免模糊不清
  • 避免误导名称,例如那些带有类型的名称,如其实是个对象类型的dataList,和容易混淆字母的相近名称,如loerrorIoerror
  • 使名称易读,尽量使用技术性词汇,而不是口头语和俗语,也不要使用耍宝类型的命名,例如delete_yeye
  • 尽量使用问题解决方案领域的命名,而不是业务领域的命名。因为在大多数情况下,知道这段代码做了什么比知道这段代码要解决什么业务问题重要。
  • 使用易搜索的命名,这包括以下两点:
    • 非必要的情况,避免为名称加上相同前缀,而应该使用尽量小的类和函数来达到标注名称作用域的效果。
    • 尽量避免使用易与其他词汇重复的命名,例如e
  • 在非必要的情况下,别为命名加上类型标记。例如ArrayUsers这样毫无意义的废话。
  • 类名应为名词
  • 方法名应是动作
  • 对同一个概念保持相同的词,例如不要混用getfetchretrieve
  • 为变量添加对应语境,例如UserNameUserPhone,而不是NamePhone。但不要添加无意义的语境,例如为一个类中的所有属性加上类名前缀。

命名是编程过程中非常常见的一个动作,大部分人都是在不断的编程中,慢慢发现准确的表述和定义一个相关抽象的事务和操作为一个名称,其实是一件相当具有难度的事情。给一个函数一个贴切的名称,有时也会大大提高我们代码的清晰和整洁。
避免类似isShow,isEdit这种变量名,相比提升代码整洁度,提升后续维护这段代码的程序员的寿命大概是更直观的作用。。。。

函数

  • 尽量保持短小,缩进层级不应太多,以一层或两层为宜。

  • 只做一件事,应只专注于一个抽象层级,而不是跨越多个。同时尽量保证自顶向下的调用顺序,从而使函数顺序和抽象层级顺序相应。

  • 使用描述性的函数名称,精炼的表达出函数做的事。在名称贴切的前提下,不要去担心函数名称的长度。

  • 避免函数内部的时序性耦合,即避免函数在做一件事的过程中去做另一件在函数名称和参数上完全无法体现或者并非函数本质的事。(例如在一个检查用户名是否符合规范的函数中初始化用户密码

  • 尽量分离函数的查询和指令,避免在一个函数中过度耦合。

在第一遍写函数时,很难将函数的层级和长度控制的非常好。所以,解决办法是,完成功能,写好测试,然后开始重构。
在最理想的情况下,函数要么只进行查询,要么只进行操作。当然,很多时候我们很难将函数都控制的如此理想,对于前端来说,因为数据和操作在大部分时间都是动态绑定的,就更难实现了。但,知道了什么是好的,然后尽量靠近,也是一件非常有助于提升自我的事情啦。

函数参数

  • 避免多参数函数,尽量保证函数参数在三个以下,便于阅读和测试。

  • 避免将标示性的布尔值作为参数传入函数(传入标识符无疑是在宣布函数并不单单做一件事

  • 如果确实需要二元参数的函数,保证其名称准确,最好能体现参数顺序。

  • 如无确实必要,避免使用三个参数及以上的函数

异常处理

  • 使用异常代替返回错误码,这样可以避免对错误码的维护同时可以将错误在统一的地方捕获和处理而不是通过判断错误码来分别处理。
  • 避免将try/catch和正常流程混杂在一起,尽量将try/catch单独抽离为一个异常处理函数。(错误处理就是一件事
  • 尽量避免所有重复的部分,善用函数来抽象它们
  • 在函数长度比较长时,遵循结构化规范,保证函数只有一个入口和出口。

在前端来说,封装请求层,然后在请求层统一过滤和处理错误,将异常捕捉和页面完全分离,可以大大的提升项目的可维护性。
千万避免在数据层,例如vuex中进行错误处理,否则等项目规模稍微一大,你就等着哭吧。

注释

  • 注释相比代码,更容易说谎。因此,尽可能提高代码质量,从而能够减少注释量,记住,再好的注释也无法拯救烂代码。

  • 注释最好只用来:

    • 传递必要的信息
    • 解释意图
    • 提供警示和告知及应被注意的重点位置
    • 提供TODO(记得定期清理)
  • 避免以下类型的注释

    • 喃喃自语,不考虑阅读者的思路能否跟随
    • 废话连篇或毫无价值
    • 具有误导性的片面注释
    • 刻意的遵循格式化注释
    • 日志和署名注释或者注释掉的代码(记住我们有版本管理工具可用
    • 位置标记,例如那种一行等号的注释,可以偶尔使用,但不能滥用。
    • 花括号后的行内注释
    • 相关的代码并不在注释所在位置,即非本地的注释

在敏捷开发中其实比较推荐代码即注释的,但对于前端来说,我是真心觉得代码即注释很难实现,因为前端很难像后端那样将一些东西抽象到很高程度,毕竟你努力抽象半天,产品过来说,你在这给我再加个按钮,你的抽象可能就白抽了。所以很多时候,前端在涉及到比较细粒度的操作时,还是应该加一些注释的。

格式

  • 保证项目文件和团队的代码格式统一
  • 垂直格式,即文件长度,尽量不要太长。同时,代码的细节,应该自上而下逐步展开。
  • 通过空行来进行垂直上的区隔,从而使代码和思路更清晰。
  • 关系密切(例如互相调用的函数,处理相同任务不同状态的函数)的代码,在垂直距离上也应该距离更近。但像类的实例声明,还是应该集中于类的头部。
  • 一行代码尽量不要超出120个字符
  • 不必刻意通过空格来水平对齐
  • 规范的缩进,保证代码嵌套层次清晰

对象和数据结构

  • 做数据抽象,暴露接口,隐藏实现。

  • 面向对象将数据隐藏起来,提供操作数据的函数。面向过程直接处理数据结构。
    举例来说,有多个几何形状正方形,圆形,三角形,计算面积。
    采用面向过程的方法,我们定义三种数据结构和一个计算面积的方法,调用计算面积的方法,传入形状,判断形状再计算面积。我们非常容易再添加计算边长的方法,但如果我们新加形状,就必须修改现有所有和形状有关的方法。
    采用面向对象的方法,我们定义三种对象,每个对象有自己的计算面积方法。非常容易添加新的形状,但如果我们想添加计算边长的方法,就需要去修改每个对象。

  • 面向对象更抽象,易于添加新类型,改动对应类型的方法,但难于为所有类型添加新方法。

  • 面向过程易于为所有类型添加新方法,但添加新类型需要修改现有所有方法。

  • 当形状的方法增加频繁时,例如计算面积,计算周长,计算边数等等等,使用数据结构和面向过程的方式,当形状的种类变化频繁时,例如增加长方形,增加菱形等等,使用对象和面向对象的方式。

  • 在工作中,不带偏见的根据工作性质和需求来选择面向对象和过程的其中一个解决问题。

  • 避免混杂对象和数据结构。数据结构中并未完全不能定义方法,但必须避免将业务方法代码封装在诸如active record这种数据结构中,非常容易造成难以挽救的混乱。

  • 得墨忒定律,即对象方法应该只跟邻居对话,不与陌生人交谈。邻居有以下几个定义:

    • 类与其实例出的对象
    • 函数与传递给它的参数
    • 对象和对象的属性
      类似a().b().c()的调用方式是不推荐的。
      对于数据结构来说,因为它直接暴露了自身,所以得墨忒定律是不适用的。

错误处理

  • 保证错误处理的条理和与业务逻辑的区隔,避免凌乱,防止其打乱正常的代码逻辑
  • 使用异常而不是错误标识(例如错误码), 因为异常我们可以统一捕获和处理而不打乱正常逻辑
  • try中的代码当做事务,而catch中的代码则是事务状态的保证
  • 保证异常携带充分的错误信息
  • 通过打包封装API来将异常控制在特定的区域
  • 不要返回NULL值,这会造成后续的所有代码都陷入不断判断NULL值的尴尬境地,抛出异常或者返回一个特例对象来代替返回NULL值。
  • 不要定义显式可接收NULL值的函数,保证所有函数都是禁止接收NULL值的,从而在真正出现NULL值的函数参数时就可以明确的知道发生了错误。

边界

第三方库通常都追求普适性,但我们在使用时则想满足特定需求,这就需要我们保证代码良好的边界。

  • 通过封装来修整第三方库或者依赖,从而保证第三方库的边界不会深入系统。
  • 通过完善的测试来覆盖第三方库的调用,从而能够保证第三方库不断升级过程中仍与系统的兼容,而不是只能长久的绑定在旧版本上。
  • 对于第三方库这种我们无法控制的代码,保证整洁的边界能大大降低我们在未来改动和更新依赖的代价。

单元测试

  • 测试驱动开发,即TDD,能够大大降低代码修改和迭代的代价。它需要保证以下三条定律:
    • 先写测试,后写代码
    • 只编写刚刚开始无法通过的测试
    • 只编写刚刚开始通过测试的代码
      这也就意味着我们的测试和代码互相推动着同时前进。
  • 测试代码同样要保证整洁,可维护。
  • 测试代码最重要的是可读性。保证测试代码的简明和有力。
  • 测试代码并不需要很多生产代码的限制,例如性能方面,并不那么重要。
  • 每个测试只测试一个概念。
  • 好的测试,要保证F.I.R.S.T特性
    • 快速Fast,保证测试够快,才能频繁运行
    • 独立Independent,测试应该独立而不能互相依赖。
    • 可重复Repeatable,测试应该在各种环境中都能通过。
    • 自足验证Self-Validating 测试应该只客观的返回成功或失败
    • 及时Timely,测试应该刚刚好在开发的前面一点点,不能落后

测试,是保证代码可维护,可扩展,可复用的生命力的最有力工具。拥抱测试,就是在拥抱代码的健壮性,为代码延长寿命。但很可惜,大家都知道测试好,但都被需求的鞭子追着跑,管不了,唉,一声叹息。

  • 除非测试需要访问,否则尽量将类的属性和方法设置为私有的,提升类的封装能力。

  • 类应该和函数一样,尽量保持其的短小,避免涉及到各种公用方法和属性的神之类。一定规模的系统都会包含大量的逻辑和复杂性,通过组织短小单一的类,可以将特定时间和范围内的维护复杂性降低。

  • 类的名称应该精确描述其权责,避免名称中出现ifandbutor等关系词汇。当出现类似词汇时,说明类具有了太多权责。
    类应该遵循单一权责原则SRP:类或模块应该只有一条修改的理由。

  • 通过方法覆盖仅可能多的类变量来保持类的内聚性,当方法只能覆盖一部分类变量时,说明我们需要将类拆分为更短小更内聚的类。

  • 类应该对修改封闭,对扩展开发。即我们使用许多内聚性较高的类,在新增功能时,就可以更多的通过增加代码实现,而可以较少的去修改代码,

Vue中组件和类非常像,Angular中,组件就是个类。因此,对于前端组件来说,类的很多东西也是可以套用的。比如避免命名中有if,or,有这种单词时,说明组件是应该被拆分的。避免有N多功能的神之组件,使组件自身方法覆盖更多部分的组件数据,从而促使自己去拆分数据,提升组件的内聚程度等等,本书中关于Java类的一些内容,倒是触类旁通的让我对Vue组件的认识又加深了一些。

总的来说,《代码整洁之道》这本书真的是非常不错的一本提升程序员编程水平和认知的书,认真的读下来,让人受益匪浅,很多道理,也可以看出是作者长久编程中悟出来的,能让我等菜鸟少走许多弯路。
当然,提升代码整洁和可读性,不是通过看会书就能学会的。最重要的,还是多写多写再多写,在写的过程中不断反思和总结,将书中的内容融会贯通,才能成为真正的好程序员,与大家共勉。

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