第4章 交付用户想要的软件

没有任何计划在遇敌后还能继续执行。——Helmuth von Moltke(德国陆军元帅,1848—1916)

客户把需求交给你了,要你几年后交付这个系统。然后,你就基于这些需求构建客户需要的系统,最后按时交付。客户看到了软件,连声称赞做得好。从此你又多了一个忠实客户,接着你很开心地进入了下一个项目。你的项目通常都是这样运作的,是这样的吗?

其实,大部分人并不会遇到这样的项目。通常情况是:客户最后看到了软件,要么震惊要么不高兴。他们不喜欢所看到的软件,他们认为很多地方需要修改。他们要的功能不在他们给你的原始需求文档中。这听起来是不是更具代表性?

Helmuth von Moltke曾说过:“没有任何计划在遇敌后还能继续执行。”我们的敌人不是客户,不是用户,不是队友,也不是管理者。真正的敌人是变化。软件开发如战争,形势的变化快速而又剧烈。固守昨天的计划而无视环境的变化会带来灾难。你不可能“战胜”变化——无论它是设计、架构还是你对需求的理解。敏捷——成功的软件开发方法——取决于你识别和适应变化的能力。只有这样才有可能在预算之内及时完成开发,创建真正符合用户需求的系统。

在本章中,我们会介绍如何达到敏捷的目标。首先,要介绍为什么用户和客户参与开发如此重要,以及为什么让客户做决定(从第45页开始)。设计是软件开发的基础,没有它很难做好开发,但你也不能被它牵制。从第48页开始,将介绍如何让设计指导而不是操纵开发。说到牵制,你应确保在项目中引入合适的技术。你需要合理地使用技术(第52页介绍)。

为了让软件符合用户的需求,要一直做下面的准备工作。为了降低集成新代码带来的破坏性变化,你要提早集成,频繁集成(第58页)。当然,你不想破坏已有的代码,想让代码一直保持可以发布(从第55页开始)。

你不能一次又一次为用户演示新功能,而浪费宝贵的开发时间,因此你需要提早实现自动化部署(第61页)。只要你的代码一直可用,并且易于向用户部署,你就能使用演示获得频繁反馈(第64页)。这样你就能经常向全世界发布新版本。你想通过使用短迭代,增量发布来帮助经常发布新功能,与用户的需求变化联系更紧密(从第69页开始介绍它)。

最后,特别是客户要求预先签订固定价格合约时,很难通过敏捷的方法让客户与我们同坐一条船上。而且,事实上是固定的价格就意味着背叛承诺,我们会在第73页了解如何处理这种情况。

10 让客户做决定

“开发者兼具创新和智慧,最了解应用程序。因此,所有关键决定都应该由开发者定夺。每次业务人员介入的时候,都会弄得一团糟,他们无法理解我们做事的逻辑。”

在设计方面,做决定的时候必须有开发者参与。可是,在一个项目中,他们不应该做所有的决定,特别是业务方面的决定。

就拿项目经理Pat的例子来说吧。Pat的项目是远程开发,一切按计划且在预算内进行着——就像是个可以写入教科书的明星项目。Pat高高兴兴地把代码带到客户那里,给客户演示,却败兴而归。

原来,Pat的业务分析师没有和用户讨论,而是自作主张,决定了所有的问题。在整个开发过程中,企业主根本没有参与低级别的决策。项目离完成还早着呢,就已经不能满足用户的需要了。这个项目一定会延期,又成为一个经典的失败案例。

因而,你只有一个选择:要么现在就让用户做决定,要么现在就开始开发,迟些让用户决定,不过要付出较高的成本。如果你在开发阶段回避这些问题,就增加了风险,但是你要能越早解决这些问题,就越有可能避免繁重的重新设计和编码。甚至在接近项目最终期限的时候,也能避免与日俱增的时间压力。

例如,假设你要完成一个任务,有两种实现方式。第一种方式的实现比较快,但是对用户有一点限制。第二种方式实现起来需要更多的时间,但是可以提供更大的灵活性。很显然,你有时间的压力(什么项目没有时间压力呢),那么你就用第一种很快的方式吗?你凭什么做出这样的决定呢?是投硬币吗?你询问了同事或者你的项目经理吗?

作者之一Venkat最近的一个项目就遇到了类似的问题。项目经理为了节约时间,采取了第一种方式。也许你会猜到,在Beta版测试的时候,软件暴露出的局限让用户震惊,甚至愤怒。结果还得重做,花费了团队更多的金钱、时间和精力。

开发者(及项目经理)能做的一个最重要的决定就是:判断哪些是自己决定不了的,应该让企业主做决定。你不需要自己给业务上的关键问题做决定。毕竟,那不是你的事情。如果遇到了一个问题,会影响到系统的行为或者如何使用系统,把这个问题告诉业务负责人。如果项目领导或经理试图全权负责这些问题,要委婉地劝说他们,这些问题最好还是和真正的业务负责人或者客户商议(见习惯4,第23页)。

决定什么不该决定 Decide what you shouldn’t decide

当你和客户讨论问题的时候,准备好几种可选择的方案。不是从技术的角度,而是从业务的角度,介绍每种方案的优缺点,以及潜在的成本和利益。和他们讨论每个选择对时间和预算的影响,以及如何权衡。无论他们做出了什么决定,他们必须接受它,所以最好让他们了解一切之后再做这些决定。如果事后他们又想要其他的东西,可以公正地就成本和时间重新谈判。

毕竟,这是他们的决定。

让你的客户做决定。开发者、经理或者业务分析师不应该做业务方面的决定。用业务负责人能够理解的语言,向他们详细解释遇到的问题,并让他们做决定。

切身感受

业务应用需要开发者和业务负责人互相配合来开发。这种配合的感觉就应该像一种良好的、诚实的工作关系。

平衡的艺术

  • 记录客户做出的决定,并注明原因。好记性不如烂笔头。可以使用工程师的工作日记或日志、Wiki、邮件记录或者问题跟踪数据库。但是也要注意,你选择的记录方法不能太笨重或者太繁琐。
  • 不要用低级别和没有价值的问题打扰繁忙的业务人员。如果问题对他们的业务没有影响,就应该是没有价值的。
  • 不要随意假设低级别的问题不会影响他们的业务。如果能影响他们的业务,就是有价值的问题。
  • 如果业务负责人回答“我不知道”,这也是一个称心如意的答案。也许是他们还没有想到那么远,也许是他们只有看到运行的实物才能评估出结果。尽你所能为他们提供建议,实现代码的时候也要考虑可能出现的变化。

