@[TOC](代码整洁之道Clean Code笔记)
在线阅读:书栈网:https://www.bookstack.cn/read/Clean-Code-zh/spilt.8.docs-ch1.md
每个章节都会做一个自己的总结,并为这个章节打一个重要性的参考分数,满分五星(仅个人的角度)。
如果想尽快的了解一些代码的规范,最好看一下阿里代码规范,idea也可以安装阿里代码规范插件。
阿里开发手册是实践,这本书本身更多的是作者理念引导。理念的引导必然少不了佐证、和一些看似冗余的语句,但这都是我们站在如今开发环境上的结果论,因为所有技术性文章都是具有很强的时效性。
第一章主要介绍这本书的背景和意图,以及总结下整洁代码的理念
有的章节几节可以直接越过,但是看完的话既知所以然,又知其然
反证:糟糕代码的坏处
团队中各司其职,代码是代码人应有的责任,要主动维护,去说服那些阻碍我们优化的。。。
每个人对于整洁的定义都不同,这里只是作者代表的一些理念,要学会自我思考
成功的案例并不能让你成为成功者,只能分享给别人成功的过程,技巧
语义明确,语境明确,不冗余
**语义明确:**最好通过变量名讲解变量的意义,而不是注释。例如:魔法值,就是不能明确意义的常量
**避免误导:**例如:缩写不明确;专有名词同名;命名类型不准确;相似命名放一起区分,明确注释;相似数字字母不明确
有意义的区分,废话都是冗余。比如:Table 一词永远不应当出现在表名中
使用可以读的出来的名称
**使用可以搜索的名字:**单字母名称和数字常量仅用在短方法中的本地变量,最好不要用
**避免思维映射:**不要你觉得,别人也会这样觉得
类名和对象名应该是名词或名词短语,不应当是动词
方法名应当是动词或动词短语
重载构造器时,使用描述了参数的静态工厂方法名。例如,
Complex fulcrumPoint = Complex.FromRealNumber(23.0);
通常好于
Complex fulcrumPoint = new Complex(23.0);
可以考虑将相应的构造器设置为 private,强制使用这种命名手段。
别用双关语
使用程序员熟悉的术语,或者所涉问题领域命名
**添加有意义的语境:**对字段进行补充说明
本章所讲述的是有关编写良好函数的机制
函数的第一规则是要短小
函数应该只做好这一件事
每个函数一个抽象层级
让代码读起来像是一系列自顶向下的 TO 起头段落是保持抽象层级协调一致的有效技巧
利用多态来实现,确保每个 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());}
作者毕竟站在英语母语的基础上,还是要考虑下我们自己的环境
几乎不用看
对象曝露行为,隐藏数据。便于添加新对象类型而无需修改既有行为,同时也难以在既有对象中添加新行为。
数据结构曝露数据,没有明显的行为。便于向既有数据结构添加新行为,同时也难以向既有函数添加新数据结构。
过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。
方法不应调用由任何函数返回的对象的方法
下列代码违反了得墨忒耳律(除了违反其他规则之外),因为它调用了 getOptions( )返回值的 getScratchDir( )函数,又调用了 getScratchDir( )返回值的 getAbsolutePath( )方法。
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
在本章中,要列出编写既整洁又强固的代码——优雅地处理错误代码的一些技巧和思路
整洁代码是可读的,但也要强固。可读与强固并不冲突。如果将错误处理隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码
我们在应用程序中定义异常类时,最重要的考虑应该是它们如何被捕获,然后根据捕获规律去优化异常捕获
代码组织的更高层面——Classes
遵循标准的 Java 约定,类应该从一组变量列表开始。如果有公共静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。很少会有公共变量。公共函数应跟在变量列表之后。封装
对于函数,我们通过计算代码行数衡量大小。对于类,我们采用不同的衡量方法,计算权责(responsibility)。类的名称应当描述其权责,从命名开始规范。
**单一权责原则(SRP)**认为,类或模块应有且只有一条加以修改的理由。系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。
**内聚:**类应该只有少量实体变量。类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类上。
**扩展性:**需求会改变,所以代码也会改变。具体类包含实现细节(代码),而抽象类则只呈现概念。依赖于具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来隔离这些细节带来的影响。依赖倒置原则(Dependency Inversion Principle,DIP),DIP 认为类应当依赖于抽象而不是依赖于具体细节。
单一权责和内聚都是程度值,保证的他们平衡,逻辑内聚,权责解耦,这并不简单,SRP也充分考虑到了代码扩展性。
本章将讨论如何在较高的抽象层级——系统层级——上保持整洁
无论是设计系统或单独的模块,别忘了使用大概可工作的最简单方案。
将系统的构造与使用分开(编译和运行,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 来表达。
本章中写到的实践来自于本书作者数十年经验的精练总结。遵循简单设计的实践手段,开发者不必经年学习就能掌握好的原则和模式。
提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称,如此等等。这也是应用简单设计后三条规则的地方:消除重复,保证表达力,尽可能减少类和方法的数量。
通过迭进设计达到整洁目的,Kent Beck 关于简单设计的四条规则,据 Kent 所述,只要遵循以下规则,设计就能变得“简单”,以下规则按其重要程度降序排列:
“对象是过程的抽象。线程是调度的抽象。” ——James O
这个章节主要讲述了并发编程的来源、优势和劣势,以及如何避免、解决并发错误的方法和方向
并发是一种解耦策略。
解耦目的与时机能明显地改进应用程序的吞吐量和结构。
并发会在性能和编写额外代码上增加一些开销;
正确的并发是复杂的,即便对于简单的问题也是如此;
并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待;
并发常常需要对设计策略的根本性修改。
单一权责原则(SRP)认为,方法/类/组件应当只有一个修改的理由。并发设计自身足够复杂到成为修改的理由,所以也该从其他代码中分离出来。不幸的是,并发实现细节常常直接嵌入到其他生产代码中。
需要考虑的问题:
并发相关代码有自己的开发、修改和调优生命周期;
开发相关代码有自己要对付的挑战,和非并发相关代码不同,而且往往更为困难;
即便没有周边应用程序增加的负担,写得不好的并发代码可能的出错方式数量也已经足具挑战性。
建议:分离并发相关代码与其他代码。
两个线程修改共享对象的同一字段时,可能互相干扰,导致未预期的行为。解决方案之一是采用 synchronized 关键字在代码中保护一块使用共享对象的临界区(critical section)。限制临界区的数量很重要。更新共享数据的地方越多,就越可能:
谨记数据封装;严格限制对可能被共享的数据的访问。
避免共享数据的好方法之一就是一开始就避免共享数据。
线程应尽可能地独立,让每个线程在自己的世界中存在,不与其他线程共享数据。
学习类库,了解基本算法。理解类库提供的与基础算法类似的解决问题的特性。
学习这些基础算法,理解其解决方案。
- 基于客户端的锁定——客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码;
- 基于服务端的锁定——在服务端内创建锁定服务端的方法,调用所有方法,然后解锁。让客户端代码调用新方法;
- 适配服务端——创建执行锁定的中间层。这是一种基于服务端的锁定的例子,但不修改原始服务端代码。
尽可能减小同步区域
尽早考虑关闭问题,尽早令其工作正常。
编写有潜力曝露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败。
有一大堆问题要考虑。下面是一些精练的建议:
本章介绍了JUnit的一些简单的模块
本章详解对 org.jfree.date库中的SerialDate日期类进行重构,简化的过程。增加了测试覆盖率,修复了一些错误,澄清并缩小了代码。
本章又列举了作者之前列出过的,一些不好的习惯,并把这些比作难闻的气味
干净的代码不是通过遵循一组规则来编写的。
并发编程的一些扩充信息,多了很多的示例讲解
在本章中,我们谈到并发更新,还有清理及避免同步的规程。我们谈到线程如何提升与 I/O 有关的系统的吞吐量,展示了获得这种提升的整洁技术。我们谈到死锁及干净地避免死锁的规程。最后,我们谈到通过装置代码暴露并发问题的策略。
死锁的发生需要 4 个条件:
**互斥:**无法在同一时间为多个线程所用;数量上有限制
这种资源的常见例子是数据库连接、打开后用于写入的文件、记录锁或是信号量。
上锁及等待:当某个线程获取一个资源,在获取到其他全部所需资源并完成其工作之前,不会释放这个资源。
无抢先机制:线程无法从其他线程处夺取资源。一个线程持有资源时,其他线程获得这个资源的唯一手段就是等待该线程释放资源。
循环等待:这也被称为“死命拥抱”。想象两个线程,T1 和 T2,还有两个资源,R1 和 R2。T1 拥有 R1,T2 拥有 R2。T1 需要 R2,T2 需要 R1。
这 4 种条件都是死锁所必需的。只要其中一个不满足,死锁就不会发生。
避免死锁的一种策略是规避互斥条件。你可以:
将解决方案中与线程相关的部分分隔出来,再加以调整和试验,是获得判断最佳策略所需的洞见的正道。
干净有经验值,也有固定分,不是通过遵循一组规则来编写的,需要的是迭进,不需要钻牛角尖。
读英文原文的时候突然想到:英语大多是结果论,喜欢陈述事实,就好像罪犯的对白