阅读本书有两种原因:第一,你是个程序员;第二,你想成为更好的程序员。
本文概括性的阐述了本书每章节的主要内容,感兴趣的话可以结合本书深入理解并延伸相关观点。
1.整洁代码
- 优雅和高效的代码,代码逻辑应当直截了当,令缺陷难以隐藏,尽量减少依赖关系,使之便于维护,依据某种分层战略完善错误处理代码,性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来,整洁的代码只做好一件事。——C++语言发明者,Bjarne
- 简单代码规则(消除重复,提高表达力):能通过所有测试,没有重复代码,体现系统中的全部设计理念,包括尽量少的实体,比如类,方法,函数等。——《极限编程实施》
- 整洁的代码简单直接, 如同优美的散文,整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直接了当的控制语句。——《面向对象分析与设计》
- 整洁的代码应可由作者之外的开发者阅读和增补,应当有单元测试和验收测试,使用有意义的命名,它只提供一种而非多种做一件事的途径,它只有尽量减少的依赖,而且要明确地定义和提供清晰,尽量少的API,代码能通过字面表达含义,因为不同语言导致并非所有必须的信息均可通过代码自身清晰表达。——Dave Thomas
2.有意义的命名
- 名副其实,能指明其计量对象和计量单位;
- 避免误导,不使用平台关键词作为变量名,包括1与l,0与O之间的误导性;
- 做有意义的区分,误区:以数字系列命名(a1,a2,...an)没有提供准确信息和导向作者意图的线索;命名的冗余(Product类中命名ProductInfo子类);
- 使用读的出来的名称,便于记忆和使用的单词;
- 使用可搜索的名称,避免单字母名称和数字;
- 避免使用编码,误区:匈牙利语表记法,成员前缀(m_),接口和实现(前导字母I);
- 类名,应是名词或名词短语;
- 方法名,应是动词或动词短语;
- 每个概念对应一个词,一以贯之;
- 避免使用双关语(如add,insert,append方法区别命名);
- 使用解决方案领域名称,使用源自所涉问题领域名称;
- 添加有意义的语境,例如add,guess,create。
3.函数
- 函数应该短小,一个函数只做一件事,每个函数一个抽象层级,如此让代码拥有自顶向下的阅读顺序;
- 关于switch语句的问题:实际代码长,不止处理一件事,违反了单一全责原则,违反了开放闭合原则;
- 使用具有描述性的名称:长而有描述性的名称比短而令人费解的名称好;
- 函数参数:单参数函数(普遍形式),标识参数0/1(尽量避免),多参数(应封装成类),参数列表,动词和关键词;
- 无副作用:慎重考虑函数的时序性和有效性;
- 使用异常代替返回错误码:逻辑不会被错误处理搞乱(隔离设备关闭算法和错误处理算法);
- 不重复原则:避免冗余,面向方面编程&面向组件编程;
- 结构化编程:在小函数中,应有一个入口,一个出口(return),且尽量避免break ,continue语句,禁止goto语句。
4.注释
- 注释不能美化糟糕的代码,尽量用代码来阐述;
- 好注释:法律信息,提供信息的注释,对意图的解释,阐释晦涩的参数或翻译返回值意义,警示可能的后果,Todo注释,放大某处重要性,公共API;
- 坏注释:喃喃自语,多余的注释,误导性注释,循规式注释,日志式注释,废话注释,位置标记,括号后面的注释,归属和署名,注释掉的代码,HTML注释,非本地信息,信息过多,不明显的联系,函数头。
5.格式
- 格式的目的:代码格式关乎沟通,沟通是专业开发者的头等大事;
- 垂直格式:概念间垂直方向上的间隔,垂直方向上的靠近,垂直距离,垂直顺序;
- 横向格式:水平方向的区隔和靠近,水平对齐,缩进,空范围;
- 团队规则:一组开发者应当认同一种风格,并采用它一以贯之。
6.对象和数据结构
-
数据抽象:类不简单地用取值器和赋值器将变量推向外界,而是暴露接口,以便用户无须了解数据的实现就能操作数据本体;
-
数据,对象的反对称性(二分原理):过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不改动既有函数的前提下添加新类;也可以理解为,过程式代码难以添加新数据结构,因为必须修改所有函数,面向对象代码难以添加新函数,因为要改所有的类;
-
得墨忒耳律:模块不应了解它所操作的对象的内部情形;对象隐藏数据,暴露操作;即类C的方法f只能调用以下:
C;由f创建的对象;作为参数传递给f的对象;由C的实体变量持有的对象;
-
数据传送对象(DTO):最为精练的数据结构,一个只有公共变量,没有函数的类。
-
对象暴露行为,隐藏数据,便于添加新对象类型而无需修改既有行为,同时难以向既有对象添加新行为;数据结构暴露数据,没有明显行为,便于向既有数据添加新行为,同时难以向既有函数添加数据结构。
7.错误处理
- 使用异常而非返回错误码:这样调用代码会很整洁,逻辑不会被错误处理搞乱(隔离设备关闭算法和错误处理算法);
- 先写try-catch-finally语句:try代码块是事务,catch代码块将程序维持在一种持续状态;
- 使用未检异常:已检异常通常会破坏封装性,依赖成本会高于收益;
- 给出异常发生的环境说明:以判断错误的来源和位置,同时应创建信息充分的错误信息(失败类型,失败操作),和异常一起传递出去。
- 依调用者需要定义异常类:对于代码某个特定区域,单一异常类通常可行。当你想捕获某个异常忽略其他异常,可以使用不同异常类;
- 定义常规流程:通过特例模式,把异常行为封装到特例对象中,以应对任何失败的运算;
- 不返回和传递null值,这样可以避免很多无心之失;
- 整洁代码是可读的,也需要强固,将错误处理隔离看待,独立于主要逻辑之外,能写出强固而整洁的代码,这样我们可以单独处理它,并且极大地提升代码的可维护性。
8.边界
- 使用第三方代码:在接口提供者和使用者之间存在与生俱来的矛盾,第三方程序包和框架提供者追求普适性从而吸引广泛用户,而使用者想要得到集中满足特定需求的接口,这种矛盾会导致系统边界上的问题;
- 学习性测试:能确保第三方程序包按照我们想要的方式工作,一旦整合起来,就不能保证第三方代码总与我们的需求兼容。使用边界测试可以减轻迁移的劳力,不然可能超出应有时限,长久的绑定在旧版本上面;
- 整洁的边界:边界上的代码需要清晰的分割和定义了期望的测试,应该避免自己的代码过多地了解第三方代码的特定信息,依靠你能控制的东西,好过依靠你控制不了的东西,免得日后受他控制。
9.单元测试
-
TDD三定律:
定律一 在编写不能通过的单元测试前,不可编写生产代码。
定律二 只可编写刚好无法通过的单元测试,不能编译也算不通过。
定律三 只可编写刚好足以通过当前失败测试的生产代码。
-
保持测试整洁,测试代码和生产代码一样重要 ,需要良好设计和仔细划分,遵循生产代码的质量标准;
-
整洁的测试,唯一要素就是可读性;测试代码应当简单,精悍,足具表达力;
-
每个测试一个断言:每个测试函数只测试一个概念;
-
FIRST五规则:快速Fast,独立Independent,可重复Repeatable,自足验证Self-Validating(布尔值输出),及时Time
10.类
- 类的组织,封装以保持变量和工具函数的私有性;
- 类应该短小:函数通过代码行数衡量大小,类则通过权责(responsibility)衡量大小;
- 单一权责原则:类或模块应该有且只有一条加以修改的理由(权责);
- 系统应该由许多短小的类组成,每一个小类封装一个权责,只有一个修改的原因,并且与少数其他类一起协同达成期望的系统行为;
- 内聚性:类应该只有少数实体变量,类中每个方法都应该操作一个或多个这种变量;保持内聚性可以得到短小的类;
- 为了修改而组织(解耦):具体类包含实现细节,抽象类只呈现概念,依赖细节的类,可以借助接口和抽象类来隔离细节改变带来的影响;
11.系统
- 软件系统应将起始过程和起始过程之后的运行时逻辑分离开,在起始过程中构建应用对象,也会存在相互缠结的依赖关系;
- 将构造和使用分开的方法之一是将全部构造过程搬迁到main或被称为main的模块中,设计系统的其余部分时候,假设所有对象都已正确构造和设置;
- 依赖注入可以实现分离构造和使用:控制反转是依赖管理中的一种应用手段,控制反转将第二权责从对象中拿出来,转移到另一个专注于此的对象中,从而遵循单一权责原则,因为初始设置是一种全局设置,所以通常这种授权机制要么是main例程,要么有特定目的的容器。
- 系统也应该是整洁的,侵害性结构会湮灭领域逻辑,冲击敏捷能力,如果领域逻辑受到困扰,质量就会堪忧,因为缺陷更易隐藏,用户功能更难实现,当敏捷能力受到损害时,生产力也会降低,TDD的好处遗失殆尽;
12.迭进
- 通过跌进以达到整洁目的;
- 简单设计的四条规则:运行所有测试;不可重复;表达了程序员的意图(提高表达力);尽可能减少类和方法的数量;
- 保持代码和类的整洁,方法就是递增式的重构代码:提高内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称。
13.并发编程
- 并发是一种解耦策略。帮助我们把做什么(目的)和何时做(时机)分解开;
- 解耦目的和时机能明显地改进应用程序的吞吐量和结构;
- 单一权责原则:分离并发相关代码和其他代码;
- 限制数据作用域:谨记数据封装,严格限制对可能被共享的数据的访问;
- 使用数据副本:能有效避免共享数据,减少导致错误的可能;
- 线程应当独立:将数据分解成被独立线程操作的独立子集;
- 执行模型:生产者-消费者模型;读者-作者模型;宴席哲学家;
- 测试线程代码:将伪失败看作可能的线程问题;先使非线程代码可工作;编写可插拔的线程代码;编写可调整的线程代码;运行多于处理器数量的线程;在不同平台上运行;装置试错代码;硬编码;自动化(使用异动策略搜出错误);
14.逐步改进
- 优秀的软件设计,大都关乎分隔——创建合适的空间放置不同种类的代码,对关注面的分隔让代码更容易理解和维护。
- 糟糕的代码可以清理,但是成本昂贵,随着模块之间相互渗透,出现大量隐藏纠缠的依赖关系,找到和破除陈旧依赖关系费时又费力。解决之道就是保持代码持续整洁和简单。
15.JUit内幕
- JUnit是最有名的Java框架之一,它概念简单,定义精确,实现优雅。
16.重构SerialDate
- SerialDate是一个用Java呈现日期的类。重构使得签出代码更整洁,测试覆盖率更高,代码清晰并明显缩短。
17.味道与启发
- 注释:不恰当的信息(注释只应描述有关代码和设计的技术性信息);废弃的注释(过时,无关,不正确);冗余注释(已充分描述过的信息);糟糕的注释(言简意赅);注释掉的代码(删除注释掉的代码,通过版本控制);
- 环境:需要多步才能实现的构建(构建系统应该是单步的小操作);需要多步才能做到的测试(单指令即可运行全部测试);
- 函数:过多的参数(参数量应尽量少);输出参数(应修改所在对象状态);标识参数(布尔值参数,尽量不用);死函数(删除不调用的函数);
- 一般性问题:一个源文件存在多种语言;明显的行为未被实现;不正确的边界行为;忽视安全;重复;在错误的抽象层级上的代码;基类依赖派生类;信息过多;死代码;垂直分隔;前后不一致;混淆视听;人为耦合;特性依恋;选择算子参数;晦涩的意图;位置错误的权责;不恰当的静态方法;使用解释性变量;函数名称不应该表达其行为;理解算法;把逻辑依赖改为物理依赖;用多态替代if/else或switch/case;遵循标准约定;用命名常量替代魔术数;准确;结构甚于约定;封装条件;避免否定性条件;函数只该做一件事;掩蔽时序耦合;别随意;封装边界条件;函数只应该在一个抽象层级上;在较高层级放置可配置数据;避免传递浏览;
- Java:通过使用通配符避免过长的导入清单;不要继承常量;常量与枚举;
- 名称:采用描述性名称;名称应该与抽象层级相符;尽可能使用标准命名法;无歧义的名称;为较大作用范围选用较长的名称;避免编码;名称应该说明副作用;
- 测试:测试不足;使用覆盖率工具;别略过小测试;被忽视的测试就是对不确定事物的疑问;测试边界条件;全面测试相近的缺陷;测试失败的模式有启发性;测试覆盖率的模式有启发性;测试应该快速;