11 让设计指导而不是操纵开发

“设计文档应该尽可能详细,这样,低级的代码工人只要敲入代码就可以了。在高层方面,详细描述对象的关联关系;在低层方面,详细描述对象之间的交互。其中一定要包括方法的实现信息和参数的注释。也不要忘记给出类里面的所有字段。编写代码的时候,无论你发现了什么,绝不能偏离了设计文档。”

“设计”是软件开发过程不可缺少的步骤。它帮助你理解系统的细节,理解部件和子系统之间的关系,并且指导你的实现。一些成熟的方法论很强调设计,例如,统一过程(Unified Process,UP)十分重视和产品相关的文档。项目管理者和企业主常常为开发细节困扰,他们希望在开始编码之前,先有完整的设计和文档。毕竟,那也是你如何管理桥梁或建筑项目的,难道不是吗?

另一方面,敏捷方法建议你早在开发初期就开始编码。是否那就意味着没有设计呢?(1)不,绝对不是,好的设计仍然十分重要。画关键工作图(例如,用UML)是必不可少的,因为要使用类及其交互关系来描绘系统是如何组织的。在做设计的时候,你需要花时间去思考(讨论)各种不同选择的缺陷和益处,以及如何做权衡。

然后,下一步才考虑是否需要开始编码。如果你在前期没有考虑清楚这些问题,就草草地开始编码,很可能会被很多意料之外的问题搞晕。甚至在建筑工程方面也有类似的情况。在锯一根木头的时候,通常的做法就是先锯一块比需要稍微长一点的木块,最后细致地修整,直到它正好符合需求。

但是,即使之前已经提交了设计文档,也还会有一些意料之外的情况出现。时刻谨记,此阶段提出的设计只是基于你目前对需求的理解而已。一旦开始了编码,一切都会改变。设计及其代码实现会不停地发展和变化。

一些项目领导和经理认为设计应该尽可能地详细,这样就可以简单地交付给“代码工人们”。他们认为代码工人不需要做任何决定,只要简单地把设计转化成代码就可以了。就作者本人而言,没有一个愿意在这样的团队中做纯粹的打字员。我们猜想你也不愿意。

如果设计师们把自己的想法绘制成精美的文档,然后把它们扔给程序员去编码,那会发生什么(查阅习惯39,在第152页)?程序员会在压力下,完全按照设计或者图画的样子编码。如果系统和已有代码的现状表明接收到的设计不够理想,那该怎么办?太糟糕了!时间已经花费在设计上,没有工夫回头重新设计了。团队会死撑下去,用代码实现了明明知道是错误的设计。这听起来是不是很愚蠢?是够愚蠢的,但是有一些公司真的就是这样做的。

设计满足实现即可,不必过于详细 Design should be only as detailed as needed to implement

严格的需求—设计—代码—测试开发流程源于理想化的瀑布式(2)开发方法,它导致在前面进行了过度的设计。这样在项目的生命周期中,更新和维护这些详细的设计文档变成了主要工作,需要时间和资源方面的巨大投资,却只有很少的回报。我们本可以做更好。

设计可以分为两层:战略和战术。前期的设计属于战略,通常只有在没有深入理解需求的时候需要这样的设计。更确切地说,它应该只描述总体战略,不应深入到具体的细节。

做到精确

如果你自己都不清楚所谈论的东西,就根本不可能精确地描述它。——约翰·冯·诺依曼

前面刚说过,战略级别的设计不应该具体说明程序方法、参数、字段和对象交互精确顺序的细节。那应该留到战术设计阶段,它应该在项目开发的时候再具体展开。

良好的战略设计应该扮演地图的角色,指引你向正确的方向前进。任何设计仅是一个起跑点:它就像你的代码一样,在项目的生命周期中,会不停地进一步发展和提炼。

战略设计与战术设计 Strategic versus tactical design

下面的故事会给我们一些启发。在1804年,Lewis与Clark(3)进行了横穿美国的壮举,他们的“设计”就是穿越蛮荒。但是,他们不知道在穿越殖民地时会遇到什么样的问题。他们只知道自己的目标和制约条件,但是不知道旅途的细节。

软件项目中的设计也与此类似。在没有穿越殖民地的时候,你不可能知道会出现什么情况。所以,不要事先浪费时间规划如何徒步穿越河流,只有当你走到河岸边的时候,才能真正评估和规划如何穿越。只有到那时,你才开始真正的战术设计。

不要一开始就进行战术设计,它的重点是集中在单个的方法或数据类型上。这时,更适合讨论如何设计类的职责。因为这仍然是一个高层次、面向目标的设计。事实上,CRC(类—职责—协作)卡片的设计方法就是用来做这个事情的。每个类按照下面的术语描述。

  • 类名。
  • 职责:它应该做什么?
  • 协作者:要完成工作它要与其他什么对象一起工作?

如何知道一个设计是好的设计,或者正合适?代码很自然地为设计的好坏提供了最好的反馈。如果需求有了小的变化,它仍然容易去实现,那么它就是好的设计。而如果小的需求变化就带来一大批基础代码的破坏,那么设计就需要改进。

好设计是一张地图,它也会进化。设计指引你向正确的方向前进,它不是殖民地,它不应该标识具体的路线。你不要被设计(或者设计师)操纵。

切身感受

好的设计应该是正确的,而不是精确的。也就是说,它描述的一切必须是正确的,不应该涉及不确定或者可能会发生变化的细节。它是目标,不是具体的处方。

平衡的艺术

  • “不要在前期做大量的设计”并不是说不要设计。只是说在没有经过真正的代码验证之前,不要陷入太多的设计任务。当对设计一无所知的时候,投入编码也是一件危险的事。如果深入编码只是为了学习或创造原型,只要你随后能把这些代码扔掉,那也是一个不错的办法。
  • 即使初始的设计到后面不再管用,你仍需设计:设计行为是无价的。正如美国总统艾森豪威尔所说:“计划是没有价值的,但计划的过程是必不可少的(4)。”在设计过程中学习是有价值的,但设计本身也许没有太大的用处。
  • 白板、草图、便利贴都是非常好的设计工具。复杂的建模工具只会让你分散精力,而不是启发你的工作。

