代码整洁之道Clean Code笔记
@
- 第 1 章 Clean Code 整洁代码(3星)
- 第 2 章 Meaningful Names 有意义的命名(3星)
- 第 3 章 Functions 函数(3星)
- 第 4 章 Comments 注释(2星)
- 第 5 章 Formatting 格式 (1星)
- 第 6 章 Objects and Data Structures 对象和数据结构(4星)
- 第 7 章 Error Handling 错误处理(4星)
- 第 9 章 Unit Tests 单元测试(3星)
- 第 10 章 Classes 类(5星)
- 第 11 章 Systems 系统(5星)
- 第 12 章 Emergence 迭进
- 第 13 章 Concurrency 并发编程(5星)
- 第 15 章 JUnit Internals JUnit 内幕(2星)
- 第 16 章 重构 SerialDate(4星)
- 第 17 章 味道与启发(3星)
- 附录 A 并发编程 II(4星)
- 总结
在线阅读:书栈网: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 种条件都是死锁所必需的。只要其中一个不满足,死锁就不会发生。
避免死锁的一种策略是规避互斥条件。你可以:
- 使用允许同时使用的资源;
- 增加资源数量,使其等于或大于竞争线程的数量;
- 在获取资源之前,检查是否可用。
- 不上锁及等待
- 满足抢先机制
- 不做循环等待
将解决方案中与线程相关的部分分隔出来,再加以调整和试验,是获得判断最佳策略所需的洞见的正道。
总结
干净有经验值,也有固定分,不是通过遵循一组规则来编写的,需要的是迭进,不需要钻牛角尖。
读英文原文的时候突然想到:英语大多是结果论,喜欢陈述事实,就好像罪犯的对白