代码整洁之道Clean Code笔记

@

在线阅读:书栈网:https://www.bookstack.cn/read/Clean-Code-zh/spilt.8.docs-ch1.md

每个章节都会做一个自己的总结,并为这个章节打一个重要性的参考分数,满分五星(仅个人的角度)。

如果想尽快的了解一些代码的规范,最好看一下阿里代码规范,idea也可以安装阿里代码规范插件。

阿里开发手册是实践,这本书本身更多的是作者理念引导。理念的引导必然少不了佐证、和一些看似冗余的语句,但这都是我们站在如今开发环境上的结果论,因为所有技术性文章都是具有很强的时效性。

第 1 章 Clean Code 整洁代码(3星)

第一章主要介绍这本书的背景和意图,以及总结下整洁代码的理念

有的章节几节可以直接越过,但是看完的话既知所以然,又知其然

?为什么要整洁的代码

反证:糟糕代码的坏处

团队中各司其职,代码是代码人应有的责任,要主动维护,去说服那些阻碍我们优化的。。。

?什么叫做整洁代码

每个人对于整洁的定义都不同,这里只是作者代表的一些理念,要学会自我思考

  • 意图明确:只做一件事,提高表达力
  • 扩展性:提早构建小规模、简单抽象
  • 简洁:减少重复代码,包括尽量少的实体,比如类、方法、函数等
  • 正确性:能通过所有测试
  • 体现系统中的全部设计理念
  • 读与写花费时间的比例超过 10:1,代码阅读重要性,要对自己的读者负责
  • 整洁代码需要从点滴做起

成功的案例并不能让你成为成功者,只能分享给别人成功的过程,技巧

第 2 章 Meaningful Names 有意义的命名(3星)

语义明确,语境明确,不冗余

  • 语义明确:最好通过变量名讲解变量的意义,而不是注释。例如:魔法值,就是不能明确意义的常量

  • 避免误导:例如:缩写不明确;专有名词同名;命名类型不准确;相似命名放一起区分,明确注释;相似数字字母不明确

  • 有意义的区分,废话都是冗余。比如:Table 一词永远不应当出现在表名中

  • 使用可以读的出来的名称

  • 使用可以搜索的名字:单字母名称和数字常量仅用在短方法中的本地变量,最好不要用

  • 避免思维映射:不要你觉得,别人也会这样觉得

  • 类名和对象名应该是名词或名词短语,不应当是动词

  • 方法名应当是动词或动词短语

重载构造器时,使用描述了参数的静态工厂方法名。例如,

Complex fulcrumPoint = Complex.FromRealNumber(23.0);

通常好于

Complex fulcrumPoint = new Complex(23.0);

可以考虑将相应的构造器设置为 private,强制使用这种命名手段。

  • 别用双关语

  • 使用程序员熟悉的术语,或者所涉问题领域命名

  • 添加有意义的语境:对字段进行补充说明

第 3 章 Functions 函数(3星)

本章所讲述的是有关编写良好函数的机制

  • 函数的第一规则是要短小

  • 函数应该只做好这一件事

  • 每个函数一个抽象层级

让代码读起来像是一系列自顶向下的 TO 起头段落是保持抽象层级协调一致的有效技巧

  • switch

利用多态来实现,确保每个 switch 都埋藏在较低的抽象层级,而且永远不重复

例子:所有的员工都有一样的流程,是否发薪日、计算薪水、发薪水,但是不同类型的员工具体流程动作不一样

public Money calculatePay(Employee e)
        throws InvalidEmployeeType {
    switch (e.type) {
        case COMMISSIONED:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvalidEmployeeType(e.type);
    }
}

多态实现:更改之后再增加职员类型,每种职工只需要做自己的事情,不需要所有的类型当方法都更改,只需更改实现工厂实现类

public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-----------------
public class EmployeeFactoryImpl implements
        EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmploye(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}
  • 使用描述性的名称

别害怕长名称、别害怕花时间取名字、命名方式要保持一致

  • 函数参数: 最理想的参数数量是零
  • 无副作用函数:不要把相关性不强的逻辑放进来,强调复用性
  • 分隔指令与询问:避免混乱
  • 使用异常替代返回错误码
if (deletePage(page) == E_OK) {    if (registry.deleteReference(page.name) == E_OK) {        if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {            logger.log("page deleted");        } else {            logger.log("configKey not deleted");        }    } else {        logger.log("deleteReference from registry failed");    }} else {    logger.log("delete failed");    return E_ERROR;}

