《Clean Code》代码整洁之道 一书相关读书笔记,整洁的代码是自解释的,阅读代码应该如同阅读一篇优秀的文章,见字知意,可以一会儿明白大概的代码功能。代码首先要能读懂,其次才去要求功能实现。java
做为开发者来讲,在如今基本都讲究团队合做的工做方式下,规范远比功能重要,开发的功能也许在版本迭代中就不复存在了,可是规范倒是一直存在的。正则表达式
为了整洁的代码,就必须遵循一些统一的团队规范。混乱不堪连命名都没法取好的代码,只会给增长后续接手人员维护的成本数据库
本章主要表述糟糕的代码对项目进度的提高并没有太多的帮助,只会如同一堆朝不保夕的柴火同样,后续开发者又会理不清关系常常直接接着往上面扔其它柴火,最终结果就是轰然倒塌没法维护。因此在一开始咱们便要最大程度的保持代码整洁。编程
所谓的整洁代码,我的理解大概是符合如下要求: 1.代码格式统一一种规范,好比缩进、空行、空格等等; 2.使用有意义的命名,包含类名、变量名、函数名等等,可以一眼就基本看出所要完成的大概操做; 3.类、函数功能单一简短,避免臃肿以及过多嵌套;设计模式
可以读出单词意思,符合实际用处的变量命名让代码阅读过程更加愉悦,再简洁的代码一旦命名糟糕都会致使模糊度增长,难以读懂意思。api
类名、对象名一般都是名词/名词短语,方法名通常都是动词/动词短语。命名时结合上下文的关联性进行命令,能够产生有意义的语境。例如score和student、exam等词语在一块儿时,容易让人联想到score表明学生考试的成绩,固然也有能够添加必要的前缀增长语境,例如examScore数组
函数的第一规则是短小,第二规则是更短小,函数长度最好不超过一屏幕,不然须要来回滚动,阅读不方便;安全
每一个函数功能单一,嵌套不超过2层,只作一件事,作好这件事。判断是否功能单一,能够看是否还能够再抽出一个函数;bash
越是短小的函数,功能越是单一,命名就更加针对性;服务器
自顶向下的阅读规则,每一个函数一个层级并调用下一个层级函数;
switch语句的最好在较低的抽象层级上使用,用于建立多态。 使用描述性的名称,别惧怕名称太长。长而具备描述性的名称要比短而使人费解的名称好。
最理想的参数数量是零,没有足够的理由不要使用3个及以上的参数数量,不只增长了理解成本,并且从测试角度看,参数组合对编写单元测试来说即是麻烦事。当参数数量太多,应该考虑整合部分参数为类对象。
部分函数所执行的处理可能隐藏着与函数名不相符的操做,这种函数具备破坏性,违反了单一原则。好比在void checkPassword(String username, String password)函数中若是隐藏着Session.Initialize()会话初始化操做,可能会致使引用的人只是想判断用户名和密码是否正确,结果却致使了当前正常的会话出现了问题,正确的作法能够是该函数命名为checkPasswordAndSessionInitialize(String username, String password)
上述这2点也是平时编程中容易犯的错误: 参数数量的过多,一方面增长了阅读理解函数名称的时间成本,在阅读具体实现中,还要时刻注意各个参数的具体引用位置与效果; 在一个api中处理过多的事情,而名称有没有描述清楚,会致使不清楚具体实现的人进行调用后而出现的未知的错误;
消除重复代码,重复是软件中的罪恶之源,许多原则与实践都是为了消除重复而建立。去除过多的冗余,让代码逻辑一目了然,也让后续修改维护更加简单。
像写一篇文章对待代码,初稿也许粗陋无序,你就斟酌推敲,直到达到你心目中的样子。
写注释的动机之一是为了由于糟糕代码的存在,怕别人看不懂而决定添加点注释,但最好的处理是把代码弄干净,而不是去添加那些没有必要的注释,要相信好的代码是"自说明"的;
注释都有可能存在误导、不适用或者提供错误的信息,也许源于一开始就表述错误的注释,也许由于后续代码已经修改但却未及时更新注释等等缘由致使;
对于引用某些没法修改的第三方库文件返回的晦涩难懂的返回值,适当给予注释说明,阐释含义有助于理解;
对于相对复杂的处理流程,注释解释做者的意图能够提供理解相关实现的有用信息;
编写公共API就该为它编写良好的标准的java doc说明。
坏的注释存在如下状况: 1.多余的注释:简单明了的函数,其函数注释说明纯属多余,读注释的时间可能多余读函数代码的时间;
2.误导性的注释:注释不够精确,存在与实际运行不相符的状况,误导使用者进行错误的处理;
3.日志性的注释:在每一个模块开始添加每一次修改的注释性日志已经没有必要,如今不少代码维护工具提供更好的修改记录;
4.废话性的注释:好比private int dayofMonth ; 注释为 the day of the month ;
5.循规性的注释: 每一个函数或者每一个变量都有javadoc标准的注释彻底没有必要,除非是编写公共API;
能用函数和变量自说明的时候,就别添加任何注释。注释自己是为了解释未能自行解释的代码,若是注释自己还须要解释,那就毫无做用。
代码格式很重要,由于代码的可读性会对之后可能发生的修改行为产生深远的影响。我也曾认为"让代码能工做"才是头等大事,后面慢慢的会发现,所编写的功能在下一个版本也许就被修改掉不复存在了,但代码格式规范确实一直存在下去的。良好的代码格式能够促进理解沟通,在协同合做的当下,注意规范很重要。
每一个人可能偏好的代码格式不一样,可是团队合做中就该商量统一一种好的代码格式,全部人都按照这种标准进行开发工做。对于代码的垂直格式,好比类文件尽可能功能简单,注意能拆分的进行拆分,以避免类过大会变得臃肿难以维护。在功能描述上注重从名称上就能够大概了解主要处理的功能点。对于代码的水平格式,一行代码尽可能不超过一个屏幕的显示范围。
这章主要讲的是对象与数据结构优缺点。对象主要提供接口而隐藏其内部实现,而数据结构提供纯数据读写而不带任何有其它意义的函数操做。
在个人理解上: 过程式编程主要是单独使用业务类去处理各类数据结构,这就致使了在各个方法中传参为Object对象而后须要用判断语句判断每一种数据结构对应的类型,才好方便作到根据传入的不一样数据结构类型来返回不一样的结果。这种方式好处就是业务类中增长了新的操做函数,并不须要去修改已存在的数据结构类,可是若是增长了新的数据结构类型则须要在业务类中的每一个操做函数中增长该结构类型的判断;
面向对象由于推崇接口式编程,若是一开始接口没考虑清楚,那么增长减小接口就会致使全部实现类都须要进行修改,可是相比较下面向对象方便在建立新的类对象时,不影响其它对象的操做接口,由于实现上是多态的。 前者方便修改已存在的操做函数但难以增长类,后者方便增长类,但难以修改已存在的接口。并无孰优孰劣,须要根据实际去决定采用哪一种。
这2个小节主要讲了错误处理中的2个建议,第1个是使用异常代替返回码,第2个是对于要捕获的异常先书写Try-Catch-Finally语句。 对于第1个书上说是为了代码书写过程当中不须要每次调用都要去判断返回的各类错误码的状况,影响代码整洁会打乱代码逻辑。但有时候实际使用过程,仍是常常会有将错误码定义为返回值的状况,对调用者来说方便判断结果,通常这种状况的错误码都会有不少种类型,甚至存在不一样的错误代码可能都当作成功处理的状况,只是单单使用抛出异常的方式有时候并非太好。 对于第2个,这是一种好的代码规范,对于须要捕获错误的函数,理论上try开头,finally结尾。
若是底层api已经被普遍使用,须要抛出新异常,建议内部封装处理好,避免上层每个环节都须要从新throw或catch该新的异常。根据实际使用的状况,对于主动抛出的自定义异常最好进行分类,而且携带异常说明,方便排查是什么缘由致使。函数的处理上,别最好避免传入null和返回null的状况,避免引用的地方都要增长非null判断,影响整洁。
使用第三方代码或者开源库的时候,避免直接在生产代码中验证功能,能够尝试编写对应的单元测试来熟悉其使用方法,理解提供的api接口,这样当开源库版本发生变化,能够快速根据以前编写的单元测试来验证功能是否会受到影响。对于开源库/第三方提供的接口,能够尝试再进行一层封装,使用方便本身理解的接口名,避免代码各处直接引用其提供的接口,这样之后即使替换为不一样的开源库或者开源库接口发生变动,也能够作到改动最小。
这几个小节主要讲述单元测试的重要性,单元测试与生产代码并行甚至优先于生产代码诞生,这样保证项目完成之时有着覆盖各个方面的单元测试体系。不能由于是单元测试就双重标准判断,不去注重单元测试的整洁。一套好的单元测试可让你无所顾忌的修改现有代码的框架,也是你进行大改动的底气,而同生产代码同样,整洁的单元测试是必不可少的,不然一旦成套的单元测试臃肿不堪难以维护,会致使单元测试没法跟上生产代码的更新速度,最终变为效率上的累赘,一旦抛弃又没法保证主体功能的稳定。
实际开发中,其实每每忽略了单元测试的编写,我感受这不只仅是由于项目周期赶的问题,而是咱们一开始就没有将单元测试的周期安排进去。因为这习惯性的忽略,每每是每次修改,反而须要耗费更多时间去自测。
单元测试用应该要尽量少的断言操做,而且保证测试用例只覆盖一个点。整洁的测试用例符合"First"原则: 快速(Fast),测试速度快;
独立(Independent),用例之间相互独立,互不影响和依赖;
可重复(Repeatable),测试可在任务环境下重复使用,好比有网络、无网络,生产环境或者质检环境;
自足验证(Self-Validating),用例不该该须要人工额外操做来判断是否经过,好比查看日志文件等等。用例自己应该直接经过断言判断是否成功。
及时(Timely),测试用例应该及时编写,而且最好优先于生产代码编写。不然先编写生产代码的话,每每会发现生产代码难以测试。
复制代码
类的规则与函数相似,第一规则是短小,第二规则仍是短小。越是短小的类越能符合单一职责原则。 项目工程达到必定程度后,类一不当心就会臃肿,须要再将类进行分割成一个个单一职责的小类,整洁的系统就该由许多短小的类而不是少许巨大的类来组成。
类的设计应该体现内聚性,即模块内的各个元素应该紧密的联系在一块儿。这边包含类中成员变量与方法命名上的要有关联性,类中成员变量应该尽可能被更多的类方法所操做,这样会使得方法与变量更加粘聚。内聚性高,意味着方法与变量相互依赖,互相结合成一个单一总体,职责也就越单一。 若是类中某些变量只被少数几个方法共享,意味着这几个方法和变量能够再单独拆分出一个小类出来,不然堆积了愈来愈多的这种变量将会致使内聚性的下降。
这几个小节主要讲“将构造与使用相互分离"的好处,书本描述得有点难以理解,意思大概就是: 构造与使用分离,会使得类之间的耦合度降低,方便后续进行实现上的替换修改。 举个相对简单的例子说明:
public class HandlerImpl {
public void handler() {
Component component= new component();
component.handle();
}
}
复制代码
在这个例子中,因为构造与使用没有分离,即HandlerImpl便是Component的构造者(调用了构造函数),又是Component的执行者(调用了方法),在以后假设须要将Component的实现替换为接口实现类,或者其它类,就会致使整个系统中不少相似这样的地方都须要进行替换为 Component component= new ComponentImpl()、Component2 component= new Component2();这样修改太多,耦合度过高,影响后续维护。
相似"分离构造与使用"可使用相似工厂模式+面向接口编程,如:
public class HandlerImpl {
public void handler() {
Component component= ComponentFactory.getComponent();
component.handle();
}
}
public class ComponentFactory {
public static Component getComponent() {
Component component= new componentImpl1();
return;
}
}
复制代码
后续即使替换Component的实现,或者增长对使用Component条件的判断,都只须要修改构造器ComponentFactory而不须要取修改全部使用的地方:
public class ComponentFactory {
public static Component getComponent() {
if (...) {
return new componentImpl1();
}
if (...) {
return new componentImpl2();
}
}
}
复制代码
这几个小节主要讲述了系统整洁,要达到系统整洁,须要注意模块功能的划分,注意简单的Been对象不与业务逻辑耦合在一块儿,作到职责单一。项目初始阶段,就将将来系统方方面面考虑周全,提供一大堆目前尚不要求的API或者功能点是没必要要的,所要作到的是划分好各个功能模块,注意各个模块之间的可扩展性与相互隔离。这样方便后面不断进行扩展和重构优化,快速迭代敏捷开发。
横贯式关注面? 持久化? AOP ?EJB?表示这章有点懵,看不太懂?
复制代码
如何达到简单整洁的设计,其实有四条规则能够遵照,按重要性从大到小排序为: 1.运行全部的测试: 这意味着须要编写单元测试,也是不少人所欠缺的,完整的单元测试会迫使你在编码过程注重可测性,也就必须划分好模块功能,遵照好单一职责。完善的测试也是不断重构的底气。
2.消除重复: 重复是良好设计的天敌,各类优秀的设计模式也是为了消除重复。不光是雷同的代码段重复,还有其它行为的重复,好比功能上能够支持同样的效果(像是int getSize()和boolean isEmpty(),这时候其实isEmpty()没什么必要),过多的重复会有没必要要的风险和增长额外的复杂度。
3.表达清楚意图: 这就要求命名要准确明了,上下文关联性强,可以阅读出做者的意图。
4.尽量减小类和方法的数量: 各类设计模式和功能上的划分每每会伴随类和方法数量的增长,但并非说类和方法越多越好。有些能够避免的就该避免掉,像是为每一个类都建立接口类其实并无必要。
并发编发能够对"时机"和"目的"解耦,不像单线程将2者紧密耦合在一块儿。解耦2者的目的,能够明显改进应用程序的吞吐量和结构。结构上看更像是多台计算机同时工做,而不是一个大循环,系统所以更容易被理解,可以更好地切分关注面。并发编程的是一把双刃剑,大部分状况下能够显著提升性能,可是也有须要注意的地方: 1.数据共享后存在多线程访问修改的同步问题; 2.根据状况也可以使用数据复本代替同步数据块来避免共享数据,后续统一汇总复本合并修改; 3.并发会在性能和编写额外代码上增长一些开销; 4.每一个线程尽量只运行在本身的小世界,尽可能不与其它线程共享数据
讲述了并发编程可能会致使死锁、互斥、等待等等状态,要编写好的并发代码,须要遵循单一职责原则。将系统切分为线程相关代码和与线程无关的代码的POJO,确保代码简单,目的集中。须要了解和学习相关并发库的知识,学会分析并发问题产生的缘由。注意锁定的代码区域尽量小,避免下降性能。测试多线程须要注意在不一样平台下、数量不等的线程以及使用各类wait、sleep、yield、priority装置代码,改变代码的运行顺序,用以尽早的发现各类问题。
前面章节有讲述过好的代码就像好的散文同样,可以让人愉快的阅读。而散文每每须要经历过各类草稿阶段方能成正稿,代码同样是这个道理,须要持续优化修改,方能符合整洁之道。 这章强调的是逐步改进,每改进一步,就完整运行伴随生产代码产生的各类测试用例,确保每一步修改都不会留下问题,不会影响下一步修改,循循渐进,最终完成代码总体的重构。也再一次说明,完善的测试用例很重要。
这章以几个单元测试用例为例,讲解用例从凌乱臃肿到整洁合理的修改过程。主要是对前面几章讲解的代码整洁之道的应用,方法单一职责、命名准确优雅,从方法、变量以及排版上重构出整洁的代码
依旧举个例子讲解修改过程: 完善测试用例,逐步修改逐步测试; 去除一些多余的注释,对于星期使用枚举代替int类型避免须要进行有效值判断; 使用工厂模式完成实体建立,实现构造与使用的分离; 优化方法和变量的命名去除重复的代码、消除魔术数的存在;
讲述总结了须要优化的地方:
对于注释: 1.不恰当的信息,好比修改历史; 2.废弃的注释,即过期、无关或者不正确的注释; 3.冗余的注释,例如除了函数签名什么都没有注明的javadoc; 4.注释掉的代码块,不明其意的糟糕注释;
对于函数: 1.参数越少越好,最多不超过3个; 2.避免输出参数,应使用返回值或者参数对象状态代替; 3.避免标识参数,例如布尔值参数; 4.去除没有调用的方法;
通常性问题: 1.去除重复代码块; 2.保证边界代码正确执行; 3.正确的抽象层级; 4.多态代替多重判断操做; 5.命名清楚表达意图; 等等
java中: 1.能够说使用通配符来避免过长的导入清单,好比import package.*; 2.避免从最低继承体系中继承常量; 3.使用合适的枚举代替常量;
名称: 1.采用准确的描述性名称,统一规范; 2.避免有歧义的名称,不畏惧使用较长的名称;
测试: 1.测试用例先行,早于生产代码一步; 2.边界测试完善; 3.不忽略小细节的测试; 4.测试速度要足够快速; 5.对于失败的测试用例要更加全面的进行测试;
高并发网络通讯性能测试中如何有效提升服务器吞吐量,主要取决于服务器处理的处理方法是与I/O有关的话就能够经过建立尽量多的线程来达到性能的提高,固然也要作个最大量的限制避免超过JVM容许的范围。若是是由于CPU数量限制的话,那只能经过提高硬件来解决了。2者常见的处理范围: I/O---使用套接字、连接到数据库、等待虚拟内存交换等; 处理器---数值计算、正则表达式处理、垃圾回收等; 另外编写服务器代码时,须要注意对各个功能职责的划分,避免堆砌在一块儿。
高并发下还须要注意代码执行路径的问题,未加锁的代码在多线程访问的状况下,每每获取到的结果前期百怪,很大程度上是普通的java语句生成字节码后每每是一句java对应多句字节码,并发编程下,线程访问就有可能处在各个字节码步骤下。
1.java自己就提供了不少并发的类库处理,像是Executor线程池框架,咱们能够直接使用方便支持一些简单的并发操做; 2.Synchronized锁定操做以及CAS能够很大程度上保证并发下资源访问的线程安全,大部分状况下CAS的效率都会比Synchronized高; 3.有些类操做并非线程安全的,好比数据库链接、部分容器类操做等; 4.对于多个单方法线程安全的操做,组合起来就不必定是安全的,方法之间的依赖可能破坏并发代码,好比如下2个例子:
案例一:
对于HashTable map,存在如下2个线程安全的操做:
public synchronize boolean containsKey(key);
public synchronize void put(key, value);
可是组合起来要实现假设map不存在才容许添加的操做时:
if(!map.containsKey(key)) {
map.put(key, valie);
}
复制代码
单线程执行下并不会有啥问题,可是高并发下就有几率出现线程1经过containsKey的判断后,可能cpu时间片切换等问题,线程1等待,轮到线程2执行也经过了containsKey,这时候就会出现线程1和线程2都会排队执行到put操做,与本来期待不符合。
解决办法:
能够是整合成一个api操做进行锁定:
public synchronize void putIfAbsent(key, value) {
if(!map.containsKey(key)) {
map.put(key, valie);
}
}
或者受到调用前手动锁定代码块:
synchronize(map) {
if(!map.containsKey(key)) {
map.put(key, valie);
}
}
复制代码
案例二:
public class IntegerIterator {
Integer value = 0;
public synchronize boolean hasNext(){
if (value < 100){
return true;
}
return false;
}
public Integer Integer next() {
if(value == 100) {
throw new xxxException();
}
return value++;
}
}
复制代码
使用:
IntegerIterator iterator = new IntegerIterator();
while(iterator.hasNext()) {
int value = iterator.next();
// ...
}
复制代码
该例子一样在高并发下容易出问题,可是可能须要好久才能出现而且难以跟踪,由于须要再边界时value = 99,此时多个线程同时经过了hasNext判断后,后续就会抛出对应的异常。解决方法相似案例一
多线程下对有限资源的争夺容易形成死锁、互斥、循环等待等等异常状况。在实际使用中咱们能够优化代码减小此类状况的发生,好比知足线程资源的抢先机制,规划好各线程资源获取的顺序等等。因为并发引发的问题难以发现与模拟,能够尝试借用一些工具进行测试,好比IBM ConTest(表示没下载到)