12 合理地使用技术

“你开始了一个新的项目,在你面前有一长串关于新技术和应用框架的列表。这些都是好东西,你真的需要使用列表中所有的技术。想一想,你的简历上将留下漂亮的一笔,用那些伟大的框架,你的新应用将具有极高技术含量。”

从前,作者之一Venkat的同事Lisa向他解释自己的提议:她打算使用EJB。Venkat表示对EJB有些顾虑,觉得它不适合那个特殊的项目。然后Lisa回答道:“我已经说服了我们经理,这是正确的技术路线,所以现在不要再扔‘炸弹’了。”这是一个典型的“简历驱动设计”的例子,之所以选择这个技术,是因为它很美,也许还能提高程序员的技能。但是,盲目地为项目选择技术框架,就好比是为了节省税款而生孩子,这是没有道理的。

盲目地为项目选择技术框架,就好比是为了少交税而生孩子 Blindly picking a framework is like having kids to save taxes

在考虑引入新技术或框架之前,先要把你需要解决的问题找出来。你的表述方式不同,会让结果有很大差异。如果你说“我们需要xyzzy技术,是因为……”,那么就不太靠谱。你应该这样说:“……太难了”或者是“……花的时间太长了”,或者类似的句子。找到了需要解决的问题,接下来就要考虑:

  • 这个技术框架真能解决这个问题吗?是的,也许这是显而易见的。但是,这个技术真能解决你面临的那个问题吗?或者,更尖锐一点说,你是如何评估这个技术的?是通过市场宣传还是道听途说?要确保它能解决你的问题,并没有任何的毒副作用。如果需要,先做一个小的原型。
  • 你将会被它拴住吗?一些技术是贼船,一旦你使用了它,就会被它套牢,再也不可能回头了。它缺乏可取消性(查阅[HT00]),当条件发生变化时,这可能对项目有致命打击。我们要考虑它是开放技术还是专利技术,如果是开放的技术,那又开放到什么程度?
  • 维护成本是多少?会不会随着时间的推移,它的维护成本会非常昂贵?毕竟,方案的花费不应该高于要解决的问题,否则就是一次失败的投资。我们听说,有个项目的合同是支持一个规则引擎,引擎一年的维护费用是5万美元,但是这个数据库只有30条规则。这也太贵了。

当你在考察一个框架(或者任何技术)的时候,也许会被它提供的各种功能吸引。接着,在验证是否使用这个框架的时候,你可能只会考虑已经发现的另外一些功能。但是,你真的需要这些功能吗?也许为了迎合你发现的功能,你正在为它们找问题。这很像站在结账处一时冲动而买些无用的小零碎(那也正是商场把那些小玩意儿放到那里的原因)。

不久前,Venkat遇到了一个项目。咨询师Brad把一个专有框架卖给了这个项目的管理者。在Venkat看来,这个框架本身也许还有点儿意思,但是它根本不适合这个项目。

尽管如此,管理者却坚决认为他们要使用它。Venkat非常礼貌地停手不干了。他不想成为绊脚石,阻碍他们的工作进度。一年之后项目还没有完成——他们花了好几个月的时间编写代码来维护这个框架,为了适应这个框架,他们还修改了自己的代码。

Andy有过相似的经历:他的客户想完全透明地利用开源,他们拥有“新技术大杂烩”,其中的东西太多,以至于无法让所有的部分协同工作。

如果你发现自己在做一些花哨的东西(比如从头创建自己的框架),那就醒醒吧,闻闻烟味有多大,马上该起火了。你的代码写得越少,需要维护的东西就越少。

不要开发你能下载到的东西 Don’t build what you can download

例如,如果你想开发自己的持久层框架,记住Ted Neward的评论:对象—关系的映射就是计算机科学的越南战场(5)。你可以把更多的时间和精力投入到应用的开发——领域或具体应用中。

根据需要选择技术。首先决定什么是你需要的,接着为这些具体的问题评估使用技术。对任何要使用的技术,多问一些挑剔的问题,并真实地作出回答。

切身感受

新技术就应该像是新的工具,可以帮助你更好地工作,它自己不应该成为你的工作。

平衡的艺术

  • 也许在项目中真正评估技术方案还为时太早。那就好。如果你在做系统原型并要演示给客户看,也许一个简单的散列表就可以代替数据库了。如果你还没有足够的经验,不要急于决定用什么技术。
  • 每一门技术都会有优点和缺点,无论它是开源的还是商业产品、框架、工具或者语言,一定要清楚它的利弊。
  • 不要开发那些你容易下载到的东西。虽然有时需要从最基础开发所有你需要的东西,但那是相当危险和昂贵的。

13 保持可以发布

“我们刚试用的时候发现了一个问题,你需要立即修复它。放下你手头的工作,去修复那个刚发现的问题,不需要经过正规的程序。不用告诉其他任何人——赶快让它工作就行了。”

这听起来似乎没什么问题。有一个关键修复的代码必须要提交到代码库。这只是一件小事,而且又很紧急,所以你就答应了。

修复工作成功地完成了。你提交了代码,继续回到以前那个高优先级的任务中。忽然一声尖叫。太晚了,你发现同事提交的代码和你的代码发生了冲突,现在你使得每个人都无法使用系统了。这将会花费很多精力(和时间)才能让系统重新回到可发布的状态。现在你有麻烦了。你必须告诉大家,你不能交付你承诺的修复代码了。而魔鬼在嘲笑:“哈哈哈!”

这时候,你的处境会很糟糕:系统无法发布了。你弄坏了系统,也许会带来更糟糕的后果。

1836年,当时的墨西哥总统安东尼奧·洛佩斯·德·圣安那将军,率领部队穿越得克萨斯州西部,追赶败退的萨姆·休斯顿将军。当圣安那的部队到达得克萨斯州东南方向的布法罗河岸的沼泽地带的时候,他命令自己的部队就地休息。传说中认为他是太过自信,甚至没有安排哨兵。就在那个傍晚,休斯顿发动了突然袭击,这时圣安那的部队已经来不及编队了。他们溃不成军,输掉了这场决定性的战争,从此永远改变了得克萨斯州的历史(6)。

