在 “测试驱动设计,第 1 部分” 和 “测试驱动设计,第 2 部分” 中,我 介绍了测试如何为新的项目实现更好的设计。在 “组合方法和 SLAP” 中,我讨 论了两种关键模式 — 组合方法(composed method)和单一抽象层原理 — 为您 的代码结构提供了整体目标。需要牢记这些模式。一旦拥有了一个现有软件项目 ,那么发现和利用设计元素的主要方法就是进行重构。在 Martin Fowler 的经典 著作 Refactoring 中,他将重构定义为 “一种严格的技术,可以重新构造现有 代码体,修改代码的内部结构,但是不会影响代码的外部行为”。重构是一种具 有某种目的的结构转换。对任何项目来说,值得称赞的一点就是拥有可以轻松进 行重构的代码库。在本文中,我将讨论如何使用重构技术来查找隐藏在代码库中 的未得到充分设计的代码。
关于本系列
本 系列 旨在从全新的视角来介绍经常讨论但是又难以理解的软件架构和设计 概念。通过具体示例,Neal Ford 将帮助您在演化架构 和紧急设计 的灵活实践 中打下坚实的基础。通过将重要的架构和设计决定推迟到最后关键时刻,您可以 防止不必要的复杂度降低软件项目的质量。
单元测试可以提供最重要的安全屏障,允许您按照自己的意愿重构代码库。如 果您的项目的代码覆盖率达到了 100%,那么可以安全地重构代码。如果尚未达到 这个程度的测试,那么草率地进行重构就会变得比较危险。本地化修改可以很容 易地应用并且可以立即看到修改效果,但是副作用产生的破坏也会使您非常苦恼 。软件会产生无法预料的耦合点,对代码的某一部分进行微小的修改会影响到整 个代码库,造成数百行代码发生错误。要安全地修改代码并找出大量错误,需要 进行广泛的单元测试。对于一个为期 2 年的 ThoughtWorks 项目,技术主管在运 行该项目的前一天对代码进行了 53 处不同的重构。他在进行重构时信心满满, 因为项目拥有广泛的代码覆盖率。
如何实现可以进行重大重构的代码库?一个办法就是不要编写任何代码,直到 您将测试添加到整个项目中。当您提出这个建议后,您将被解雇,然后您可以去 另一家重视单元测试的公司工作。这种方法可能不是很好。另一个好方法是让团 队的其他成员认识到测试的价值并开始缓慢地围绕代码的最关键部分添加测试。 做好规划并在近期内宣布一个日期:“从下周四启动,我们的代码覆盖率将不断 增长”。每次编写新代码时,添加一个测试,每次修复一个 bug 时,编写一个测 试。通过围绕最敏感的代码部分(新特性和容易出现 bug 的部分)逐步添加测试 ,那么测试就可以发挥最大的作用。
单元测试检验原子性行为。但是,如果您的代码库没有坚持组合方法的思想, 该怎么办?换句话说,如果您的所有方法都具有几十或几百行代码,并且每个方 法执行大量的任务,那么应该怎么做?您可以使用单元测试框架来围绕这些方法 编写粗粒度功能测试,主要关注方法的输入和输出状态的转换。这种方法不如单 元测试,因为不能对行为进行彻底的检验,但是总比不采取任何措施要好。对于 代码中真正关键的部分,可能需要在进行重构之前添加一些功能测试作为一种安 全保障。
重构机制非常简单,并且所有主要 IDE 目前都提供了出色的重构支持。比较 困难的地方在于确定对哪些内容 进行重构。这就是本文其余部分要解决的问题。
与基础设施耦合
Java 世界的所有开发人员都使用框架来启动开发并提供最好的关键基础设施 (不需要您编写的基础设施)。但是框架(包括商业的和开源的)所隐含的一个 危险就是:它们总是试图让您与其进行紧密耦合,这使得发现代码中隐藏的设计 变得更加困难。
框架和应用服务器都提供了 helper 类,诱使您实施一种更加简单的开发:如 果您仅仅是导入和使用它们的某些类,那么完成特定的任务将变得非常容易。一 个典型的例子就是 Struts,这是一种非常流行的开源 Web 框架。Struts 包括了 一组 helper 类来帮助您处理常见问题。例如,如果允许您的域类扩展 Struts ActionForm 类,那么 Struts 将自动从请求中填充表单字段,处理验证和生命周 期事件,并执行其他比较简单的行为。换而言之,Struts 提供了某种权衡:使用 我们的类将使您的开发工作变得非常轻松。它鼓励您创建一种类似于图 1 所示的 结构:
图 1. 使用 Struts ActionForm 类