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

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

马草原 801 2022-08-11

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

代码整洁之道

引言

《代码整洁之道》是一本旨在帮助软件开发者写出简洁、清晰、易读、易维护的高质量代码的经典之作。

当代软件开发面临着不断增长的复杂性与日益迫切的时间压力,而《代码整洁之道》(Clean Code) 一书正是为解决这些挑战而诞生的指南。在这本由Robert C. Martin所著的经典之作中,我们将领略到代码优雅之美,深入了解如何以清晰简洁的方式编写出高质量的代码。
在软件开发领域,代码是我们的创作。然而,当代码不断演进,修改与添加时,随之而来的是增长的复杂性与可读性下降,这不仅会影响开发效率,还会导致更多的错误和难以维护的系统。《代码整洁之道》为我们提供了解决这些问题的宝贵建议。
本书将深入探讨代码整洁的概念与原则,并介绍了一系列实用的技巧与模式,帮助我们写出简单、清晰、易读、易维护的代码。通过遵循这些准则,我们能够更好地组织代码结构、取舍命名与注释,以及保持代码的灵活性与可复用性。此外,作者还通过生动的案例和贴近实际开发场景的示例,让读者深刻体会代码整洁之道的重要性和实际应用。
这本书的意义不仅在于提供了具体的编程技巧,更重要的是它唤起了我们对代码质量的关注和追求。通过学习与实践,我们能够成为更出色的软件工程师,提高团队的开发效率,降低维护成本,从而创造出更为可靠、健壮的软件系统。
让我们一同踏上这段关于代码整洁之道的探索之旅,相信在这个过程中,我们会受益匪浅,编程技艺将得到长足的提升。无论是初涉编程领域的新手,还是经验丰富的资深开发者,这本书都将为我们带来新的启示与领悟。让我们砥砺前行,共同追求代码的完美与优雅!

一、代码格式

  1. 风格保持一致性:代码应该遵循统一的格式和风格。无论是缩进、括号的位置、命名规范还是注释风格等,都应该保持一致,这样可以使代码更易读、易懂,并且减少团队成员之间的认知差异。
  2. 使用有意义的命名:变量、函数、类等的命名应该清晰、具有描述性,能够准确地表达其用途和含义。避免使用简单的单字母命名或者过于晦涩的缩写,而应该选择能够清楚传达意图的命名方式。
  3. 适度添加空白行:通过合理地使用空白行可以提高代码的可读性。在逻辑上相关的代码块之间插入空白行,以及在函数定义和类声明之间留出适当的间隔,可以使代码结构更清晰,便于阅读和理解。
  4. 正确使用注释:注释应该清晰、简明扼要地解释代码的意图和目的。避免不必要的注释或者过多冗长的注释。代码本身应该尽可能地自我解释,注释只应在需要解释特殊处理或者是复杂的业务逻辑时使用。
  5. 限制代码行的长度:过长的代码行会导致阅读困难,应该尽量限制在某个合理的字符数范围内。通常建议不超过80-120个字符。对于过长的表达式或者函数调用,可以使用适当的换行和缩进来提高可读性。

二、对象和数据结构

  1. 对象应该暴露行为,隐藏内部的实现细节:对象应该通过方法提供抽象接口来操作其内部状态,而不直接暴露其数据。这样可以保持对象的封装性灵活性,并减少对内部结构的依赖。
  2. 避免混淆对象和数据结构:避免将对象和数据结构混合在一起,因为它们具有不同的目的和责任。对象应该具有行为和封装性,而数据结构只专注于数据的存储和访问不牵连业务逻辑。
  3. 数据对象的传递应避免直接暴露数据:在传递数据对象时,可以通过使用封装的数据结构来隐藏和保护数据,同时提供访问数据的方法。

三、错误处理

  1. 编写清晰的错误处理代码:良好的错误处理代码应当具有清晰整洁的逻辑结构和可读性。将错误处理代码与正常逻辑分开,并使用合适的命名、注释和日志记录来提高代码的可读性。
  2. 不要返回Null值或者传递Null值:Null值没有明确的语义、降低代码的可读性、还可能引发空指针异常。
  3. 使用不可控异常:可控异常会改变方法签名,需要声明可能抛出的异常,对调用者的方法签名也会有影响,依赖成本太高。

四、单元测试

  1. 保持测试代码的整洁:测试代码和生产代码一样重要,应该像生产代码一样保持整洁。
  2. 一个测试方法只测试一种概念:不要写庞大的单测方法,一个单测方法只测试一种场景,简洁明了。
  3. 构造-操作-检验:遵循构造-操作-检验的结构编写测试,每个测试一个断言校验。
  4. F.I.R.S.T.:单测要做到可以快速运行、独立运行、可重复运行、自足验证(给出布尔值的测试结果),并且要及时编写测试程序。

五、系统

  1. 系统的构造和使用分开:使用依赖注入、控制反转、工厂模式等手段主动分解依赖。
  2. 考虑扩容和升级:在系统的设计上要考虑到未来的扩容升级,降低耦合度,方便修改。
  3. 测试驱动系统架构:通过测试驱动的方法,可以在系统开发阶段发现问题,并及时整改,提高系统的质量和可维护性。

