十、类
除非我们将注意力放到代码组织的更高层面,就始终不能得到整洁的代码
10.1 类的组织
- 遵循标准的java公约,类应该从一组变量列表开始,如果有公共静态常量,应该先出现。然后是私有的静态变量,以及私有实体变量,很少会有公共变量。
- 公共函数应跟着变量列表之后,我们喜欢把由某个公共函数调用的私有工具函数紧随在该公共函数后面。
- 我们喜欢保持变量和工具函数的私有性,但不执著于此。
10.2类应该短小
对于函数,我们通过计算代码行数衡量大小,对于类,我们采用不同的衡量方法,计算权责。
类的名称应当描述其权责。实际上,命名正是帮助判断类的长度的第一个手段
单一权责原则
- 单一权责认为,类或者模块应有且只有一条加一下修改的理由。
- 类只应该有一个权责。鉴别权责(修改的理由)常常帮助我们在代码中认识到并创建出更好的抽象类。
- 许多开发者害怕数量巨大的短小单一目的的类会导致难以一目了然抓住全局,他们认为,要搞清楚一件交大工作如何完成,就得在类与类之间找来找去。但是这并不是事实。
- 每个达到一定规模的系统都会包括大量的逻辑和复杂性,管理这种复杂性的首要目的就是加以组织,以便开发者知道到哪里找到东西,并且在某个特定时间只需要理解直接有关的复杂性。反之,又有巨大、多目的类的系统,总是让我们在目前并不需要了解的一大堆东西中艰难跋涉。
- 系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。
内聚
- 类应该只有少量实体变量。类中的每个方法都应该操作一个或者多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类上。
- 内聚性高,意味着类中的方法和变量相互依赖、互相结合成一个逻辑整体。
- 保持函数和参数列表短小的策略,有时会导致为一组子集方法所用的实体变量数量增加。出现这种情况时,往往意味着至少有一个类要从大类中挣扎出来。你应该尝试将这些变量和方法拆分到两个或者多个类中,让新的类更为内聚。
保持内聚性就会得到许多短小的类
- 将大函数拆为许多小函数,往往也是将类拆分为多个小类的时机。程序会更加有组织,也会拥有更为透明的结构。
- 【重构后的程序比之前的程序长了很多,这里有几个原因: 其一,重构和的程序采用了更长、更有描述性的变量名。其二。重构后的程序将函数和声明当做代码添加注释的一种手段。其三,我们采用了空格和格式技巧让程序更可读。】
10.3为了修改而组织
- 对于多数系统,修改将直接持续。在整洁的系统中,我们对类加以组织,以降低修改的风险。
- 类应当对扩展开放,对修改封闭。通过子类化的手段,是类添加新功能时开放的,而且可以同时不触及其他类。
- 需求会变化,所以代码也会变化。具体的类包含了实现细节,而抽象类只呈现概率,依赖与具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来了隔离这些细节带来的影响。
- 通过降低链接度,我们的类就遵循了另一条设计原则,依赖倒置原则。
十一、系统
城市能运转,还因为它演化出恰当的抽象等级和模块。好让个人和他们所管理的“组件”即使在不了解全局时也能有效地运转。
有些人负责全局,有些人负责细节。
整洁的代码帮助我们在较低的抽象层级上达成这一目的。
11.1将系统的构建和使用分开
- 构建和使用是非常不一样的过程。
- 将关注的方面分离开,是软件技艺中最古老也最重要的设计技巧。
- 软件系统应该将起始过程和起始过程之后的运行时逻辑分离开,在起始过程中构建应用对象,也会存在相互纠缠的依赖关系。
分解main
将构建与使用分开的方法之一就是将全部构建过程搬迁到main或者被称为main的某块中,设计系统的其余部分时,假设所有对象都已经正确构建和设置。
工厂
当然,有时应用程序也需要负责何时创建对象。我们可以使用抽象工程模式让应用自行控制何时创建对象,但构造的细节却隔离于应用程序代码之外。
依赖注入
依赖注入可以实现分离构建与使用。在依赖管理情景中,对象不应负责实体化对象的依赖,反之,它应当将这份全责转移交给其他“有权力”的机制,从而实现控制的反转。
11.2扩容
‘一开始就做对系统’纯属神话。反之,我们应该只去实现今天的用户故事,然后重构,明天再去扩展系统、实现新的用户故事。这就是迭代和增量敏捷的精髓所在。测试驱动开发、重构以及他们打造出的整洁代码,在代码层面保证了这个过程的实现。
11.3java代理
代理适用于简单的情况。
11.4纯javaaop框架
11.5aspectJ的方面
11.6测试驱动系统架构
11.7 优化决策
模块化和关注面切分成就了分散化管理和决策。
拥有模块化关注面的POJO系统提供的敏捷能力,允许我们基于最新的知识做出最优化的、时机刚好的决策。决策的复杂性也降低了。
11.8明智使用添加了可论证价值的标准
有了标准,就更容易用想法和组件、雇佣拥有相关经验的人才、封装好点子,以及将组件连接起来。不过,创立标准的过程有时却漫长到行业等不及的程度,有些标准没有能与它要服务的采用者的真实需求相结合。
11.9系统需要领域特定的语言
- 领域特定语言允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用POJO表达
- 在所有的抽象层级上,意图都应该清晰可辩。
- 无论是设计系统或者单独的模块,别忘记了使用大概可工作的最简单方案。
十二、迭进
12.1通过迭进设计达到整洁目的
设计必须制造出如预期一般的工作系统,这是首要因素。
全面测试并持续通过所有测试的系统,就是可测试的系统。
紧耦合的代码难以编写测试。同样,编写测试越多,就越会遵循DIP之类的规则,使用依赖注入、接口和抽象等工具尽可能减少耦合。如此一来,设计就有长足的进步。
12.2简单设计规则-1 运行所有测试
有了测试,就能保持代码和类的整洁,方法就是递增式地重构代码。添加几行代码后,就要暂停,琢磨一下,变化了的设计。设计退步了吗?如果是,就要清理他,并且运行测试,保证没有破坏任何东西。测试消除了对清理代码就会破坏代码的恐惧。
12.3简单设计规则2-4 重构
消除重复,保证表达力,尽可能减少类和方法的数量。
12.4不可重复
重复是拥有良好设计系统的大敌。它代表额外的工作、额外的风险和额外且不必要的复杂度。
"小规模复用"可大量降低系统复杂性。
模板方法模式是一种移除高层级重复的通用技巧
12.5表达力
作者把代码写得越清晰,其他人花在理解代码上的时间也就越少,从而减少缺陷,缩减维护成本。
- 可以通过选好的名称来表达,我们想要听到类名和好函数名,而且查看其权责时不会大吃一惊。
- 也可以通过采用标准命名法来表述
- 也可以通过保持函数和类的尺寸短小来表达。短小的类和函数通常易于命名,易于编写,易于理解
- 编写良好的单元测试也是具有表达性。
12.6尽可能少的类和方法
我们的目标是在保持函数和类短小的同时,保持整个系统短小精悍,不过要记住,这在关于简单设计的四条规则里面优先级最低。
尽管是类和函数的数量尽量少是重要的,但是更重要的是却是测试、消除重复和表达力。
十三、并发编程
编写整洁的并发程序很难-非常难。
13.1 为什么要并发
并发是一种解构策略,他帮助我们把做什么(目的)和何时(时机)做分解开。
- 并发会在性能和编写额外代码上增加一些开销
- 正确的并发是复杂的,即便对于简单的问题也是如此
- 并发缺陷并非总是能重现,所以常被当做偶发事件而忽略,未被当做真的缺陷看待。
- 并发尝尝需要对设计策略的根本性修改。
13.2 挑战
13.3并发防御原则
- 单一权责原则 建议:分离并发相关代码和其他代码
- 推论:限制数据作用域 建议:谨记数据封装,严格限制对可能被共享的数据的访问。
- 推论:使用数据副本
- 推论:线程应尽可能地独立 建议:尝试将数据分解到可被独立线程操作的独立子集。
13.4了解java库
- 使用类库提供的线程安全集群
- 使用executor框架执行无关任务
- 尽可能使用非锁定解决方案
- 有几个类并不是线程安全的
13.5了解执行模型
13.6警惕同步方法之间的依赖
避免使用一个共享对象的多个方法。
13.7保持同步区域微小
将同步延展到最小临界区域范围外,会增加资源争用、降低执行效率
13.8很难编写正确的关闭代码
尽早考虑关闭问题,尽早令其工作正常、
13.9测试线程代码
编写有潜力暴露问题的测试,在不同的编程配置、系统配置和负债条件下频繁运行,如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败。
十四、逐步改进
毁坏程序的最好方法之一就是以改进之名大动其结构。有些程序永远不能从这种所谓”改进“中恢复过来。问题在于,很难让程序以”改进“之前的方式工作。
为了避免这种情况发生,采用测试驱动开发的规程。这种手法的核心原则之一是保持系统始终能运行。换言之,采用TDD,不允许做出破坏系统的修改。每次修改必须保证系统像以前一样工作。
每次修改一个地方,持续运行测试,如果测试出错,在做下一个修改前确保其通过。
放进拿出是重构过程中常见的事情。小步幅和保持测试通过,意味着你会不断的移动各种东西。重构有点像解魔方。需要经过许多小步骤,才能达到较大的目标。每一步都是下一步的基础。
保持代码持续整洁和简单。永远不然腐坏有机会开始。
十五、JUnit内幕
十六、重构SerialDate
十七、味道与启发
坏味道(需要修改的点)
1、注释:
- 不恰当的信息
- 废弃的注释
- 冗余注释
- 糟糕的注释
- 注释掉的代码
2、环境:
- 需要多少步才能实现的构建
- 需要多少步才能做到的测试
3、函数:
- 过多的参数
- 输出参数
- 标识参数(布尔值参数大声宣告函数不止做了一件事,令人迷惑)
- 死函数(永不被调用的方法应该被丢弃)
4、一般性问题
- 一个源文件中存在多种语言
- 明显的行为未被实现
- 不正确的边界行为(别依赖直觉,追索每一种边界条件,并编写测试代码)
- 忽视安全
- 重复
- 在错误的抽象层级上的代码
- 基类依赖于派生类
- 信息过多(设计良好的模块有着非常小的接口)
- 死代码
- 垂直分隔
- 前后不一致
- 混淆视听
- 人为耦合
- 特性依恋
- 选择算子参数
- 晦涩的意图
- 位置错误的权责
- 不切当的静态方法
- 使用解释性变量
- 函数名称应该表达其行为
- 理解算法
- 把逻辑依赖改为物理依赖
- 用多态代替if/else 或者Switch/case
- 遵循标准约定
- 用命名常量代替魔术数
- 准确
- 结构基于约定
- 封装条件
- 避免否定性条件
- 函数只该做一件事
- 掩蔽时序耦合
- 别随意
- 封装边界条件
- 函数应该只在一个抽象层级上
- 在较高层级放置可配置数据
- 避免传递浏览
5、java
- 通过使用通配符避免过长的导入清单
- 不要集成常量
- 常量 VS 枚举
6、名称
- 采用描述性名称
- 名称应与抽象层级相符
- 尽可能使用标准命名法
- 无歧义的名称
- 为较大作用范围选用较长名称(名称的作用范围越大,名称就该越长,越准确)
- 避免编码
- 名称应该说明副作用
7、测试
- 测试不足
- 使用覆盖率工具
- 别忽略小测试
- 被忽略的测试就是对不确定事物的疑问
- 测试边界条件
- 全面测试相近的缺陷
- 测试失败的模式有启发性
- 测试覆盖率的模式有启发性
- 测试应该快速