代码整洁之道

Clean code

Posted by Bryan on February 17, 2019

基础介绍

程序员荐书的榜单上,屡屡看到《代码整洁之道》 ,处于程序员自我修养的需要,买来拜读一下,按照原书的章节,整理相关内容如下。

全书梗概

有意义的命名

  1. 代码中命名必须名副其实,保证命名不需要额外的注释来解释;
  2. 避免误导性的命名,比如与特定名词相同的命名,以及不同之处较小的命名;
  3. 命名应该是有意义,避免无意义的废号,比如 a, the, variable 等无意义的名字;
  4. 类名的命名应该是名词或名词短语,方法名应该是动词或动词短语;
  5. 在工程中,每个概念对应一个词,并且一以贯之,不要随意更换;
  6. 使用解决方案领域的名称,尽量使用计算机科学术语,算法名,模式名或数学术语,因为阅读你的代码都是程序员,使用程序员共同的词汇易于理解;
  7. 使用源自问题领域的名称,如果某些问题与问题领域相关,可以使用问题领域相关的词汇,这样程序员至少可以请教问题领域相关的专家来交接相关名词;
  8. 给名称添加有意义的语境,如果可以的话,可以使用累,函数,或命名空间来放置名称,提供语境;如果这些都做不到,可以在名称前添加前缀;

函数

  1. 函数最重要的规则就是要短小,短小的代码更易于理解;

  2. 函数应该只做一件事情,判断函数是否只做了一件事的方法:判断函数内的代码是否是同一抽象层上的步骤。另一判断方法是:查看当前函数是否还能被拆出一个函数;

  3. 每个函数只有一个抽象层级,多个抽象层级难以理解,读者难以理解某个表达式是基础概念还是细节,不利于代码的阅读;

  4. switch会导致函数比较大,建议是将switch埋在抽象工厂地下,不需要其他人关注;

  5. 函数参数尽量少,避免三个及以上的参数;

  6. 代码需要保证无副作用,尽量避免执行一些隐含的副作用,而且在函数名上还看不到此副作用。这种代码很容易被错误地使用;

  7. 分隔指令和查询,将修改型的方法与查询型的方法分隔,修改型的方法不要提供返回值;

  8. 使用异常提到错误码,使用错误码的方式就会导致违背指令和查询分隔的原则,这样调用者被迫写出类似如下所示的代码:

    if (deletePage(page) == E_OK)
    
  9. 抽离 try/catch 代码块,将 try/catch 代码块从正常的主体部分抽离出来,因为函数只做一件事,而 try/catch 异常处理本身就是一件事,那么就不应该与正常的逻辑代码在写在同一个函数中了;

  10. 不要重复自己,重复的代码是邪恶的根源;

  11. 结构化编程,每个函数只有一个入口,一个出口,因此代码中只应该出来一个 return 语句;

  12. 不一定最初就要写出完全符合规范的函数,可以先满足需求,接着再打磨和重构代码;

注释

  1. 最好的注释是没有注释,尽量使用代码本身去解释行为,而注释本身也不能挽救差的代码;
  2. 下面是仅剩的值得写注释的情况:
    • 法律信息,比如 copyright 之类的信息;
    • 提供信息的注释,比如使用注释去解释特定的返回值,但是更好的办法是使用函数名称代替注释;
    • 对意图的解释,有时候给出一个特定的解决方案,为了让后来的人理解意图,可以加上注释;
    • 对一些难以理解的参数或返回值的意义的注释,可以将难以理解的值的可读性提高;
    • 警示,对后续修改此段代码的警示;
    • TODO 注释,表示一些后续带完成或优化的内容;
    • 公共 API 中的 Javadoc

格式

  1. 代码格式和可读性会影响可维护性和可拓展行,必须慎重对待;

  2. 垂直格式:

    • 封包声明、导入声明和每个函数之间,都需要空白行隔开;
    • 垂直方向关系紧密的代码应该靠近,关系疏远的代码之间应该分隔;
    • 变量声明应该靠近使用的位置;
    • 相关的函数应该靠近,调用函数应该放在被调用函数的上面;
  3. 水平格式:

    • 单行代码的长度不应该超过可视范围,建议不超过120字符;
    • 根据代码的层次关系正确使用缩进,方便阅读;
    • 空范围的分号需要另起一行并增加缩进,否则很难阅读吗,类似如下:
    while (dis.read(buf, 0, readBufferSize) != -1)
    ;
    
  4. 团队之间应该有统一的格式;

对象与数据结构

  1. 对象与数据结构看起来类似,事实上有一些不同之处,对象将数据隐藏在抽象之后,暴露操作数据的函数;而数据结构则暴露数据,不提供有意义的数据;
  2. 德墨忒尔律:模块不应了解其所操作对象的内部情形,对象主要暴露操作,而隐藏数据,因此不应该给对象添加存取器暴露其内部结构;
  3. 数据传送对象,可以通过构建的数据结构传输一系列数据,此时就不需要增加相关操作,只需要增加存取器即可;