On the other hand, if you use exceptions instead of returned error codes, then the error processing code can be separated from the happy path code and can be simplified:

另一方面,如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化:

 复制代码try {    deletePage(page);    registry.deleteReference(page.name);    configKeys.deleteKey(page.name.makeKey());} catch (Exception e) {    logger.log(e.getMessage());}
  • 重复可能是软件中一切邪恶的根源
  • 好的代码需要慢慢打磨,精简,优化(这正是我喜欢的过程,乐此不疲)

第 4 章 Comments 注释(2星)

作者毕竟站在英语母语的基础上,还是要考虑下我们自己的环境

  • 注释不能美化糟糕的代码,注释不能成为糟糕代码的发言人,代码才是核心
  • 别误导,别废话,适时整理TODO、注释的代码块

第 5 章 Formatting 格式 (1星)

几乎不用看

  • 垂直、横向格式:代码缩进

第 6 章 Objects and Data Structures 对象和数据结构(4星)

对象曝露行为,隐藏数据。便于添加新对象类型而无需修改既有行为,同时也难以在既有对象中添加新行为。

数据结构曝露数据,没有明显的行为。便于向既有数据结构添加新行为,同时也难以向既有函数添加新数据结构。

  • 数据抽象:数据封装,隐藏具体行为
  • 数据、对象的反对称性

过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。

  • 得墨忒耳律

方法不应调用由任何函数返回的对象的方法

下列代码违反了得墨忒耳律(除了违反其他规则之外),因为它调用了 getOptions( )返回值的 getScratchDir( )函数,又调用了 getScratchDir( )返回值的 getAbsolutePath( )方法。

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

第 7 章 Error Handling 错误处理(4星)

在本章中,要列出编写既整洁又强固的代码——优雅地处理错误代码的一些技巧和思路

整洁代码是可读的,但也要强固。可读与强固并不冲突。如果将错误处理隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码

  • 使用异常而非返回码,用统一异常处理处理异常
  • 在编写可能抛出异常的代码时, 先写 Try-Catch-Finally 语句
  • 使用不可控异常,可控异常的代价就是违反开放/闭合原则
  • 给出异常发生的环境说明
  • 依调用者需要定义异常类

我们在应用程序中定义异常类时,最重要的考虑应该是它们如何被捕获,然后根据捕获规律去优化异常捕获

  • 别返回 null 值
  • 别传递 null 值

第 9 章 Unit Tests 单元测试(3星)

  • 保持测试整洁,测试代码和生产代码一样重要
  • 整洁的测试,最重要的要素是可读性
  • 每个测试一个断言
  • 测试应该有以下规则:快速、独立、可重复、自足验证、及时

第 10 章 Classes 类(5星)

代码组织的更高层面——Classes

  • 将系统的构造与使用分开
  • 类的组织

遵循标准的 Java 约定,类应该从一组变量列表开始。如果有公共静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。很少会有公共变量。公共函数应跟在变量列表之后。封装

  • 类应该短小

对于函数,我们通过计算代码行数衡量大小。对于类,我们采用不同的衡量方法,计算权责(responsibility)。类的名称应当描述其权责,从命名开始规范。

单一权责原则(SRP)认为,类或模块应有且只有一条加以修改的理由。系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。

内聚:类应该只有少量实体变量。类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类上。

扩展性:需求会改变,所以代码也会改变。具体类包含实现细节(代码),而抽象类则只呈现概念。依赖于具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来隔离这些细节带来的影响。依赖倒置原则(Dependency Inversion Principle,DIP),DIP 认为类应当依赖于抽象而不是依赖于具体细节。

单一权责和内聚都是程度值,保证的他们平衡,逻辑内聚,权责解耦,这并不简单,SRP也充分考虑到了代码扩展性。

第 11 章 Systems 系统(5星)

本章将讨论如何在较高的抽象层级——系统层级——上保持整洁

