《Clean Code》阅读笔记

转载自网络

这是一本真正的好书,不过如果读者没有一定的经验,以及缺乏对编程境界的追求的话,可能认为这本书很一般。当然,对于有心人来说,这本书里面的部分东西可能都已经习以为常了。

那么,你是怎样的呢?

另外我为什么写的是《Clean Code》而不是《代码整洁之道》,因为这本书很多地方你需要看原版的文字才能get到作者真正想表达的意思。如果有能力还是看原版吧。

看原版书,你能学到很多术语表达,在你看外文技术文章的时候更容易帮助你理解全文。如increase cohesion - 增加内聚性,decrease coupling - 减少耦合,separate concerns - 关注点分离,modularize system concerns - 模块化系统关注点,这些都是很经典的表达。

I sincerely and strongly recommend u to read 《Clean Code》 rather than 《代码整洁之道》

一、整洁代码 ⭐

关键词:优雅

  1. 代码逻辑直接了当,让缺陷难以隐藏
  2. 尽量减少依赖关系,使之便于维护
  3. 依据某种分层策略完善错误处理代码
  4. 性能调至最优,省得引诱别人做没规矩的优化
  5. 整洁的代码只做一件事
  6. 简单直接,具有可读性
  7. 有单元测试和验收测试
  8. 有意义的命名
  9. 代码应在字面上表达其含义
  10. 尽量少的实体:类、方法、函数
  11. 没有重复代码

整洁的代码读起来令人愉悦

二、有意义的命名 ⭐⭐

  • 使用带有语义的命名,能 够让维护代码的人更容易理解和修改代码
  • 编程本来就是一种社会活动(大部分的编程活动都是人与人协作的过程)
  • 避免思维映射,明确才是王道
  • 尽可能要做到“顾名思义”,看到名称就能知道这个变量、函数、类、包的意义、用途。

具体规则

  1. 名副其实:名称不需要注释补充就可见其含义、用途

  2. 不要写多余的废话或者容易让人混淆的命名。

    比如"customerObject"和"customer", "ProductInfo"和"ProductData";这种就是意义混杂的废话。如果真的有区别,就用特定的可以区分的命名来描述它。

  3. 使用读得出来的名称。

  4. 使用可搜索的名称。

    MAX_CLASSES_PER_STUDENT很容易,但想找数字7就麻烦了。

  5. 类名和对象名应该是名词或名词短语。

  6. 方法名应当是动词或动词短语。

    如postPayment、deletePage或save。属性访问器、修改器和断言应该根据其值命名,并依Javabean标准加上get、set和is前缀。

  7. 每个抽象概念选一个词,并且一以贯之

    我的理解中,在同个领域模型中,就应该只有一个命名,比如订单号,同个系统中不应该出现TradeNo、OrderNo等多个命名。

  8. 尽量用术语(CS术语,算法,数学术语)命名

    尽管用那些计算机科学(Computer Science,CS)术语、算法名、模式名、数学术语。

  9. 上一条无法做到的情况下,尽量使用源自所涉问题领域的名称。

    如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称。

  10. 添加富有意义的语境,例如利用UserInfo类封装各种个人信息

三、函数 ⭐⭐

编程就像讲故事,要用准确、清晰、富有表达力的语句(代码)

  • 好的函数应该做到自顶向下阅读代码时,像是在阅读报刊文章。
  • 写代码很像是写文章。先想怎么写就怎么写,然后再打磨:分解函数、修改名称、消除重复
  • 编程其实是一门语言设计艺术,大师级程序员把程序系统当做故事来讲。使用准确、清晰、富有表达力的代码来帮助你讲故事。

具体规则

  1. 短小!短小!短小

    重要的事情说3遍。

  2. 函数应该做一件事。做好这件事。只做这一件事

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

    这个是编者认为非常重要的一点,也是本人在开发过程当中看到最多的问题。应该处于不同抽象层级的代码混乱在一起时,阅读和理解起来会很痛苦。

    引原文描述:

    函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。

    但是,就像作者说的,这条规则很难

  4. 使用描述性的名称

    长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。

    为只做一件事的小函数取个好名字。函数越短小、功能越集中,就越便于取个好名字。

  5. 拒绝boolean型标识参数。

    例: CopyUtil.copyToDB(isWorkDB) --> CopyUtil.copyToWorkDB(), CopyUtil.copyToLiveDB()

    (但是编者阅读很多源码里面也没有遵守,手动狗头...)

  6. 如果一定需要多个参数,那么可能需要对参数进行封装

  7. 使用异常代替返回错误码,错误处理代码就能从主路径代码中分离出来得到简化。

  8. Don't Repeat Yourself(经典的DRY原则)

  9. 先把函数写出来,再规范化