任何时候只要你没有准备好,那就是敌人进攻你的最佳时机。好好想一想,你的项目进入不可发布状态的频率是多少?你的源代码服务器中的代码,是不是像圣安那在那个决定性的黄昏一样——没有进行编队,遇到紧急情况无法立即启动。

已提交的代码应该随时可以行动 Checked-in code is always ready for action

在团队里工作,修改一些东西的时候必须很谨慎。你要时刻警惕,每次改动都会影响系统的状态和整个团队的工作效率。在办公室的厨房里,你不能容忍任何人乱丢垃圾,为什么就可以容忍一些人给项目带来垃圾代码呢?

下面是一个简单的工作流程,可以防止你提交破坏系统的代码。

  • 在本地运行测试。先保证你完成的代码可以编译,并且能通过所有的单元测试。接着确保系统中的其他测试都可以通过。
  • 检出最新的代码。从版本控制系统中更新代码到最新的版本,再编译和运行测试。这样往往会发现让你吃惊的事情:其他人提交的新代码和你的代码发生了冲突。
  • 提交代码。现在是最新的代码了,并且通过了编译和测试,你可以提交它们了。

在做上面事情的时候,也许你会遇到这样一个问题——其他人提交了一些代码,但是没有通过编译或者测试。如果发生了这样的事情,要立即让他们知道,如果有需要,可以同时警告其他的同事。当然,最好的办法是,你有一个持续集成系统,可以自动集成并报告集成结果。

这听起来似乎有点恐怖,其实很简单。持续集成系统就是在后台不停地检出、构建和测试代码的应用。你可以自己使用脚本快速实现这样的方式,但如果你选择已有的免费、开源的解决方案,它们会提供更多的功能且更加稳定。有兴趣的话,可以看一看Martin Fowler的文章(7),或者是Mike Clark编著的图书《项目自动化之道》[Cla04]。

再深入一点,假设你得知即将进行的一次重大修改很可能会破坏系统,不要任其发生,应该认真地警告大家,在代码提交之前,找出可以避免破坏系统的方法。选择可以帮助你平滑地引入和转换这些修改的方法,从而在开发过程中,系统可以得到持续的测试和反馈。

虽然保持系统可发布非常重要,但不会总是那么容易,例如,修改了数据库的表结构、外部文件的格式,或者消息的格式。这些修改,通常会影响应用的大部分代码,甚至导致应用暂时不可用,直到大量的代码修改完成。尽管如此,你还是有办法减轻这样的痛苦。

为数据库的表结构、外部文件,甚至引用它的API提供版本支持,这样所有相关变化都可以进行测试。有了版本功能,所做的变化可以与其他代码基相隔离,所以应用的其他方面仍然可以继续开发和测试。

你也可以在版本控制系统中添加一个分支,专门处理这个问题(使用分支需要十分小心,不好的分支也许会给你带来更多的麻烦。详情可以查阅《版本控制之道——CVS》或《版本控制之道——Subversion》)。

保持你的项目时刻可以发布。保证你的系统随时可以编译、运行、测试并立即部署。

切身感受

你会觉得,不管什么时候,你的老板、董事长、质量保障人员、客户或者你的配偶来公司参观项目的时候,你都能很自信并毫不犹豫地给他们演示最新构建的软件。你的项目一直处于可以运行的稳定状态。

平衡的艺术

  • 有时候,做一些大的改动后,你无法花费太多的时间和精力去保证系统一直可以发布。如果总共需要一个月的时间才能保证它一周内可以发布,那就算了。但这只应该是例外,不能养成习惯。
  • 如果你不得不让系统长期不可以发布,那就做一个(代码和架构的)分支版本,你可以继续进行自己的实验,如果不行,还可以撤销,从头再来。千万不能让系统既不可以发布,又不可以撤销。

14 提早集成,频繁集成

“只要没有到开发的末尾阶段,就不要过早地浪费时间去想如何集成你的代码,至少也要等开发差不多的时候,才开始考虑它。毕竟,还没有完成开发,为什么要操心集成的事情呢!在项目的末尾,你有充裕的时间来集成代码。”

我们说过,敏捷的一个主要特点就是持续开发,而不是三天打鱼两天晒网似地工作。特别是在几个人一起开发同一个功能的时候,更应该频繁地集成代码。

很多开发者用一些美丽的借口,推迟集成的时间。有时,不过是为了多写一些代码,或者是另一个子系统还有很多的工作要完成。他们很容易就会这样想:“现在手头上的工作压力够大了,到最后我才能做更多的工作,才能考虑其他人代码。”经常会听到这样的借口:“我没有时间进行集成”或者“在我机器上设置集成环境太费事了,我现在不想做它”。

但是,在产品的开发过程中,集成是一个主要的风险区域。让你的子系统不停地增长,不去做系统集成,就等于一步一步把自己置于越来越大的风险中,世界没有了你仍然会转动,潜在的分歧会继续增加。相反,尽可能早地集成也更容易发现风险,这样风险及相关的代价就会相当低。而等的时间越长,你也就会越痛苦。

作者之一Venkat小时候生活在印度钦奈市(Chennai),经常赶火车去学校。像其他的大城市一样,印度的交通非常拥挤。他每次必须在车还没有停稳的时候,就跳上去或者跳下来。但,你不能从站的地方一下子跳上运行的火车,我们在物理课上学习过这种运动定律。而应该是,首先你要沿着火车行驶的方向跑,边跑边抓住火车上的扶手,然后跳入火车中。

软件集成就像这一样。如果你不断地独立开发,忽然有一天跳到集成这一步,千万不要为受到打击而吃惊。也许你自己在项目中就有这样的体会:每次到项目结束的时候都觉得非常不爽,大家需要日日夜夜地进行集成。

你能集成并且独立

集成和独立不是互相矛盾的,你可以一边进行集成,一边进行独立开发。

使用mock对象来隔离对象之间的依赖关系,这样在集成之前就可以先做测试,用一个mock对象模拟真实的对象(或者子系统)。就像是拍电影时在光线的掩饰下使用替身一样,mock对象就是真实对象的替身,它并不提供真实对象的功能,但是它更容易控制,能够模仿需要的行为,使测试更加简单。

你可以使用mock对象,编写独立的单元测试,而不需要立刻就集成和测试其他系统,只有当你自信它能工作的时候,才可以开始集成。