六、 迭进

  1. 通过迭进设计达到整洁目的:在开发过程中,不断地进行小规模的迭代和改进,以逐步提高代码的质量和可维护性。
  2. 运行所有测试:测试可以保证代码正确性,避免在迭代中引入新问题。
  3. 重构:在迭代过程中应该不断的停下思考,不断重构,保持代码的质量。
  4. 不可重复:消除重复代码,使系统更简洁,尽可能少的类和方法,短小精悍的类和方法。

七、味道与启发

  1. 注释:

    1. 不恰当的信息:注释信息和代码毫不相关的、不恰当的。
    2. 废弃的注释:过时、无关或不正确的注释就是废弃的注释。
    3. 冗余注释:如果注释描述的是某种充分自我描述了的东西,那么注释就是多余的。
    4. 糟糕的注释:需要解释的注释是糟糕的。
    5. 注释掉的代码:代码不需要就删除,不要害怕删除代码,版本控制系统会保存它的所有历史版本。
  2. 环境:

    1. 需要多步才能实现的构建:不应该依赖各种环境因素,应该可以通过简单的命令轻松构建。
    2. 需要多步才能做到的测试:应当能够发出单个指令就可以运行全部单元测试。
  3. 函数:

    1. 过多的参数:函数的参数量应该少。没参数是最好的。
    2. 标识参数:布尔值参数大声宣告函数做了不止一件事。它们令人迷惑,应该消灭掉。
    3. 短小、单一职能:函数应该短小精悍,一个函数只做一件事,目的明确。
  4. 一般性问题:

    1. 一个源文件中存在多种语言:一个源文件混杂多种不同的语言往往会令人迷惑,降低代码可读性。
    2. 明显的行为未被实现:函数的目的未被实现,读者无法信任作者,需要深入实现确认代码详细逻辑。
    3. 忽视安全:代码安全问题非常重要,不要留下任何一个隐患。
    4. 死代码:死代码就是不执行的代码、没有意义的代码。
    5. 垂直分隔:变量、逻辑代码段应该尽可能的缩小垂直距离,读起来思路连贯。
    6. 耦合:不互相依赖的东西不该耦合。
    7. 晦涩的意图:代码要尽可能具有表达力。
    8. 用命名常量替代魔法值:增加代码的可维护性和可读性。
    9. 通过使用通配符避免过长的导入清单:过长的导入清单令读者望而却步,可以使用通配符导入整个包。
    10. 不要继承常量:使读者疑惑,降低代码可读性。
  5. 名称:

    1. 采用描述性名称:不要太快取名。确认名称具有描述性,能够做到见名之意。
    2. 尽可能使用标准命名法:如果名称基于既存约定或用法,就比较易于理解。
    3. 无歧义的名称:选用不会混淆函数或变量意义的名称。
  6. 测试:

    1. 使用覆盖率工具:使用IDE的覆盖率工具来监控测试,不要认为“看起来够了”。
    2. 使用小测试:一个测试只测试一个小功能,职责清晰 易于维护。
    3. 测试应该快:慢速的测试是不会被运行的测试。时间一紧,较慢的测试就会被摘掉。
    4. 有意义的测试:测试是为了保证代码符合预期,不要为了覆盖率而写测试。

八、并发编程

为什么需要并发编程?

  1. 提高程序性能:并发编程可以将任务分解为多个并发执行的子任务,从而利用多核处理器和多线程的优势,提高程序的整体性能。通过并行处理,可以更高效地利用计算资源,加快任务的完成速度。
  2. 缩短程序的响应速度:在单线程的程序中,如果遇到耗时的操作,会导致整个程序阻塞,无法响应其他任务或用户请求。而通过并发编程,可以将耗时的操作放到独立的线程中进行,并发执行其他任务,从而使程序更具有响应性,提高用户体验。
  3. 提高资源利用率:并发编程可以更好地利用计算机资源。通过将任务并发执行,可以减少空闲时间,使CPU、内存和其他资源得到更充分的利用,提高系统的资源利用率。
  4. 改善程序结构和设计:并发编程要求将程序分解为独立的任务和模块,这可以促使开发人员更好地组织代码、模块化程序,并提高代码的可读性和可维护性。并发编程也能够促使开发人员更深入地思考程序的并发性和线程安全性,从而提升代码的质量

带来的挑战、问题:

  1. 竞态条件:当多个线程同时访问和修改共享数据时,可能会导致竞态条件。竞态条件可能会导致不确定的结果或不正确的程序行为。
  2. 死锁:死锁是指两个或多个线程彼此等待对方持有的资源,导致程序无法继续执行。这种情况下,线程会被永久阻塞。
  3. 线程安全性:在并发编程中,需要保证多个线程对共享数据的访问是安全的,不会出现数据竞争和不一致的情况。确保线程安全性是一个重要的挑战。
  4. 线程切换开销:由于操作系统在多个线程之间切换执行,会导致一定的开销。频繁的上下文切换可能会影响程序的性能。
  5. 活锁:活锁是指线程不断地相互响应对方,但却无法继续执行的情况。线程在活锁中没有进展,程序无法完成任务。
  6. 饥饿:饥饿指的是某个线程无法获取所需的资源,因为其他线程一直占用着这些资源。饥饿可能导致线程无法继续执行,从而降低程序的性能和响应能力。