四、注释

这节实际上内容不多,尽量避免注释

  • 别给糟糕的代码加注释(专家建议不如重写)
  • 把力气花在写清楚明白的代码上,直接保证无需编写注释。
  • 好的注释:
    • 法律信息
    • 提供信息
    • 解释意图
    • 警示
    • TODO注释

五、格式 ⭐

  • 代码格式很重要。代码格式关乎沟通,而沟通是专业开发者的头等大事。
  • 向报纸格式学习代码编写。

具体规则

  1. 垂直距离

    1. 变量声明应该尽可能靠近使用位置,本地变量应该在函数顶部出现
    2. 实体变量应该放在类的顶部声明
    3. 相关的函数应该放在一起
    4. 函数的排列顺序保持其相互调用的顺序
  2. 水平位置

    1. 一行代码尽量短,不超过100、 120、150 个字符。

      这个在常见的IDE中可以设置提示线。下图是IDEA的配置位置。

      remote

      效果:

      remote
    2. 用空格将相关性弱的分开

    3. 声明和赋值不需要水平对齐

    4. 注意缩进

  3. 团队之间形成一致的代码格式规范(Checkstyle 插件了解一下?)

    不要使用不同的风格来编写源代码,会增加其复杂度。

六、对象与数据结构

这块有两个我比较在意的概念

  • 要弄清楚数据结构和对象的差异:对象把数据隐藏于抽象之后,曝露操作数据的函数。数据结构曝露其数据,没有提供有意义的函数

  • The Law of Demeter:模块不应了解它所操作对象的内部情形。

    更准确更白话地说:方法不应调用由任何函数返回的对象的方法。只跟朋友谈话,不与陌生人谈话。

    反例:

    final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
    

七、错误处理 ⭐⭐

  • 一个原则:错误处理很重要,但是如果它搞乱了代码逻辑,就是错误的做法

  • 整洁代码是可读的,但也要强固。可读与强固并不冲突。如果将错误处理隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码。做到这一步,我们就能单独处理它,也极大地提升了代码的可维护性。

具体规则

  1. 使用异常而非返回码

  2. 使用不可控异常(这点深有体会,checked Exception的代价很大)

    这里作者想说明的是,在使用受检异常时,你首先要考虑这样是否能值回票价。因为受检异常违反了开闭原则,当你在一个方法内抛出了受检异常时,你就得在catch语句和抛出异常之间的方法调用链中的每个方法签名中声明这个异常。

    这意味着,你对软件较低层级的修改,会涉及到较高层级的签名。封装被打破了,因为在抛出路径中的每个函数都要去了解下一层级的异常细节。既然异常旨在让你能在较远处处理错误,可控异常以这种方式破坏封装简直就是一种耻辱

    如果你在编写一套关键代码库,则可控异常有时也会有用:你必须捕获异常。但对于一般的应用开发,其依赖成本要高于收益。

  3. 给出异常发生的环境说明(这个也很重要)

    创建信息充分的错误消息,并和异常一起传递出去。在消息中,包括失败的操作和失败类型。如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来。

    良好的日志和异常机制,是不应该出现调试的。打日志和抛异常,一定要把上下文给出来,否则,等于在毁灭命案现场,把后边处理问题的人,往歪路上带。

    需要调试来查找错误时,往往是一种对异常处理机制的侮辱

  4. 使用通用异常类打包第三方API包的异常(如调用一些第三方支付SDK等)

  5. 尝试使用特例模式(SPECIAL CASE PATTERN),将异常行为封装到特例对象中。

    很巧妙高级的一种设计模式。

    // 修改前
    try {
        MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
        m_total += expenses.getTotal();
    } catch(MealExpensesNotFound e) {
        m_total += getMealPerDiem();
    }
    
    
    // 优化之后,当没有餐食消耗(即上述代码抛出MealExpensesNotFound的情况),返回特例对象
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
    
    // 特例对象
    public class PerDiemMealExpenses implements MealExpenses {
        public int getTotal() {
        // return the per diem default
        }
    }
    
  6. 不要返回null,不要传递null

    相信不少程序员都深受null 的困扰。返回null值,基本上是在给自己增加工作量,也是在给调用者添乱。只要有一处没检查null值,应用程序就会失控。

    在大多数编程语言中,没有良好的方法能对付由调用者意外传入的null值。事已如此,恰当的做法就是禁止传入null值。

