Clean Code读书笔记
一.整洁代码
如何做到整洁
怎么做 | 为什么 |
---|---|
代码逻辑直接了当 | 便于理解 |
有意义的命名 | 便于理解 |
减少依赖关系 | 便于维护 |
分层战略完善错误 | 缺陷难以隐藏 |
有单元测试和验收测试 | 风险控制 |
尽量“少” | 单一职责原则 |
如果每个例程都让你感到深合己意,那就是整洁代码,如果代码让编程语言看上去像是专为解决那个问题而存在,就可以称之为漂亮的代码。
二.有意义的命名
- 不要以数字系列命名
- 相似的类要加以区分:如:Product类、ProductInfo类、ProductData、ProductObject类的区别
- 使用读的出来的名称,可使用谐音,如2,4
- 使用可搜索的名称,不要过短或者过普遍
- 方法名最好是动词或动词短语
- 方法中, 可增加局部变量, 以增加代码的可读性
每段代码都有其实际应用的场景,一段代码如果难以理解,很多时候并不是因为代码复杂,而是代码的模糊度较高,难以明确代码的意图。
三.函数
基本规范
- 短小
- 只做一件事
public static String RenderPageWithSetupAndTeardowns(PageData pageData, Boolean isSuite) throws Exception{
if ( isTestPage( pageData ) ) {
includeSetupAndTeardownPages( pageData , isSuite );
}
return pageData.getHtml( );
}
/*
看起来像是做了三件事:
判断是否为测试页面;如果是,则包含设置和分析步骤;返回Html。
Bob 提供了”To”(要)原则,也就是以To起头段落来描述这个函数。
要(To) RenderPageWithSetupAndTeardowns,检查页面是否为测试页,如果是测试页,就包含设置和分析步骤,无论是不是测试页,都返回HTML。
这三件事情都处于一个抽象层次上,所以RenderPageWithSetupAndTeardowns做了一件事。
判断函数只做了一件事还有另一个方法:看能否再拆出一个函数
*/
函数参数
- 参数越少,困难越少
- 参数超过三个封装成类
- 禁止将boolean类型作为参数:宣布函数不止做一件事
函数的抽象层
- 函数的抽象层级以功能来划分,比如去买书,抽象层级:买书>去书店>走路
- 函数的抽象层级的最高层类似于人类自然语言,使代码清晰易懂,简洁。
- 确保,每个函数,一个抽象层级,也就是说一个函数中的语句都要在同一抽象层级上。我们想要让代码拥有自顶向下的阅读顺序,因此让每个函数后面都跟着下一抽象层级的函数,此举方便了以后修改代码和阅读代码。
函数编写细则
- 不要重复自己
- 函数应该只做一件事
- 函数要么“做什么事”,要么“回答什么事”。即是将指令和询问分隔开来。
- 抽离try/catch代码块。把其主体部分抽离出来,另外写成函数。错误处理的代码应该就是包含它的函数的所有代码
沃德原则:如果每个历程都让你感到深和己意,那就是整洁的代码
四.注释
- 提供信息的注释。例如解释某个抽象方法的返回值,规定参数的顺序和个数。
- 对意图的解释。使别人更清楚一段复杂代码是在干什么。
注释不能美化糟糕的代码
用代码来阐述
五.格式
垂直格式
- 变量声明尽可能靠近其使用位置。
- 最上面的代码应该是最抽象的,底部细节应该在下面实现
- 若某个函数调用另一个,就应当把他们放在一起。
横向格式
1.适当对齐,间隔
团队规则
- 一组开发者应当认同一种模式风格,每个成员都应该采用那种风格
- 好的软件系统是由一系列读起来不错的代码文件组成的,需要拥有一致和顺畅的风格
代码格式关乎沟通,而沟通是专业开发者的头等大事
六.对象和数据结构
对象和数据结构的区别
- 数据结构中的对象只是数据,面向对象中的对象包括了数据和行为。
- 数据结构暴露其数据,没有提供有意义的函数;对象把数据隐藏于抽象之后,暴露操作数据的函数。
- 数据结构难以添加新的的数据类型,因为需要改动所有函数,面向对象的代码则难以添加新的函数,因为需要修改所有的类。
得墨忒耳律
-
得墨忒耳律(The Law of Demeter):模块不应了解它所操作对象的内部情形,意味着对象不应通过存取器曝露其内部结构,因为这样更像是曝露而非隐藏其内部结构
- 类C的方法f只应调用以下对象的方法:
- C
- 由f创建的对象
- 作为参数传递给f的对象;
- 由C的实体变量持有的对象;
- 类C的方法f只应调用以下对象的方法:
方法不应调用由任何函数返回的对象的方法,换句话说,只和朋友说话,不和陌生人说话。以下就是违反该法则的一段代码:
七.错误信息
使用异常而非返回码
- 遇到错误时,最好抛出一个异常。调用代码很整洁,其逻辑不会被错误处理搞乱
先写Try-Catch-Finally语句
- 异常的妙处之一是,它们在程序中定义了一个范围。执行try-catch-finally语句中try部分的代码时,你是在表明可随时取消执行,并在catch语句中接续
- try代码块就像是事务,catch代码块将程序维持在一种持续状态
- 在编写可能抛出异常的代码时,最好先写try-catch-finally语句,能帮你定义代码的用户应该期待什么,无论try代码块中执行的代码出什么错都一样
使用不可控异常
- 可控异常,就是指在方法签名中标注异常。但有时候会产生多层波及,有时候你对较底层的代码修改,可能会波及很多上层代码
给出异常发生的环境说明
- 抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所
- 应创建信息充分的错误消息,并和异常一起传递出去
定义常规流程
- 特例模式(SPECIAL CASE PATTERN,[Fowler]),创建一个类或配置一个对象,用来处理特例,异常行为被封装到特例对象中
别返回null值
- 在方法中返回null值,不如抛出异常或返回特例对象
错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法
八.边界(使用第三方代码)
学习性测试
- 学习性测试毫无成本,编写测试是获得这些知识(要使用的API)的容易而不会影响其他工作的途径
- 学习性测试确保第三方程序包按照我们想要的方式工作
整洁的边界
- 边界上的改动,有良好的软件设计,无需巨大投入和重写即可进行修改
- 边界上的代码需要清晰的分割和定义了期望的测试。依靠你能控制的东西,好过依靠你控制不了的东西,免得日后受它控制
- 可以使用ADAPTER模式将我们的接口转换为第三方提供的接口
九.单元测试
TDD三定律
- 在编写能通过的单元测试前,不可编写生产代码(测试先行)
- 只可编写刚好无法通过的单元测试,不能编译也算不通过(测试一旦失败,开始写生产代码)
- 只可编写刚好足以通过当前失败测试的生产代码(老测试一旦通过,返回写新测试)
保持测试整洁
- 脏测试等同于没测试,测试必须随生产代码的演进而修改,测试越脏,就越难修改
- 测试代码和生产代码一样重要,它需要被思考、被设计和被照料,它该像生产代码一般保持整洁
- 如果测试不能保持整洁,你就会失去它们,没有了测试,你就会失去保证生产代码可扩展的一切要素
单元测试的固定模式
构造-操作-检验模式写成一个函数
- 第一个环节构造测试数据
- 第二个环节操作测试数据的
- 第三个环节,检验操作是否得到期望的结果。
每个测试一个断言
- JUnit中每个测试函数都应该有且只有一个断言语句
- 最好的说法是单个测试中的断言数量应该最小化
- 更好一些的规则或许是每个测试函数中只测试一个概念
- 最佳规则是应该尽可能减少每个概念的断言数量,每个测试函数只测试一个概念
F.I.R.S.T
- 快速(Fast)测试应该够快
- 独立(Independent)测试应该相互独立
- 可重复(Repeatable)测试应当可在任何环境中重复通过
- 自足验证(Self-Validating)测试应该有布尔值输出,自己就能给出对错,而不需要通过看日志,比对结果等方式验证
- 及时(Timely)测试应及时编写
十.类
类的组织
- 类应该从一级变量列表开始,如果有公共静态变量,应该先出现,然后是私有静态变量,以及实体变量,很少会有公共变量
- 公共函数应该跟在变量列表之后
- 保持变量和工具函数的私有性,但并不执着于此
类应该短小
- 类名应该精确,职责单一,并且能够描述其权责
- 内聚性强,方法操作的变量越多,就越黏聚到类上,如果一个类的每个变量都被每个方法所使用,则该类具有最大的内聚性
- 保持函数和参数列表短小的策略,有时会导致为一组子集方法所用的实体变量数量增加。出现这种情况时,往往意味着至少有一个类要从大类中挣扎出来。你应当尝试将这些变量和方法分拆到两个或多个类中,让新的类更为内聚
- 将大函数拆为许多小函数,往往也是将类拆分为多个小类的时机
为了修改而组织
- 在整洁的系统中,我们对类加以组织,以降低修改的风险
- 开放-闭合原则(OCP):类应当对扩展开放,对修改封闭
- 在理想系统中,我们通过扩展系统而非修改现有代码来添加新特性
- 依赖倒置原则(Dependency Inversion Principle,DIP),类应该依赖于抽象而不是依赖于具体细节
我们知道编写一个类不是一触而就的,而是通过了无数次修进的。而系统的每处修改(添加功能,改变逻辑方法等)都让我们冒着系统会出现问题的风险。这时候我们要对类加以修进(组织和重构),以降低修改所面临的风险。
十一.系统
如何建造一个城市
- 每个城市都有一组人管理不同的部分,有人负责全局,其他人负责细节
- 深化出恰当的抽象等级和模块,好让个人和他们所管理的“组件”即便在不了解全局时也能有效地运转
将系统的构造与使用分开
- 构造与使用是非常不一样的过程
- 软件系统应将启始过程和启始过程之后的运行时逻辑分离开,在启始过程中构建应用对象,也会存在互相缠结的依赖关系
- 将构造与使用分开的方法之一是将全部构造过程搬迁到main或被称为main的模块中,设计系统的其余部分时,假设所有对象都已正确构造和设置
- 可以使用抽象工厂模式让应用自行控制何时创建对象,但构造的细节却隔离于应用程序代码之外
- 控制反转将第二权责从对象中拿出来,转移到另一个专注于此的对象中,从而遵循了单一权责原则。在依赖管理情景中,对象不应负责实体化对自身的依赖,反之,它应当将这份权责移交给其他“有权力”的机制,从而实现控制的反转
扩容
- “一开始就做对系统”纯属神话,反之,我们应该只去实现今天的用户故事,然后重构,明天再扩展系统、实现新的用户故事,这就是迭代和增量敏捷的精髓所在。测试驱动开发、重构以及它们打造出的整洁代码,在代码层面保证了这个过程的实现
- 软件系统与物理系统可以类比。它们的架构都可以递增式的增长,只要我们持续将关注面恰当地切分
- 持久化之类关注面倾向于横贯某个领域的天然对象边界
Java代理AOP
- 通过方面式(AOP)的手段切分关注面的威力不可低估。假使你能用POJO编写应用程序的领域逻辑,在代码层面与架构关注面分离开,就有可能真正地用测试来驱动架构
- 没必要先做大设计(Big Design Up Front,BDUF),BDUF甚至是有害的,它阻碍改进,因为心理上会抵制丢弃即成之事,也因为架构上的方案选择影响到后续的设计思路
- 我们可以从“简单自然”但切分良好的架构开始做软件项目,快速交付可工作的用户故事,随着规模的增长添加更多基础架构
- 最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java(或其他语言)对象实现,不同的领域之间用最不具有侵害性的方面或类方面工具整合起来,这种架构能测试驱动,就像代码一样
优化决策
- 模块化和关注面切分,成就了分散化管理和决策
- 延迟决策至最后一刻也是好手段,它让我们能够基于最有可能的信息做出选择
- 拥有模块化关注面的POJO系统提供的敏捷能力,允许我们基于最新的知识做出优化的、时机刚好的决策,决策的复杂性也降低了
明智使用添加了可论证价值的标准
- 有了标准,就更易复用想法和组件、雇用拥有相关经验的人才、封装好点子,以及将组件连接起来。不过,创立标准的过程有时却漫长到行业等不及的程度,有些标准没能与它要服务的采用者的真实需求相结合
系统需要领域特定语言
- 领域特定语言(Domain-Specific Language, DSL)是一种单独的小型脚本语言或以标准语言写就的API,领域专家可以用它编写读像是组织严谨的散文一般的代码
- 领域特定语言允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用POJO来表达
城市能运转,还因为它演化出来恰当的抽象等级和模块,好让个人和他们所管理的“组件”在不了解全局时也能有效运转。
十二.迭代
简单规则:
- 通过所有测试
- 不可重复
- 表达了程序员的意图
- 尽可能减少类和方法的数量
- 以上规则按其重要程序排列
十三.并发编程
为什么要并发
- 并发是一种解耦策略,它帮助我们把做什么(目的)和何时(时机)做分解开
- 解耦目的与时机能明显地改进应用程序的吞吐量和结构
- 单线程程序许多时间花在等待web套接字I/O结束上面,通过采用同时访问多个站点的多线程算法,就能改进性能
常见的迷思和误解
- 并发总能改进性能:只在多个线程或处理器之间能分享大量等待时间的时候管用
- 编写并发程序无需修改设计:可能与单线程系统的设计极不相同
- 在采用web或ejb容器时,理解并发问题并不重要
有关编写并发软件的中肯的说法
- 并发会在性能和编写额外代码上增加一些开销
- 正确的并发是复杂的,即使对于简单的问题也是如此
- 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待
- 并发常常需要对设计策略的根本性修改
并发防御原则
- 单一权责原则
- 限制数据作用域
- 使用数据副本
- 线程应尽可能独立
了解Java库
- 使用类库提供的线程安全群集
- 使用executor框架(executor framework)执行无关任务
- 尽可能使用非锁定解决方案
- 有几个类并不是线程安全的
了解执行模型
警惕同步方法之间的依赖
保持同步区域微小
很维编写正确的关闭代码
- 平静关闭很难做到,常见问题与死锁有关,线程一直等待永远不会到来的信号
- 建议:尽早考虑关闭问题,尽早令其工作正常
测试线程代码
- 建议:编写有潜力曝露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败
- 将伪失败看作可能的线程问题:线程代码导致“不可能失败的”失败,不要将系统错误归咎于偶发事件
- 先使非线程代码可工作:不要同时追踪非线程缺陷和线程缺陷,确保代码在线程之外可工作
- 编写可插拔的线程代码,能在不同的配置环境下运行
- 编写可调整的线程代码:允许线程依据吞吐量和系统使用率自我调整
- 运行多于处理器数量的线程:任务交换越频繁,越有可能找到错过临界区域导致死锁的代码
- 在不同平台上运行:尽早并经常地在所有目标平台上运行线程代码
- 装置试错代码:增加对Object.wait()、Object.sleep()、Object.yield()、Object.priority()等方法的调用,改变代码执行顺序,硬编码或自动化
十四.逐步改进
- 要编写清洁代码,必须先写肮脏代码,然后再清理它
- 毁坏程序的最好方法之一就是以改进之名大动其结构