单元测试
- TDD: Test-Driven-Development 测试驱动开发
- 敏捷和TDD鼓励我们编写自动化单元测试
TDD三定律
- 定律1: 在编写不能通过的单元测试前,不可编写生产代码
- 在编写生产代码之前,先编写测试用例
- 只有在编写的测试失败的情况下,才可以写生产代码
- 定律2: 只可编写刚好无法通过的单元测试,不能编译也算不过
- 单元测试应当尽量拆分的小一些
- 定律3: 只可编写刚好足以通过当前失败测试的生产代码
- 编写尽量少的生产代码,尽快脱离测试失败的状态
保持测试整洁
- 如果测试不能保持整洁,你就会失去测试,没有了测试,就无法保证生产代码的可扩展性
- 正是单元测试保证你的代码可扩展,可维护,可复用
- 有了测试便不再害怕改变,没有测试,每一次改变都可能出现Bug
- 测试保证了改变,没有测试代码,你就不敢进行改动
- 测试保证了质量
整洁的测试
- 整洁测试的三个要素就是可读性、可读性、可读性
- 比起生产代码,测试代码更加注重可读性。在测试中需要使用少量的表述来说明大量的内容
- 每个测试一般都分为三个步骤:
- 构造测试数据
- 操作测试数据
- 检测结果是否符合预期
双重标准
- 和生产代码相比,测试代码有另一套的标准: 测试代码必须简单、精悍、富有表达力,同时和生产代码一样有效
- 生产环境和测试环境一般是不同的,很多不可以在生产环境做的事情,都可以在测试环境做
每个测试一个断言
- 这个标准或许比较严苛,但是好处也显而易见: 测试都只有一个容易理解的结论
- 不比过于苛责,更准确的说法是,每个测试用例中都应该使断言数量最小化
F.I.R.S.T
Fast
: 测试需要能快速执行。如果它不能快速执行,你就不会想频繁执行它,那么就不能尽早发现问题,修复问题和清理代码的成本就会变高,最终代码就会出问题Independent
: 测试应该彼此独立。一个测试不应建立在另一个测试的基础上,测试需要可以以任何顺序独立运行Repeatable
: 测试应该可以在任何环境中重复通过。生产环境、QA环境等等Self-Validating
: 无论测试成功失败都应该有布尔输出值。不应该查看日志文件来判断测试是否通过,也不应该人工比较两个文件来判断测试是否通过。Timely
: 测试应该及时编写。单元测试应该恰好在使其通过的生产代码之前编写。如果在编写生产代码之后编写测试,那么就会发现生产代码难以测试。可能会认为生产代码难以测试而不去编写测试代码
类(Class)
关注整洁代码的高层次
类应该短小
- 对于函数,我们通过其行数来衡量其是否短小。对于类,我们需要通过其职能来判断其是否短小
- 类的名称应该表明其职能,如果无法为某个类命名,大抵这个类的职能过多
- 类名越不清晰,可能这个类的职能就越多
- 开闭原则,类应当对扩展开放,对修改封闭
- 依赖倒置原则
单一职责原则
- 单一职责原则表明,类或者模块有且只有一个变更的原因
- 类应当只有一个职责——不应存在多于一个导致类变更的原因
- 在面向对象的设计中,单一权责至关重要
- 系统应该由许多短小的类,而不是少量巨大的类组成。每个小类只有一个职责,也只有一个修改的原因,并与少数其他类一起协同达成期望的系统功能
内聚
- 类中应该只有少量的实体变量,类的每个方法都操作一个或者多个变量
- 通常一个方法操作的类的变量越多,这个方法就越聚合到这个类
- 可以尝试将变量和方法拆分到两个或者更多类中,让这些类更加内聚
保持内聚可以得到许多短小的类
- 仅仅是将大函数变成许多小函数就可能会导致许多其他的类出现
- 将大函数拆成小函数,往往也是大类拆成小类的时候,程序会更加有组织
系统
将系统的构建和使用分割开
- 构建和使用是不一样的过程
分解Main
- 将构建和使用分隔开的方法之一就是将全部的构建过程搬迁到Main或者被称为Main的模块中
- main函数创建系统所需的对象,在传递给应用程序,程序只管使用
工厂
依赖注入
扩容
- 一开始就做对纯属神话
- 我们只关注当前的功能,接着重构,扩展系统去实现将来的功能
迭代
简单设计四条规则: 重要性依次降低
- 运行所有的测试
- 不可重复
- 表达程序员的意图
- 减少类和方法的数量
运行所有的测试
- 设计制造的系统必须可以像我们预期那样执行
重构
- 有了测试就可以保持代码和类的整洁,方法就是递归地重构代码
- 测试消除了清理代码就会破坏代码的恐惧
不要重复
- 重复是设计良好的系统的最大敌人
- 重复代表额外的工作,额外的风险和额外且不必要的复杂度
- 类似的代码往往可以调整的更相似,这样就更加容易重构
- 模板方法模式也是移除高级重复的通用技巧
表达力
- 软件项目的主要成本在于长期维护
- 我们需要花费大量是时间去理解代码在做什么
- 我们很容易写出自己可以理解但是其他人无法理解的代码
- 简介准确的命名、短小的函数、书写良好的单元测试都是提高表现力的手段
- 最重要的增加代码表达力的手段是尝试
- 尝试去让下一个阅读代码的人理解起来更容易,牢记,下一个阅读代码的人可能就是你自己
尽可能少的类和方法
- 凡事都有度,所以也可以理解为,函数的方法的数量也要少
- 目标: 在保持函数和类短小的同时,保持系统短小精悍
并发
误解
- 并发总是能提升性能。真实情况是并发只在多进程和多线程的程序需要进行大量的时候可以提高性能
- 编写并发程序的时候不需改变设计。真实情况是并发的程序和单线程设计基本不同
- 在使用WEB或EJB容器的时候,不需要关注并发问题。真实情况是最好要理解你的容器在做什么
关于并发的一些相对中肯的观点
- 并发在性能以及编写其他代码方面都会产生一些开销
- 即使是简单的问题实现正确的并发也是复杂的
- 并发错误通常不可重复,通常将它们视为一次性问题而不是真正的缺陷
- 并发通常需要从设计上进行根本性改变
并发防御原则
单一职责原则
- 类/方法/组件只应该有一个改变的原因。而并发设计本身就足够复杂到可以成为修改的理由
- 并发相关的代码应该有自己的开发,改变和调优的生命周期
- 并发相关的代码往往面临比非并发代码更高的挑战
- 建议分离并发相关的代码与其他代码
推论: 限制数据作用域
- 两个线程共同修改一个共享变量的同一字段时就会发生不合预期的行为
- 通常的解决办法就是设置共享变量的临界区
- 但是需要谨记数据封装,严格限制对可能被共享的数据的访问
推论: 使用数据副本
- 避免共享数据最好的方法就是一开始就避免共享数据
- 某些情况下以只读的形式处理复制的数据可以大大减少出错的可能
推论: 线程应该尽可能独立
- 尝试将数据分解到可被独立线程操作的独立集合上面
执行模型
- 生产者-消费者模型
- 读者-写者模型
- 哲学家就餐问题
警惕同步方法之间的依赖
- 同步方法之间的依赖性会导致并发代码中的细微错误
- 避免使用一个共享对象的多个方法
尽可能缩小同步的区域
- 临界区加锁会带来额外的开销,所以我们最好尽可能少的设计临界区
测试代码的正确性
- 确保并发代码的正确性基本是不可能的
- 测试无法保证正确性,但是良好的测试可以降低风险