八、边界

边界这一章个人读起来比较难懂。感觉像是翻译的问题。

原书这一章节的名字叫做"Boundaries"。

这一章篇幅较短,意义有点难懂,这里简单总结:作者的意思是让我们自己的代码和第三方库的代码不要耦合太紧密,需要有清晰的Boundaries。

同时也给出了第三方类库的学习建议:探索性地学习测试,以此熟悉类库,写出良好的代码。

九、单元测试

  • 测试代码和生产代码一样重要。它可不是二等公民。

    它需要被思考、被设计和被照料。它该像生产代码一般保持整洁。

    测试代码需要随着生产代码的演进而修改,如果测试不能保持整洁,只会越来越难修改。

  • 整洁的测试有什么要素?有三个要素:可读性,可读性和可读性

  • 每个测试一个断言,每个测试一个概念。

单测本身也应该成为Code Review的一部分,单测写的好,bug一定少。

TDD 三定律

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

任何一种迭代和增量的交付方式,都会遇到一个严肃的灵魂拷问:频繁对软件做修改,如何保障软件不被改坏?这个问题,用人肉测试解决不了。交付越频繁,人肉测试就越不可能跟上节奏。自动化的、快速且可靠的、覆盖完善的测试必不可少。这种要求,后补式的、黑盒的测试方法不可能达到,必须在开发软件的过程中内建。

当团队被迫采用迭代和增量的需求管理和项目管理方式,对应的配置管理和质量保障手段就必须跟上。TDD不是锦上添花,而是迭代和增量交付不可或缺的基石

F.I.R.S.T.

整洁的测试应该遵循以下5条规则:

  • 快速(Fast)

    测试应该够快。测试应该能快速运行。测试运行缓慢,你就不会想要频繁地运行它。如果你不频繁运行测试,就不能尽早发现问题,也无法轻易修正,从而也不能轻而易举地清理代码。最终,代码就会腐坏。

  • 独立(Independent)

    测试应该相互独立。某个测试不应为下一个测试设定条件。你应该可以单独运行每个测试,及以任何顺序运行测试。当测试互相依赖时,头一个没通过就会导致一连串的测试失败,使问题诊断变得困难,隐藏了下级错误。

  • 可重复(Repeatable)

    测试应当可在任何环境中重复通过。你应该能够在生产环境、质检环境中运行测试,也能够在无网络的列车上用笔记本电脑运行测试。如果测试不能在任意环境中重复,你就总会有个解释其失败的接口。当环境条件不具备时,你也会无法运行测试。

  • 自足验证(Self-Validating)

    测试应该有布尔值输出。无论是通过或失败,你不应该查看日志文件来确认测试是否通过。你不应该手工对比两个不同文本文件来确认测试是否通过。如果测试不能自足验证,对失败的判断就会变得依赖主观,而运行测试也需要更长的手工操作时间

  • 及时(Timely)

    测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。如果在编写生产代码之后编写测试,你会发现生产代码难以测试。你可能会认为某些生产代码本身难以测试。你可能不会去设计可测试的代码

十、类 ⭐⭐

类应该尽量短小

对于衡量类的大小,这里书中提出了一个不同的衡量方法:计算权责。我理解的意思就是,一个类承担了太多的权责之后,这个类就算大了。

所以书中随即提出了SRP - 单一权责原则(也叫单一职责原则)

单一权责原则

单一权责原则(SRP)认为,类或模块应有且只有一条加以修改的理由。该原则既给出了权责的定义,又是关于类的长度的指导方针。类只应有一个权责——只有一条修改的理由。