错误处理

  1. 不要使用返回码而应该使用异常,这样方便写出更清晰的代码,否则扰乱使用者,而且使用者不得不将异常处理与正常的业务逻辑写在一起,非常不优雅;
  2. 抛出的每个异常都应该有足够的环境信息,表明错误的来源与出处;
  3. 可以根据自己需要定义异常类,比如在使用第三方API时,可以将第三方API中的异常进行打包,降低对第三方API的依赖;
  4. 特例对象,某些情况下,可以将异常处理转化为返回特定的特例对象,方便调用者写出更漂亮的代码;
  5. 方法不要返回null值,这样会只要使用者没有检查 null 值就会导致NullPointerException , 可以选择抛出异常或者返回特例对象;
  6. 不要传递null值,这样也容易导致异常;

浏览和学习边界

  1. 在使用第三方服务时,为了避免因为第三方服务的修改而需要大量修改业务代码,可以根据业务场景都第三方服务进行封装,定义好相关边界;也可以利用适配器模式对第三方接口进行一对一的封装;

单元测试

  1. 测试驱动开发(TDD)原则:
    • 在编写不能通过的单元测试的代码之前,不能编写生产代码;
    • 只可编写刚好无法通过的单元测试,不能编译也算不通过;
    • 只可编写刚好足以通过当前失败测试的生产代码;
  2. 必须保证测试代码的整洁,否则测试代码质量的低下会导致生产代码的质量的腐坏;
  3. 每个测试只有一个断言,每次测试一个概念,主要强调测试应该尽可能小,不要将多个测试糅合在一起;
  4. 测试应该遵循的原则:
    • 快速,因为测试需要反复多次执行,执行太慢会让使用者失去耐心,放弃测试;
    • 独立,测试用例之间应该是独立的,不应该存在相互依赖的关系;
    • 可重复,测试用例会反复执行,需要保证可重复性;
    • 自足验证,测试用例本身就需要能验证测试是否通过,不应该再依赖日志等去验证测试用例;
    • 及时,测试用例应该及时编写,在生产代码编写之前就应该编写测试代码;

  1. 类的变量和工具函数应该尽量私有化;
  2. 类应该尽量短小,而类的短小在于权责应该尽量少,一般情况下,类应该只有一个权责,也就是说类只有一个修改它的理由;
  3. 类应该保持内聚性,如果类中内聚性不足,可以考虑分拆类;

迭进

  1. 遵循 Kent Beck 关于简单设计的规则,对于设计出好的软件帮助巨大,下面是具体的规则:
    • 运行所有测试;
    • 不可重复;
    • 表达了程序员的意图;
    • 尽可能减少类或方法的数量;
  2. 运行所有测试:保证系统的可测试性,只要系统可测试,就会导向保持类短小且单一的设计方案。保证系统的可测试性,也会将项目导向松耦合的设计方案;
  3. 不可重复:重复是良好测试的大敌,必须努力清除重复;类似的代码也可以通过调整得更相似之后进行重构;
  4. 表达力:提升代码的表达力,让代码易于理解和维护,提升的方法如下:
    • 选用好的名称,好的类名和函数名会利于理解;
    • 保持函数和类的尺寸短小;
    • 通过采用标准命名法来实现,比如通过VISITOR去表达访问者模式,清晰表达你的设计;
    • 编写好的单元测试,单元测试在一定程度上可以起到文档的作用;
    • 最重要的方法还是尝试,在完成功能之后,努力对代码进行重构以便更容易理解;
  5. 尽可能减少类和方法:不要因为教条主义去增加无意义的类和方法,在保证类和方法短小的基础上,尽可能减少类和方法的数量;

并发编程

  1. 并发编程防御原则
    • 单一权责原则,分离并发相关代码与其他代码;
    • 限制数据作用域,临界区的数量越多,可能出错的可能性越大,限制临界区的数量;
    • 使用数据副本,对于需要使用共享数据的地方,看看是否可以通过使用数据副本进行替代,避免增加临界区;
    • 线程独立,将数据尽可能分离,各个线程使用独立的数据,避免数据的共享;
  2. 尽可能减少同步区域,避免不必要的代码加入同步区域;

总结

整体看下来,《代码整洁之道》是一本还不错的技术书,介绍了各种写出更漂亮代码的原则与实践,与《重构》可以配合看,基本的原则就都覆盖到了。但是和《重构》比较起来,后续章节大段粘代码,而且和重构比起来,总结的优雅代码原则的全面性和丰富性上差了一些。比较了一下豆瓣评分,群众的眼睛还是雪亮的。