无论是设计系统或单独的模块,别忘了使用大概可工作的最简单方案。

  • 将系统的构造与使用分开(编译和运行,java的环境中Spring通过依赖注入(Dependency Injection,DI),控制反转(Inversion of Control,IoC)已经帮我们把这件事做了)。延后初始化的好处:这种手段在 DI 中也有其作用。首先,多数 DI 容器在需要对象之前并不构造对象。其次,许多这类容器提供调用工厂或构造代理的机制,而这种机制可为延迟赋值或类似的优化处理所用。

  • **AOP: **在 AOP 中,被称为方面(aspect)的模块构造指明了系统中哪些点的行为会以某种一致的方式被修改,从而支持某种特定的场景。这种说明是用某种简洁的声明或编程机制来实现的。

  • 代理:使用代理,代码量和复杂度是代理的两大弱点,创建整洁代码变得很难!另外,代理也没有提供在系统范围内指定执行点的机制,而那正是真正的 AOP 解决方案所必须的

  • 纯净的 Java AOP 框架:Bean工厂内,每个 bean 就像是嵌套“俄罗斯套娃”中的一个,每个由数据存取器对象(DAO)代理(包装)的 Bank 都有个域对象,而 bean 本身又是由 JDBC 驱动程序数据源代理。通过XML/注解的方式减少对代码的入侵,只留下纯POJO。

  • AspectJ ASPECTS AspectJ 的方面:AspectJ 却提供了一套用以切分关注面的丰富而强有力的工具。

  • 测试驱动系统架构:大设计(Big Design Up Front,BDUF)——系统架构。最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯 Java(或其他语言)对象实现。不同的领域之间用最不具有侵害性的方面或类方面工具整合起来。这种架构能测试驱动,就像代码一样。

  • 优化决策:模块化和关注面切分成就了分散化管理和决策。拥有模块化关注面的 POJO 系统提供的敏捷能力,允许我们基于最新的知识做出优化的、时机刚好的决策。决策的复杂性也降低了。

  • 选择合适的架构——标准

  • 系统需要领域特定语言:领域特定语言(Domain-Specific Language,DSL)。DSL 是一种单独的小型脚本语言或以标准语言写就的 API,领域专家可以用它编写读起来像是组织严谨的散文一般的代码。领域特定语言允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用 POJO 来表达。

第 12 章 Emergence 迭进

本章中写到的实践来自于本书作者数十年经验的精练总结。遵循简单设计的实践手段,开发者不必经年学习就能掌握好的原则和模式。

提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称,如此等等。这也是应用简单设计后三条规则的地方:消除重复,保证表达力,尽可能减少类和方法的数量。

通过迭进设计达到整洁目的,Kent Beck 关于简单设计的四条规则,据 Kent 所述,只要遵循以下规则,设计就能变得“简单”,以下规则按其重要程度降序排列:

  • 运行所有测试;
  • 不可重复;
  • 表达了程序员的意图;
  • 尽可能减少类和方法的数量;

第 13 章 Concurrency 并发编程(5星)

“对象是过程的抽象。线程是调度的抽象。” ——James O

这个章节主要讲述了并发编程的来源、优势和劣势,以及如何避免、解决并发错误的方法和方向

? 为什么要并发

  • 并发是一种解耦策略。

  • 解耦目的与时机能明显地改进应用程序的吞吐量和结构。

  • 并发会在性能和编写额外代码上增加一些开销;

  • 正确的并发是复杂的,即便对于简单的问题也是如此;

  • 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待;

  • 并发常常需要对设计策略的根本性修改。

并发防御原则

  • 单一权责原则

单一权责原则(SRP)认为,方法/类/组件应当只有一个修改的理由。并发设计自身足够复杂到成为修改的理由,所以也该从其他代码中分离出来。不幸的是,并发实现细节常常直接嵌入到其他生产代码中。

需要考虑的问题:

并发相关代码有自己的开发、修改和调优生命周期;

开发相关代码有自己要对付的挑战,和非并发相关代码不同,而且往往更为困难;

即便没有周边应用程序增加的负担,写得不好的并发代码可能的出错方式数量也已经足具挑战性。

建议:分离并发相关代码与其他代码。

  • 限制数据作用域

两个线程修改共享对象的同一字段时,可能互相干扰,导致未预期的行为。解决方案之一是采用 synchronized 关键字在代码中保护一块使用共享对象的临界区(critical section)。限制临界区的数量很重要。更新共享数据的地方越多,就越可能:

谨记数据封装;严格限制对可能被共享的数据的访问。

避免共享数据的好方法之一就是一开始就避免共享数据。

线程应尽可能地独立,让每个线程在自己的世界中存在,不与其他线程共享数据。

了解 Java 库