作者还提到了,系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。

内聚

同时,作者提出了保持内聚性就会得到许多短小的类。

类的高内聚的含义是:类的实体变量应尽可能少,类中方法尽可能多地使用到这些变量。(如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性)

组织类时考虑代码的修改

在整洁的系统中,我们对类加以组织,以降低修改的风险。

  • 开放-闭合原则(OCP)

    类应当对扩展开放,对修改封闭。通过子类化手段,类对添加新功能是开放的,而且可以同时不触及其他类。

  • 依赖倒置原则(Dependency Inversion Principle,DIP)

    DIP认为类应当依赖于抽象而不是依赖于具体细节。通过这种抽象隔离了系统之间的元素,使得系统每个元素的理解变得更加容易,使用起来更加灵活、更加可复用。

十一、系统

系统构造与使用分开。

这里我理解就是将一些对象实例的初始化和使用分离解耦,将构建实例的逻辑交给一个公共的模块/类/框架来做。这里作者也介绍了开发中常见的两种方式,体现了这种思想:

  • 工厂:使用工厂方法自行决定何时创建实例,但是构造细节却在其他地方

  • 依赖注入:当A对B有依赖时,A中不负责B的实例化(这就是类的权责单一原则

后半章主要讲的是AOP的思想和具体的框架实现。就是说将一些重复性、功能性的代码(如:性能监视、日志记录、事务管理、安全检查、缓存等)进行关注面切分,模块化,成就了分散化管理和决策。最终的效果也显而易见,减少了重复代码,关注面的分离也使得设计、决策更加清晰容易。

十二、Emergence (迭进) ⭐

这一节主要是讲了四个简单的设计规则(design rules),通过遵循这四个规则,你可以编写出很好的代码,深入了解代码的结构和设计,继而以一种更简单的方式来学习掌握SRP和DIP之类的设计原则。

Four rules of Simple Design are of significant help increating well-designed software

  • 运行所有的测试

    全面测试并持续通过所有测试。遵循SRP的类,测试起来较为简单。测试编写得越多,就越能持续走向编写较易测试的代码。所以,确保系统完全可测试能帮助我们创建更好的设计。

    有了全面的测试保驾护航之后,我们就有条件一步一步地去重构完善我们的代码,目的是为了得到“高内聚,低耦合”的系统。书中也提出了下面三条简单的规则。

  • 不要重复(DRY)

  • 写出能清晰表达编码者意图的代码(Expressive)

  • 尽量减少类和方法(Mininal Classes and Methods)

    当你在重构时,按照SRP、代码可读性等规则遵守,是有可能创建出比原来更多的细小的类。但这不在本条的针对范围之内。

    这里的尽量减少,作者举例了一种情况,就是毫无意义的教条主义会导致编码人员无意识的创建很多的类和方法。不知道你有没有类似的经历,我拿我亲身体会举个例子,我很难理解在某个项目中,对一个领域对象(如User),在构建对应的Service层和Dao层的时候,一定要为每个类创建接口,即使这些接口根本不可能有其他的实现类。

十三、并发

“Objects are abstractions of processing. Threads are abstractions of schedule.” — James O. Coplien

这一节作者讨论了并发编程的需求和难点,并且给出了一些解决这些困难和编写整洁并发代码的建议。因为关于并发编程有更好的资料可以学习,所以这里我就简单总结一下。

并发防御原则

  • 单一权责原则(SRP):方法/类/组件应当只有一个修改的理由
  • 限制数据作用域:严格限制对可能被共享的数据的访问
  • 使用数据复本:这点很好理解,避免数据的共享。(Java 中的ThreadLocal)
  • 线程应尽可能独立:不与其他线程共享数据。每个线程处理一个客户端请求,从不共享的源头接纳所有请求数据,存储为本地变量

小结

其他未提到的章节,是我觉得相较来说非重点的章节。还有可能会有一些内容的遗漏,因为这本书中的精华,我觉得我还需要学习领会。

好书常读常新,这本书就在我的工位上,我希望在经历一段时间的工作实践之后,再次打开这本书,我能有更多更新的一些感悟。