当你在公司昏天黑地地加班时,唯一的好处就是可以享受到免费的披萨。

独立开发和早期集成之间是具有张力的。当你独立开发时,会发现开发速度更快,生产率更高,你可以更有效地解决出现的问题(见第136页,习惯35)。但那并不意味着要你避免或延迟集成(见本页侧边栏)。你一般需要每天集成几次,最好不要2~3天才集成一次。

当早期就进行集成的时候,你会看到子系统之间的交互和影响,你就可以估算它们之间通信和共享的信息数据。你越早弄清楚这些问题,越早解决它们,工作量就越小。就好比是,刚开始有3个开发者,开发着5万行的代码,后来是5000个开发者进行3000万行代码的开发。相反,如果你推迟集成的时间,解决这些问题就会变得很难,需要大量和大范围地修改代码,会造成项目延期和一片混乱。

决不要做大爆炸式的集成 Never accept big-bang integration

提早集成,频繁集成。代码集成是主要的风险来源。要想规避这个风险,只有提早集成,持续而有规律地进行集成。

切身感受

如果你真正做对了,集成就不再会是一个繁重的任务。它只是编写代码周期中的一部分。集成时产生的问题,都会是小问题并且容易解决。

平衡的艺术

  • 成功的集成就意味着所有的单元测试不停地通过。正如医学界希波克拉底的誓言:首先,不要造成伤害。
  • 通常,每天要和团队其他的成员一起集成代码好几次,比如平均每天5~10次,甚至更多。但如果你每次修改一行代码就集成一次,那效用肯定会缩水。如果你发现自己的大部分时间都在集成,而不是写代码,那你一定是集成得过于频繁了。
  • 如果你集成得不够频繁(比如,你一天集成一次,一周一次,甚至更糟),也许就会发现整天在解决代码集成带来的问题,而不是在专心写代码。如果你集成的问题很大,那一定是做得不够频繁。
  • 对那些原型和实验代码,也许你想要独立开发,而不要想在集成上浪费时间。但是不能独立开发太长的时间。一旦你有了经验,就要快速地开始集成。

15 提早实现自动化部署

“没问题,可以手工安装产品,尤其是给质量保证人员安装。而且你不需要经常自己动手,他们都很擅长复制需要的所有文件。”

系统能在你的机器上运行,或者能在开发者和测试人员的机器上运行,当然很好。但是,它同时也需要能够部署在用户的机器上。如果系统能运行在开发服务器上,那很好,但是它同时也要运行在生产环境中。

这就意味着,你要能用一种可重复和可靠的方式,在目标机器上部署你的应用。不幸的是,大部分开发者只会在项目的尾期才开始考虑部署问题。结果经常出现部署失败,要么是少了依赖的组件,要么是少了一些图片,要么就是目录结构有误。

如果开发者改变了应用的目录结构,或者是在不同的应用之间创建和共享图片目录,很可能会导致安装过程失败。当这些变化在人们印象中还很深的时候,你可以快速地找到各种问题。但是几周或者几个月之后查找它们,特别是在给客户演示的时候,可就不是一件闹着玩的事情了。

如果现在你还是手工帮助质量保证人员安装应用,花一些时间,考虑如何将安装过程自动化。这样,只要用户需要,你就可以随时为他们安装系统。要提早实现它,这样让质量保证团队既可以测试应用,又可以测试安装过程[8]。如果还是手工安装应用,那么最后把应用部署到生产环境时会发生什么呢?就算公司给你加班费,你也不愿意为不同用户的机器或不同地点的服务器上一遍又一遍地安装应用。

质量保证人员应该测试部署过程 QA should test deployment

有了自动化部署系统后,在项目开发的整个过程中,会更容易适应互相依赖的变化。很可能你在安装系统的时候,会忘记添加需要的库或组件——在任意一台机器上运行自动化安装程序,你很快就会知道什么丢失了。如果因为缺少了一些组件或者库不兼容而导致安装失败,这些问题会很快浮现出来。

从第一天起就开始交付

一开始就进行全面部署,而不是等到项目的后期,这会有很多好处。事实上,有些项目在正式开发之前,就设置好了所有的安装环境。

在我们公司,要求大家为预期客户实现一个简单的功能演示——验证一个概念的可行性。即使项目还没有正式开始,我们就有了单元测试、持续集成和基于窗口的安装程序。这样,我们就可以更容易更简单地给用户交付这个演示系统:用户所要做的工作,就是从我们的网站上点击一个链接,然后就可以自己在各种不同的机器上安装这个演示系统了。

在签约之前,就能提供出如此强大的演示,这无疑证明了我们非常专业,具有强大的开发能力。

一开始就实现自动化部署应用。使用部署系统安装你的应用,在不同的机器上用不同的配置文件测试依赖的问题。质量保证人员要像测试应用一样测试部署。

切身感受

这些工作都应该是无形的。系统的安装或者部署应该简单、可靠及可重复。一切都很自然。

平衡的艺术

  • 一般产品在安装的时候,都需要有相应的软、硬件环境。比如,Java或Ruby的某个版本、外部数据库或者操作系统。这些环境的不同很可能会导致很多技术支持的电话。所以检查这些依赖关系,也是安装过程的一部分。
  • 在没有询问并征得用户的同意之前,安装程序绝对不能删除用户的数据。
  • 部署一个紧急修复的bug应该很简单,特别是在生产服务器的环境中。你知道这会发生,而且你不想在压力之下,在凌晨3点半,你还在手工部署系统。
  • 用户应该可以安全并且完整地卸载安装程序,特别是在质量保证人员的机器环境中。
  • 如果维护安装脚本变得很困难,那很可能是一个早期警告,预示着——很高的维护成本(或者不好的设计决策)。
  • 如果你打算把持续部署系统和产品CD或者DVD刻录机连接到一起,你就可以自动地为每个构建制作出一个完整且有标签的光盘。任何人想要最新的构建,只要从架子上拿最上面的一张光盘安装即可。

16 使用演示获得频繁反馈

“这不是你的过错,问题出在我们的客户——那些麻烦的最终客户和用户身上。他们不停地更改需求,导致我们严重地延期。他们一次就应该想清楚所有想要的东西,然后把这些需求给我们,这样我们才能开发出令他们满意的系统。这才是正确的工作方式。”

