现状
eMAN是客户的一个核心业务平台。该产品采用了典型的C/S结构,负责处理大量请求和计算的后台部分采用C++开发,负责响应用户操作和处理业务逻辑的前台部分采用Java开发;此外该产品还计划在新版本中提供基于Web的前台,这部分也采用Java开发。
在ThoughtWorks为该产品的开发团队提供咨询时,eMAN产品已经发布了十多个版本,最新版本代码量超过40万行,其中15万行是Java代码。一次又一次的赶工给它留下了大量的“技术债”:系统缺乏测试,代码质量低劣,“copy & paste”的痕迹比比皆是,维护和新功能开发举步维艰。我们这个咨询项目的主要目标之一就是为这个产品找出重构的办法。
原则
可以用两种不同的角度来看待一个软件:程序员的角度,商业的角度。
从程序员的角度看来,“成功的软件”意味着所有测试都通过、代码结构良好、并且容易理解和维护。从商业的角度看来,“成功的软件”意味着它所创造的价值超出在它身上付出的代价。
和别的任何story一样,重构的story(以及其他“技术债”类型的story)也应该符合INVEST的标准。尤其是,它们的工作量应该得到估算,它们应该按照业务价值排列优先级。因为归根到底,重构(以及其他任何开发任务)都归结为“花在代码上的成本”与“对业务创造的价值”之间的权衡。
按照定义,重构意味着“在不改变功能性行为的前提下改进代码的组织结构”。如果代码基础本身脆弱而没有测试覆盖,重构的成本就会很高,因为你需要花很大力气来确认自己的修改没有改变功能性行为。
如果偿还“技术债”的成本非常高,那么与之对应的业务价值就必须更高,否则偿还这些债务就将得不偿失。其结果是:一段代码,从程序员的角度看来越糟糕,从商业的角度来说就越不应该去动它。
综上所述,如果有人说“这一大堆代码都需要重构”,这样的说法很有可能是值得商榷的。你需要把重构划分成细粒度的、可控的story,为这些重构story制定验收标准,评估它们的优先级,估算它们的工作量,然后逐一实现它们,并且放弃一些得不偿失的重构。
在eMAN项目中,我们按照软件功能模块划分了story,而不区分是新功能开发还是重构。比如说一个典型的story可能是:
作为系统管理员我要从服务器列表中选择一个服务器从而让我可以登录到选中的服务器。
不管是新开发还是重构,这个story的验收条件是一致的:功能通过验收测试,代码符合质量要求。在实际工作中我们发现,对于同样的story,新开发和重构的工作量差别不大。这也使得迭代计划可以在story的基础上照常进行,不必特意区分重构和新功能开发。
持续集成
前面已经提到,重构story也同样应该是可验收的。除了确保其功能性行为仍然保持原样不变之外,这类story还有更多的验收条件:单元测试覆盖率、代码复杂度、编码规范等指标都应该符合项目要求。这些指标也同样适用于新功能开发的story。这些指标的报告应该自动化地生成,及时地展现给所有项目成员,为此我们需要一个持续集成环境。
eMAN项目采用CruiseControl作为持续集成工具。每次开发者往代码库中签入(check in)代码时,就会触发CruiseControl对项目进行构建,构建的内容包括编译、连接(对于后台部分)、单元测试、测试覆盖率统计、代码复杂度统计、编码规范检查、部署到测试环境、功能测试等。由于前台运行在Windows上而后台运行在Linux上,我们用两台持续集成服务器分别构建前台与后台,然后再把两者集成起来进行功能测试。
在建立持续集成环境的过程中,我们发现eMAN项目以前缺乏有效的项目自动化(automation)机制。虽然前后台分别有一些脚本用于执行编译、部署、启动应用等常规任务,但项目自动化机制还有较大的欠缺:
缺乏版本控制。原来的版本控制库中只有项目源代码和运行时配置文件,开发阶段的配置和自动化脚本都不在版本控制中。
环境依赖。原来的自动化脚本对操作系统、安装的软件环境甚至项目路径等因素都有依赖,每个开发者从版本控制库获取代码之后还需要复杂的配置才能让系统在本地运行。
自动化不彻底。原来的自动化脚本没有覆盖到构建的所有环节,并且各个环节之间没有连通,开发者必须执行多个步骤的操作才能完成构建。
在这样的自动化脚本基础上,持续集成环境无法发挥出它最大的价值,因此我们对自动化脚本做了一系列改进,达到的效果是:只要在一台干净的机器上安装Java和Ant,然后从版本控制库签出(check out)项目,在项目目录里执行ant即可完成整个构建。于是完整的构建不仅在持续集成服务器上频繁运行,还在每个开发者的工作机器上更加频繁地运行。
在这个过程中我们用到的工具(前台部分)包括:
持续集成服务器:CruiseControl
项目自动化工具:Ant
测试覆盖率和代码复杂度统计工具:Cobertura
代码风格监测工具:Checkstyle
安全网
正如前文提到的,对于重构性质的story,测试覆盖率是一项重要的验收条件。这是由重构任务的特性决定的。
重构(名词):对软件内部结构的一种调整,目的是在不改变“软件之可观察行为”的前提下,提高其可理解性,降低其修改成本。
重构之前,首先检查自己是否有一套可靠的测试机制。
——Martin Fowler,《重构》
没有一套可靠的测试机制,重构就无从谈起,因为你根本就无从知道自己做的调整是否改变了“软件之可观察行为”,甚至可能已经搞得系统不能运行还一无所知。而eMAN的现状正是如此:验收测试无法自动运行,单元测试更是在上一个版本交付之后就再也没有运行过。简而言之,eMAN目前没有测试。
对这种没有测试的系统进行重构,就像是编织一张网:先针对一小块功能编写验收测试,在这张“粗网”的保护下再逐渐给代码添加单元测试,有了粗细两层网的保护再深入重构。随着重构的开展,这张频繁自动化运行的安全网也渐渐铺开。从不断提升的测试覆盖率和不断降低的代码复杂度,我们就能清晰地看到重构的进展情况。
为什么要首先编写验收测试?当然了,如果代码本身结构良好,单元(类、方法)之间关系清晰,你也可以直接添加单元测试——但这样的代码基础就不需要大动干戈地专门组织重构了。我们在eMAN项目中发现,那些最需要重构的代码也是最难进行单元测试的,而没有测试我们又不敢动手重构。(在“典型案例”一节我们将介绍几种阻碍单元测试的常见情况和解决办法。)这时验收测试就可以在系统外围担任“看门人”,给我们一个起点:在调整代码结构以便单元测试时,我们至少知道这些调整没有破坏系统的功能。
在eMAN项目中,我们用Rational Functional Tester(RFT)来做验收测试。我们还评估了另一种针对Swing应用的功能测试工具Abbot。相比之下,RFT最大的优势在于独立性:测试工具与被测应用在不同的Java虚拟机里运行;而Abbot则是在当前虚拟机环境下运行被测应用,如果被测应用与Abbot引用同样的第三方包,就可能出现版本冲突。但RFT也有一些明显的劣势:测试案例编写难度大,占用系统资源多,与Ant集成不佳,而且价格不菲。读者应该根据自己项目的情况谨慎选择。
除了用RFT实现前台Swing应用的验收测试之外,我们还用Selenium实现了前台Web应用的验收测试。对Abbot的研究也没有浪费,我们用它来实现了Swing界面的单元测试。从理论上来说,任何一段代码都可以并且应该被测试,但适当的工具能让测试事半功倍。组合多种测试工具,从不同层面、不同角度对系统进行测试,才能织起一张可靠的安全网。
与通常说的“测试驱动开发”(TDD)相比,这种重构项目的节奏略有些不同:不是标准的“红-绿-重构”,而经常是“绿-重构-绿”。不过,这两种节奏都是敏捷项目中很常见的,下面的图就同时包含了两者。
值得一提的是,图中的弧线不仅代表开发中的一项活动、系统状态的一次变迁,而且还代表一次在结对中转移键盘的机会。在eMAN项目中,我们经常以这样的方式工作:一个人给现有代码补上一段测试,把键盘推给身边的同伴,后者重构被刚才的测试覆盖的那段代码。以这样的节奏稳步前进,确保了知识能够在结对的过程中得到传递。
我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。
我原创,你原创,我们的内容世界才会更加精彩!
【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】
微信公众号
TechTarget
官方微博
TechTarget中国
作者
相关推荐
-
多云工作负载迁移:自动化是何作用?
云计算正在发展进入一个崭新的、更成熟的阶段。云规划和部署的关注点已经从低效应用的远程托管转至对云的支持,并将其作为开发人员所使用的虚拟应用平台。
-
Puppet Enterprise为自动化提供陈述性解决方案
Puppet令组织科做出快速、可重复的变更,同时还可以自动确保云端或本地跨物理机与虚拟机(VM)的系统和设备的一致性。
-
防止重构问题最佳方法
在我们的“Ask The Experts”会话中,Brad Irby回答了这一问题:如何重构问题未发生之前防止它?
-
持续集成:敏捷最佳实践
持续交付方法开始于专注敏捷开发和完善持续集成实践。让我们谈谈持续集成,是“持续集成”,而不是“持续交付”。