并发编程防御原则

  1. 单一权责原则:并发相关代码应该从其他代码中分离出来,因为并发实现本身是复杂且具有挑战性的。并发代码应具有自己的开发、修改生命周期。
  2. 限制数据作用域:通过限制共享数据的作用域,可以避免多个线程同时修改共享对象的同一字段,减少互相干扰和未预期行为的可能性。使用 synchronized 关键字在临界区中保护共享对象的使用。
  3. 使用数据复本:避免共享数据的一种方法是复制对象并以只读方式使用。在某些情况下,可以将结果从多个线程收集到单个线程中合并。使用数据复本可以减少共享数据带来的错误可能性。
  4. 线程应尽可能地独立:让每个线程在自己的世界中存在,不与其他线程共享数据。每个线程处理一个客户端请求,接收请求数据作为本地变量,避免同步问题。将数据分解为可由独立线程操作的独立子集。这些原则和技巧可以帮助我们在并发编程中避免一些常见的问题,提高代码的可靠性和可维护性。然而,正确的并发编程仍然是一项复杂的任务,需要综合考虑各种因素,包括线程安全性、同步机制、锁策略等。

警惕同步方法之间的依赖

在并发代码中,如果有多个同步方法依赖于同一个共享对象,那么就可能会导致潜在的问题和隐含的缺陷。

问题:

  • 锁的范围不正确:如果多个同步方法使用不同的锁对象,或者锁的范围不足以覆盖整个操作,就可能导致数据竞争和并发问题。
  • 死锁:如果多个线程同时持有不同的锁,并且彼此等待对方释放锁,就可能发生死锁情况。
  • 饥饿:某些线程可能会长时间等待获取锁,而无法执行操作,导致资源饥饿问题。

解决办法:

  • 基于客户端的锁定:在客户端代码中,确保在调用多个同步方法之前,先对共享对象进行锁定,以确保锁的范围涵盖了所有相关方法的执行。
  • 基于服务端的锁定:在共享对象内部创建一个方法,该方法对共享对象进行锁定,并依次调用所有需要同步的方法,然后解锁。客户端代码只需调用该新方法,而不是直接调用多个同步方法。
  • 适配服务端:创建一个中间层,该层执行基于服务端的锁定逻辑,但不修改原始的服务端代码。这样可以保持原始代码的一致性,并在中间层中管理锁定和解锁的操作。
public class SharedObject {
    public synchronized void method1() {
        // Method 1逻辑
        method2(); // 调用了method2()
    }
    
    public synchronized void method2() {
        // Method 2逻辑
    }
}

SharedObject sharedObject = new SharedObject();

// 在同一个线程中调用method1()
sharedObject.method1();

method1()内部调用了method2(),而且两个方法都使用了synchronized关键字来进行同步。然而,由于在同一个线程中连续调用了method1(),并且method2()也是同步方法,就会导致同一个线程试图获得两个方法的锁,有死锁的可能性。

可以通过将同步锁的粒度放在更高的层次上,如基于客户端的锁定或适配服务端的方式来避免依赖问题。可以在客户端代码中使用基于客户端的锁定方式,即在调用method1()之前锁定共享对象。

SharedObject sharedObject = new SharedObject();

// 这样可以确保在调用method1()和method2()之间获得共享对象的锁,避免了死锁和其他并发问题
synchronized (sharedObject) {
    sharedObject.method1();
}

保持同步区域微小

锁是昂贵的,因为它们带来了延迟和额外开销。因此建议:尽可能减小同步区域。

测试线程代码

无法完全证明多线程代码的正确性,为了尽量减少潜在问题提高代码的质量和可靠性书中给出了以下建议:

  1. 将伪失败视为潜在的线程问题:当测试失败时,不要立即排除多线程问题,因为多线程代码可能会导致非确定性的结果。
  2. 首先确保非线程代码的正确性:在开始测试多线程代码之前,先确保非线程相关的代码是正确的。这样可以排除一些潜在的问题源。
  3. 编写可插拔的线程代码:将线程代码设计为可插拔的组件,可以方便地替换、调试和测试单个线程功能。
  4. 编写可调整的线程代码:为了方便测试,将线程代码设计为可调整参数,以便模拟不同的并发条件和负载。
  5. 运行多于处理器数量的线程:通过运行多个线程,超过系统处理器的数量,可以更好地模拟并发环境并暴露潜在的竞争条件和同步问题。
  6. 在不同平台上运行:多线程代码的行为可能因操作系统和硬件平台的差异而有所不同。在不同平台上运行测试,可以发现平台相关的问题。
  7. 调整代码并强制错误发生:通过有目的地调整代码,引入并发问题和错误的情况,以验证代码的鲁棒性和正确性。