你时常会听到一些人想要冻结需求。但是,现实世界中的需求就像是流动着的油墨(9)。你无法冻结需求,正如你无法冻结市场、竞争、知识、进化或者成长一样。就算你真的冻结了,也很可能是冻结了错的东西。如果你期望用户在项目开始之前,就能给你可靠和明确的需求,那就大错特错了,赶快醒醒吧!

需求就像是流动着的油墨 Requirements are as fluid as ink

没有人的思想和观点可以及时冻结,特别是项目的客户。就算是他们已经告诉你想要的东西了,他们的期望和想法还是在不停地进化——特别是当他们在使用新系统的部分功能时,他们才开始意识到它的影响和可能发生的问题。这就是人的本性。

作为人类,不管是什么事情,我们都能越做越好,不过是以缓慢而逐步的方式。你的客户也一样。在给了你需求之后,他们会不停地研究这些功能,如何才能让它们变得更好使用。如果,你觉得自己要做的所有工作就是按照用户最初的需求,并实现了它们,但是在交付的时候,需求已经发生了变化,你的软件可能不会令他们满意。在软件开发过程中,你将自己置于最大的风险中:你生产出了他们曾经要求过的软件,但却不是他们现在真正想要的。那最后的结果就是:惊讶、震惊和失望,而不是满意。

几年前的一次数值分析课上,老师要求Venkat使用一些偏微分方程式模拟宇宙飞船的运行轨线。

程序基于时间t的坐标点,计算出在时间t+δ的位置。程序最后绘出来的轨线图就是如图4-1中的虚线。

图4-1 计算宇宙飞船的运行轨线图4-1 计算宇宙飞船的运行轨线

我们发现,估算出来的宇宙飞船位置远远地偏离了它的真实位置。万有引力不是只在我们计算的坐标点上才起作用。实际上,万有引力一直起作用:它是连续的,而不是离散的。由于忽略了点之间的作用力,我们的计算不断引入了误差,所以宇宙飞船最后到达了错误的地方。

缩小点之间的间隔(就是δ的值),再运行计算程序,误差就会减少。这时,估算的位置(如图4-1中的实线)就和实际位置很接近了。

同理,你的客户的期望就像宇宙飞船的实际位置。软件开发的成功就在于最后你离客户的期望有多近。你计算的每个精确位置,就是一个给客户演示目前已经完成功能的机会,也正是得到用户反馈的时候。在你动身进入下一段旅程的时候,这些反馈可以用来纠正你的方向。

我们经常看到,给客户演示所完成功能的时间与得到客户需求的时间间隔越长,那么你就会离最初需求越来越远。

应该定期地,每隔一段时间,例如一个迭代的结束,就与客户会晤,并且演示你已经完成的功能特性。

如果你能与客户频繁协商,根据他们的反馈开发,每个人都可以从中受益。客户会清楚你的工作进度。反过来,他们也会提炼需求,然后趁热反馈到你的团队中。这样,他们就会基于自己进化的期望和理解为你导航,你编写的程序也就越来越接近他们的真实需求。客户也会基于可用的预算和时间,根据你们真实的工作进度,排列任务的优先级。

较短的迭代周期,会对频繁的反馈有负面影响吗?在宇宙飞船轨线的程序中,当δ降低的时候,程序运行就要花费更长的时间。也许你会觉得,使用短的迭代周期会使工作变慢,延迟项目的交付。

让我们从这个角度思考:两年来一直拼命地开发项目,直到快结束的时候,你和你的客户才发现一个基础功能有问题,而且它是一个核心的需求。你以为缺货订单是这样处理的,但这完全不是客户所想的东西。现在,两年之后,你完成了这个系统,写下了数百万行的代码,却背离了客户的期望。再怎么说,两年来辛苦写出的代码有相当大部分要重写,代价是沉重的。

相反,如果你一边开发,一边向他们演示刚完成的功能。项目进展了两个月的时候,他们说:“等一下,缺货订单根本不是这么一回事。”于是,召开一个紧急会议:你重新审查需求,评估要做多大的改动。这时只要付很少的代价,就可以避免灾难了。

要频繁地获得反馈。如果你的迭代周期是一个季节或者一年(那就太长了),就应把周期缩短到一周或者两周。完成了一些功能和特征之后,去积极获得客户的反馈。

维护项目术语表

不一致的术语是导致需求误解的一个主要原因。企业喜欢用看似普遍浅显的词语来表达非常具体、深刻的意义。我经常看到这样的事情:团队中的程序员们,使用了和用户或者业务人员不同的术语,最后因为“阻抗失调”导致bug和设计错误。为了避免这类问题,需维护一份项目术语表。人们应该可以公开访问它,一般是在企业内部网或者Wiki上。这听起来似乎是一件小事情——只是一个术语列表及其定义。但是,它可以帮助你,确保你真正地和用户进行沟通。

在项目的开发过程中,从术语表中为程序结构——类、方法、模型、变量等选择合适的名字,并且要检查和确保这些定义一直符合用户的期望。清晰可见的开发。在开发的时候,要保持应用可见(而且客户心中也要了解)。每隔一周或者两周,邀请所有的客户,给他们演示最新完成的功能,积极获得他们的反馈。

切身感受

项目启动了一段时间之后,你应该进入一种舒适的状态,团队和客户建立了一种健康的富有创造性的关系。

突发事件应极少发生。客户应该能感觉到,他们可以在一定程度上控制项目的方向。

跟踪问题

随着项目的进展,你会得到很多反馈——修正、建议、变更要求、功能增强、bug修复等。要注意的信息很多。随机的邮件和潦草的告示帖是无法应付的。所以,要有一个跟踪系统记录所有这些日志,可能是用Web界面的系统。更多详情参阅Ship it![RG05]。

平衡的艺术

  • 当你第一次试图用这种方法和客户一起工作的时候,也许他们被这么多的发布吓到了。所以,要让他们知道,这些都是内部的发布(演示),是为了他们自己的利益,不需要发布给全部的最终用户。
  • 一些客户,也许会觉得没有时间应付每天、每周甚至是每两周的会议。毕竟,他们还有自己的全职工作。所以要尊重客户的时间。如果客户只可以接受一个月一次会议,那么就定一个月。
  • 一些客户的联络人的全职工作就是参加演示会议。他们巴不得每隔1小时就有一次演示和反馈。你会发现这么频繁的会议很难应付,而且还要开发代码让他们看。缩减次数,只有在你做完一些东西可以给他们演示的时候,大家才碰面。
  • 演示是用来让客户提出反馈的,有助于驾驭项目的方向。如果缺少功能或者稳定性的时候,不应该拿来演示,那只能让人生气。可以及早说明期望的功能:让客户知道,他们看到的是一个正在开发中的应用,而不是一个最终已经完成的产品。