学习类库,了解基本算法。理解类库提供的与基础算法类似的解决问题的特性。

了解执行模型

学习这些基础算法,理解其解决方案。

  • Producer-Consumer 生产者-消费者模型
  • Readers-Writers 读者-作者模型
  • Dining Philosophers 哲学家用餐模式

警惕同步方法之间的依赖

  • 避免使用一个共享对象的多个方法
  • 有时必须使用一个共享对象的多个方法,有 3 种应对手段:
  • 基于客户端的锁定——客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码;
  • 基于服务端的锁定——在服务端内创建锁定服务端的方法,调用所有方法,然后解锁。让客户端代码调用新方法;
  • 适配服务端——创建执行锁定的中间层。这是一种基于服务端的锁定的例子,但不修改原始服务端代码。

尽可能减小同步区域

尽早考虑关闭问题,尽早令其工作正常。

测试线程代码

编写有潜力曝露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败。

有一大堆问题要考虑。下面是一些精练的建议:

  • 将伪失败看作可能的线程问题,不要将系统错误归咎于偶发事件
  • 先使非线程代码可工作, 不要同时追踪非线程缺陷和线程缺陷。确保代码在线程之外可工作。
  • 编写可插拔的线程代码,这样就能在不同的配置环境下运行。
  • 编写可调整的线程代码,要获得良好的线程平衡,常常需要试错。一开始,在不同的配置环境下监测系统性能。要允许线程数量可调整。在系统运行时允许线程发生变动。允许线程依据吞吐量和系统使用率自我调整。
  • 运行多于处理器数量的线程,系统在切换任务时会发生一些事。为了促使任务交换的发生,运行多于处理器或处理器核心数量的线程。任务交换越频繁,越有可能找到错过临界区或导致死锁的代码。
  • 在不同平台上运行
  • 装置试错代码,并强迫错误发生:有两种装置代码的方法:硬编码、自动化

第 15 章 JUnit Internals JUnit 内幕(2星)

本章介绍了JUnit的一些简单的模块

第 16 章 重构 SerialDate(4星)

本章详解对 org.jfree.date库中的SerialDate日期类进行重构,简化的过程。增加了测试覆盖率,修复了一些错误,澄清并缩小了代码。

第 17 章 味道与启发(3星)

本章又列举了作者之前列出过的,一些不好的习惯,并把这些比作难闻的气味

干净的代码不是通过遵循一组规则来编写的。

附录 A 并发编程 II(4星)

并发编程的一些扩充信息,多了很多的示例讲解

在本章中,我们谈到并发更新,还有清理及避免同步的规程。我们谈到线程如何提升与 I/O 有关的系统的吞吐量,展示了获得这种提升的整洁技术。我们谈到死锁及干净地避免死锁的规程。最后,我们谈到通过装置代码暴露并发问题的策略。

  • 死锁

死锁的发生需要 4 个条件:

互斥:无法在同一时间为多个线程所用;数量上有限制

这种资源的常见例子是数据库连接、打开后用于写入的文件、记录锁或是信号量。

上锁及等待:当某个线程获取一个资源,在获取到其他全部所需资源并完成其工作之前,不会释放这个资源。

无抢先机制:线程无法从其他线程处夺取资源。一个线程持有资源时,其他线程获得这个资源的唯一手段就是等待该线程释放资源。

循环等待:这也被称为“死命拥抱”。想象两个线程,T1 和 T2,还有两个资源,R1 和 R2。T1 拥有 R1,T2 拥有 R2。T1 需要 R2,T2 需要 R1。

这 4 种条件都是死锁所必需的。只要其中一个不满足,死锁就不会发生。

避免死锁的一种策略是规避互斥条件。你可以:

  • 使用允许同时使用的资源;
  • 增加资源数量,使其等于或大于竞争线程的数量;
  • 在获取资源之前,检查是否可用。
  • 不上锁及等待
  • 满足抢先机制
  • 不做循环等待

将解决方案中与线程相关的部分分隔出来,再加以调整和试验,是获得判断最佳策略所需的洞见的正道。

总结

干净有经验值,也有固定分,不是通过遵循一组规则来编写的,需要的是迭进,不需要钻牛角尖。

读英文原文的时候突然想到:英语大多是结果论,喜欢陈述事实,就好像罪犯的对白

posted @ 2021-12-02 23:04  好奇新  阅读(855)  评论(0编辑  收藏  举报