17 使用短迭代,增量发布

“我们为后面的3年制定了漂亮的项目计划,列出了所有的任务和可交付的时间表。只要我们那时候发布了产品,就可以占领市场。”

统一过程和敏捷方法都使用迭代和增量开发(10)。使用增量开发一次开发应用功能的几个小组。每一轮的开发都是基于前一次的功能,增加为产品增值的新功能。这时,你就可以发布或者演示产品。

迭代开发是,在小且重复的周期里,你完成各种开发任务:分析、设计、实现、测试和获得反馈,所以叫作迭代。

迭代的结束就标记一个里程碑。这时,产品也许可用,也许不可用。在迭代结束时,新的功能全部完成,你就可以发布,让用户真正地使用,同时提供技术支持、培训和维护方面的资源。每次增加的新功能都会包含多次迭代。

根据Capers Jones的格言:“……大型系统的开发是一件非常危险的事情。”大型系统更容易失败。它们通常不遵守迭代和增量开发的计划,或者迭代时间太长(更多关于迭代和演进开发的讨论,以及和风险的关系、生产率和缺点,可以查阅Agile and Iterative Development:A Manager’s Guide[Lar04]一书)。Larman指出,软件开发不是精细的制造业,而是创新活动。规划几年之后客户才能真正使用的项目注定是行不通的。

给我一份详细的长期计划,我就会给你一个注定完蛋的项目 Show me a detailed long-term plan, and I’ll show you a project that’s doomed

对付大项目,最理想的办法就是小步前进,这也是敏捷方法的核心。大步跳跃大大地增加了风险,小步前进才可以帮助你很好地把握平衡。

在你周围,可以看到很多迭代和增量开发的例子。比如W3C(万维网联盟)提出的XML规范DTD(Document Type Definitions,文档类型定义),它用来定义XML文档的词汇和结构,作为原规范的部分发布。虽然在DTD设计的时候就解决了很多问题,但是在真正使用的时候,又显现出很多问题和限制。基于用户的反馈对规范就有了更深一层的理解,这样就诞生了更加高效的第二代解决方案,例如Schema。如果他们一开始就试图进行一些完美的设计,也许就看不到XML成为今天的主流了——我们通过提早发布获得了灼见和经验。

大部分用户都是希望现在就有一个够用的软件,而不是在一年之后得到一个超级好的软件(可以参见《程序员修炼之道——从小工到专家》“足够好的软件”一节[HT00])。确定使产品可用的核心功能,然后把它们放在生产环境中,越早交到用户的手里越好。

根据产品的特性,发布新的功能需要几周或者几个月的时间。如果是打算一年或者两年再交付,你就应该重新评估和重新计划。也许你要说,构建复杂的系统需要花费时间,你无法用增量的方式开发一个大型的系统。如果这种情况成立,就不要生产大的系统。可以把它分解成一块块有用的小系统——再进行增量开发。即使是美国国家航空航天局(NASA)也使用迭代和增量开发方式开发用于航天飞机的复杂软件(参见 Design,Development,Integration:Space Shuttle PrimaryFlight Software System[MR84])。

询问用户,哪些是使产品可用且不可缺少的核心功能。不要为所有可能需要的华丽功能而分心,不要沉迷于你的想象,去做那些华而不实的用户界面。

有一堆的理由,值得你尽快把软件交到用户手中:只要交到用户手里,你就有了收入,这样就有更好的理由继续为产品投资了。从用户那里得到的反馈,会让我们进一步理解什么是用户真正想要的,以及下一步该实现哪些功能。也许你会发现,一些过去认为重要的功能,现在已经不再重要了——我们都知道市场的变化有多快。尽快发布你的应用,迟了也许它就没有用了。

使用短迭代和增量开发,可以让开发者更加专注于自己的工作。如果别人告诉你有一年的时间来完成系统,你会觉得时间很长。如果目标很遥远,就很难让自己去专注于它。在这个快节奏的社会,我们都希望更快地得到结果,希望更快地见到有形的东西。这不一定是坏事,相反,它会是一件好事,只要把它转化成生产率和正面的反馈。

图4-2描述了敏捷项目主要的周期关系。根据项目的大小,理想的发布周期是几周到几个月。在每个增量开发周期里,应该使用短的迭代(不应该超过两周)。每个迭代都要有演示,选择可能提供反馈的用户,给他们每人一份最新的产品副本。

图4-2 嵌套敏捷开发周期图4-2 嵌套敏捷开发周期

增量开发。发布带有最小却可用功能块的产品。每个增量开发中,使用1~4周左右迭代周期。

切身感受

短迭代让人感觉非常专注且具效率。你能看到一个实际并且确切的目标。严格的最终期限迫使你做出一些艰难的决策,没有遗留下长期悬而未决的问题。

平衡的艺术

  • 关于迭代时间长短一直是一个有争议的问题。Andy曾经遇到这样一位客户:他们坚持认为迭代就是4周的时间,因为这是他们学到的。但他们的团队却因为这样的步伐而垂死挣扎,因为他们无法在开发新的代码的同时又要维护很多已经完成了的代码。解决方案是,在每4周的迭代中间安排一周的维护任务。没有规定说迭代必须要紧挨着下一个迭代。
  • 如果每个迭代的时间都不够用,要么是任务太大,要么是迭代的时间太短(这是平均数据,不要因为一次迭代的古怪情况而去调整迭代时间)。把握好自己的节奏。
  • 如果发布的功能背离了用户的需要,那么多半是因为迭代的周期太长了。用户的需要、技术和我们对需求的理解,都会随着时间的推移而变化,在项目发布的时候,需要清楚地反映出这些变化。如果你发现自己工作时还带有过时的观点和陈腐的想法,那么很可能你等待太长时间做调整了。
  • 增量的发布必须是可用的,并且能为用户提供价值。你怎么知道用户会觉得有价值呢?这当然要去问用户。

18 固定的价格就意味着背叛承诺

“对这个项目,我们必须要有固定的报价。虽然我们还不清楚项目的具体情况,但仍要有一个报价。到星期一,我需要整个团队的评估,并且我们必须要在年末交付整个项目。”

固定价格的合同会是敏捷团队的一大难题。我们一直在谈论如何用持续、迭代和增量的方式工作。但是现在却有些人跑过来,想提早知道它会花费多少时间及多少成本。

从客户方来看,这完全是理所应当的。客户觉得做软件就好比是盖一栋楼房,或者是铺设一个停车场,等等。为什么软件不能像建筑业等其他传统的行业一样呢?

也许它真的与建筑有很多相似之处——真正的建筑行业,但不是我们想象中的建筑业。根据英国1998年的一个研究,由于错误而返工的成本大约占整个项目成本的30%(11)。这不是因为客户的需求变化,也不是物理定律的变化,而是一些简单错误。比如,横梁太短,窗户洞太大,等等。这些都是简单并且为人熟悉的错误。

软件项目会遭遇各种各样的小错误,还要加上基础需求的变化(不,我要的不是一个工棚,而是一栋摩天大楼),不同个体和团队的能力差别非常巨大(20倍,甚至更多),当然,还不停地会有新技术出现(从现在开始,钉子就变成圆形的了)。

软件项目天生就是变化无常的,不可重复。如果要提前给出一个固定的价格,就几乎肯定不能遵守开发上的承诺。那么我们有什么可行的办法呢?我们能做更精确的评估吗?或者商量出另外一种约定。

固定的价格就是保证要背叛承诺 A fixed price guarantees a broken promise

根据自己的处境,选择不同的战略。如果你的客户一定要你预先确定项目的报价(比如政府合约),那么可能你需要研究一些重型的评估技术,比如COCOMO模型或者功能点分析法(Function Point analysis)。但它们不属于敏捷方法的范畴,并且使用它们也要付出代价。如果这个项目本质上和另一个项目十分相似,并且是同一个团队开发的,那么你就好办了:为一个用户开发的简单网站,与下一个会非常相似。

但是,很多项目并不像上面所说的那么如意。大部分项目都是业务应用,一个用户和另一个用户都有着巨大的差别。项目的发掘和创造需要很多配合工作。或许你可以提供稍有不同的安排,试试下面的办法。

  1. 主动提议先构建系统最初的、小的和有用的部分(用建筑来打个比方,就是先做一个车库)。挑选一系列小的功能,这样完成第一次交付应该不多于6~8周。向客户解释,这时候还不是要完成所有的功能,而是要足够一次交付,并能让用户真正使用。
  2. 第一个迭代结束时客户有两个选择:可以选择一系列新的功能,继续进入下一个迭代;或者可以取消合同,仅需支付第一个迭代的几周费用,他们要么把现在的成果扔掉,要么找其他的团队来完成它。
  3. 如果他们选择继续前进。那么这时候,应该就能很好地预测下一个迭代工作。在下一个迭代结束的时候,用户仍然有同样的选择机会:要么现在停止,要么继续下一个迭代。

对客户来说,这种方式的好处是项目不可能会死亡。他们可以很早地看到工作的进度(或者不足之处)。他们总是可以控制项目,可以随时停止项目,不需要缴纳任何的违约金。他们可以控制先完成哪些功能,并能精确地知道需要花费多少资金。总而言之,客户会承担更低的风险。而你所做的就是在进行迭代和增量开发。基于真实工作的评估。让团队和客户一起,真正地在当前项目中工作,做具体实际的评估。由客户控制他们要的功能和预算。

切身感受

你的评估数据会在整个项目中发生变化——它们不是固定的。但是,你会觉得自信心在不断增加,你会越来越清楚每个迭代可以完成的工作。随着时间的推移,你的评估能力会不断地提高。

平衡的艺术

  • 如果你对答案不满意,那么看看你是否可以改变问题。
  • 如果你是在一个基于计划的非敏捷环境中工作,那么要么考虑一个基于计划且非敏捷的开发方法,要么换一个不同的环境。
  • 如果你在完成第一个迭代开发之前,拒绝做任何评估,也许你会失去这个合同,让位于那些提供了评估的人,无论他们做了多么不切实际的承诺。
  • 敏捷不是意味着“开始编码,我们最终会知道何时可以完成”。你仍然需要根据当前的知识和猜想,做一个大致的评估,解释如何才能到达这个目标,并给出误差范围。
  • 如果你现在别无选择,你不得不提供一个固定的价格,那么你需要学到真正好的评估技巧。
  • 也许你会考虑在合同中确定每个迭代的固定价格,但迭代的数量是可以商量的,它可以根据当前的工作状况进行调整[又名工作条款说明(Statement of Work)]。

【注释】

  1. 查阅Martin Fowler的文章Is Design Dead**?**(http://www.martinfowler.com/articles/designDead.html),它是对本主题深入讨论的一篇好文章。
  2. 瀑布式开发方法意味着要遵循一系列有序的开发步骤,前面是定义详细的需求,然后是详细的设计,接着是实现,再接着是集成,最后是测试(此时你需要向天祈祷)。那不是作者首先推荐的做法。更多详情可以查阅[Roy70]。
  3. 这世界真小,Andy还是William Clark的远亲呢。
  4. 1957年的演讲稿。
  5. Ted Neward曾写过The Vietnam of Computer Science著名文章,逐一探讨了对象—关系映射的缺点。——编者注
  6. http://www.sanjacinto-museum.org/The_Battle/April_21st_1836。
  7. http://www.martinfowler.com/articles/continuousIntegration.html。
  8. 确保他们能提前告诉你运行的软件版本,避免出现混乱。
  9. Edward V. Berard曾经指出:“如果需求能被冻结,那么开发软件就如在冻冰上走路一样简单。”
  10. 但是,所有减肥方案都会建议你应该少吃多做运动。然而,每份关于如何达到目标的计划都会不尽相同。
  11. Rethinking Construction: The Report of the Construction Task Force,1998年1月1日,英国副首相办公室地方政府和地区运输部文件。
下一节:一步行动,胜过千万专家的意见。——Bill Nye,The Science Guy科普节目主持人