敏捷软件开发:原则、模式与实践(C#版.修订版)

978-7-115-29468-5
作者: 【美】Robert C. Martin Micah Martin
译者: 邓辉孙鸣
编辑: 杨海玲
分类: C#

图书目录:

详情

本书深入而生动地使用真实案例讲解了面向对象基本原则、重要的设计模式、UML和敏捷实践等程序员必备的知识,书中还提供了极具价值的可重用的C#源代码,重点讲述了如何使用UML和设计模式解决面向客户系统的问题。

图书摘要

敏捷软件开发宣言

我们正在通过亲身实践以及帮助他人实践,揭示更好的软件开发方法。通过这项工作,我们认为:

人和交互  重于  过程和工具

可以工作的软件  重于  面面俱到的文档

客户合作  重于  合同谈判

随时应对变化  重于  遵循计划

虽然右项也有其价值,但我们认为左项更加重要。

Kent Beck         James Grenning      Robert C.Martin

Mike Beedle        Jim Highsmith      Steve Mellor

Arie van Bennekum      Andrew Hunt      Ken Schwaber

Alistair Cockburn      Ron Jeffries       Jeff Sutherland

Ward Cunningham      Jon Kern        Dave Thomas

Martin Fowler        Brian Marick

敏捷宣言遵循的原则

我们遵循以下原则。

我们最优先要做的是通过尽早地、持续地交付有价值的软件来满足客户需要。

我们欢迎需求的变化,即使到了开发后期。敏捷过程能够驾驭变化,为客户创造竞争优势。

经常交付可以工作的软件,从几个星期到几个月,时间间隔越短越好。

在整个项目开发期间,业务人员和开发人员必须朝夕工作在一起。

依靠斗志高昂的人构建项目。给他们提供所需的环境和支持,并且信任他们能够完成任务。

在团队内部,最有效率也最有效果的信息传达方式,就是面对面的交谈。

可以工作的软件是进度主要的度量标准。

敏捷过程提倡可持续开发。出资人、开发者和用户应该总是保持稳定的开发速度。

对卓越技术和良好设计的不断追求有助于提高敏捷性。

简单——尽量减少工作量的艺术是至关重要的。

最好的构架、需求和设计都源自自我组织的团队。

每隔一定时间,团队都要总结如何更有效率,然后相应地调整自己的行为。

软件开发方法学精选系列

Agile Principles,Patterns,and Practices in C#

敏捷软件开发:原则、模式与实践(C#版·修订版)

[美]Robert C.Martin Micah Martin 著

邓辉 孙鸣 译

人民邮电出版社

北京

图书在版编目(CIP)数据

敏捷软件开发:原则、模式与实践:C#版/(美)马丁(Martin,R.C.),(美)马丁(Martin,M.)著;邓辉,孙铭译.--修订本.--北京:人民邮电出版社,2013.1

(软件开发方法学精选系列)

书名原文:Agile Principles,Patterns,and Practices in C﹟

ISBN 978-7-115-29468-5

Ⅰ.①敏… Ⅱ.①马…②马…③邓…④孙… Ⅲ.①软件开发②C语言—程序设计 Ⅳ.①TP311.52②TP312

中国版本图书馆CIP数据核字(2012)第220634号

内容提要

享誉全球的面向对象技术大师Robert C.Martin在本书中深入而生动地使用真实案例讲解了面向对象设计的基本原则、重要的设计模式、UML和敏捷方法。

本书Java版曾荣获2003年第13届Jolt大奖,是公认的典著作。本书是C#程序员提升功力的绝佳教程,也可用作高校计算机、软件工程专业本科生、研究生的教材或参考书。

软件开发方法学精选系列

敏捷软件开发:原则、模式与实践(C#版·修订版)

◆著 [美]Robert C.Martin Micah Martin

译 邓辉 孙鸣

责任编辑 杨海玲

◆人民邮电出版社出版发行  北京市崇文区夕照寺街14号

邮编 100061  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京鑫正大印刷有限公司印刷

◆开本:800×1000 1/16

印张:35  2013年1月第1版

字数:936千字  2013年1月北京第1次印刷

著作权合同登记号 图字:01-2007-1482号

ISBN 978-7-115-29468-5

定价:79.00元

读者服务热线:(010)67132692 印装质量热线:(010)67129223

反盗版热线:(010)67171154

广告经营许可证:京崇工商广字第0021号

版权声明

Authorized translation from the English language edition entitled Agile Principles,Patterns,and Practices in C#,1st Edition,0131857258 by Robert C.Martin and Micah Martin,published by Pearson Education,Inc,publishing as Prentice Hall,Copyright © 2007 Pearson Education,Inc.

All rights reserved.No part of this book may be reproduced or transmitted in any form or by any means,electronic or mechanical,including photocopying,recording,or by any information storage retrieval system,without permission from Pearson Education,Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD.,and POSTS & TELECOMMUNICATIONS PRESS Copyright © 2012.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。

译者序

2002年10月,“Bob大叔”(Robert C.Martin)终于推出了软件开发社团期待已久的Agile Software Development,Principles,Patterns,and Practices一书。该书以真实案例为基础,通过真实开发场景再现的方式对软件开发中涉及的各种知识及其有效的运用方法进行了讲解。这种做法得到了广大软件从业人员的一致认可。该书一出版就好评如潮,并毫无争议地获得了第 13 届软件开发图书类的 Jolt大奖。

次年,“Bob大叔”又推出了另外一本书UML for Java Programmers。该书秉承了上一本书的讲解风格,不过其重点在于UML。“Bob大叔”在书中介绍了一些常用的UML特性;更为重要的是,他把重点放在了如何在真实的项目开发中,以注重实效的态度来使用 UML。对于那些习惯于动辄画数十页精美的 UML 图,并把这些 UML 图当成真正的软件设计的架构师们来说,该书无疑是对他们的当头棒喝。

应该说,这两本书中所教授的内容和思维方法是与具体编程语言无关的,但是许多软件开发人员还是很希望这些知识能够基于自己特定的语言和开发平台进行讲解。作为一名资深的软件咨询大师,“Bob大叔”当然很清楚这一点。为了让.NET开发社团也能够像Java社团那样,学习到这些可以改善软件开发状况,并让程序员感受到开发乐趣的敏捷开发和敏捷设计的权威知识,“Bob大叔”于2006年7月推出了一本新书Agile Principles,Patterns,and Practices in C#,也就是读者正在阅读的这本书。按照“Bob大叔”的说法,这本书是他前两本书的合订本。

在本书中,“Bob 大叔”去除了前两本书中的重复内容,并把它们有机地融合在一起。此外,对于书中的案例也做了相应的调整,去掉了不为大多数开发人员熟悉的气象站案例以及略显仓促的ETS案例。“Bob 大叔”对具有典型代表性的薪水支付应用案例进行了增强,使其贯穿全书,并增加了关于数据库和MVP模式两章内容使得该案例更加完整。这种做法使得本书读起来更加顺畅。读者花一本书的价格买到“Bob大叔”的两本经典著作,从某种意义上来讲可以看作是“Bob大叔”对.NET社团作出的补偿。

能够在4年后再次翻译“Bob大叔”的这本新书,对我个人而言,既是一种荣幸,同时也是对这几年敏捷开发实践的一次难得的反思、总结以及再学习的机会。曾经有读者认为本书中讲的东西太多、太杂,很多内容完全可以独立成书,放在一起显得比较散乱。我不认同这种观点。敏捷开发的核心就是以最低的成本,最快速地为客户提供价值。书中所讲述的过程方法、实践、设计原则、模式以及思考方式看似独立,其实都围绕在这个核心周围,并以相互支援的方式为达成这个核心目标服务。为了能够快速提供价值,我们应用短迭代、快速交付的开发方法;为了保证这些价值是客户真正需要的,我们和客户紧密合作并应用反馈驱动的方法;为了能够降低软件的演化和维护成本,我们应用好的设计原则和模式;为了降低设计成本,我们采用测试驱动、随时重构、演化设计的方法……如果能够以这个核心为主线去理解和学习书中教授的内容,效果应该会更好一些。我这几年的实践经历也证实了这一点。

软件开发应该是一项充满快乐和激情的工作,衷心希望本书能够帮助国内.NET社团的程序员朋友体会到这种快乐和激情。

邓辉

Chris Sells序[1]

在我的职业编程生涯中,所做的第一份工作是为一个bug数据库增加功能。这些功能主要是为明尼苏达大学农场校区的植物病理系服务的,因此这里的“bug”指的是真正的bug,比如蚜虫、蝗虫以及毛虫。数据库的代码原来是由一位昆虫学家编写的,他所掌握的dBase知识仅仅能够编写出一种类型的表格,然后就在应用的其余部分到处复制。在我增加功能时,我把功能尽可能地集中在一起,这样就可以在一个地方修正代码的 bug,并且也可以在一个地方增加功能。这项工作花费了我整整一个夏天,最终的功能是原来的两倍,但是代码规模却只有原来的一半。

许多年后,我和我的一位朋友因手边没有什么急迫的工作要做,所以决定一起编一些程序(编写的要么是IDispatch,要么是IMoniker[2],当时我们认为这两个东西都很重要)。我先编写一会儿代码,而他在边上观看,并告诉我哪里写错了。接着,他掌控键盘,而我在旁边提建议,然后他把键盘的控制权又交给我。就这样持续了几个小时,这是我最为满意的一段编码经历。

之后不久,我的朋友就聘用我来担任他的公司新成立的软件部门的首席架构师。作为架构工作的一部分,我经常会为一些还没有存在的对象(我假想它们已经存在)编写客户代码,并把代码移交给工程师,由他们来继续实现直到客户程序能够工作。

我猜我对敏捷开发方法各个方面的实践体验并非个例。总的来说,我在敏捷方法(比如重构、结对编程以及测试驱动开发)方面的实践是成功的,虽然我对自己所做的还没有非常清楚的认识。当然,在这之前我是可以获取一些敏捷开发方面的资料的,但是正如我不愿意从《国家地理杂志》的过期刊物中学习如何邀请女孩跳舞一样,我更希望敏捷技术能够适合于我的特定情况,也就是.NET。和那些费尽心思去学习学生中间的流行语的中学老师一样,Robert 使用.NET(即使他很清楚地指出,.NET在很多方面并不比Java优秀)在讲述我使用的语言,但他知道内容本身要比传达介质更重要。

除了.NET外,我还喜欢在尝试新东西时,能够循序渐进、逐步深入,不至于感到恐惧,但是又可以真正理解所有重要的东西。而这正是Bob大叔(Robert Martin)在本书中所做的。他的介绍性章节讲述了敏捷运动的基础知识,但没有急于向读者提及SCRUM、极限编程以及任何其他的敏捷方法,从而使读者能够以一种自己喜欢的方式来逐步进行理解。更好的是(也是Robert写作风格中我最喜欢的部分),他是通过实践来展示这些技术的:提出一个问题,分析它,就像发生在真实环境中一样;展现出错误和失策的地方以及如何通过应用他所主张的技术来解决这些问题。

我不知道Robert在本书中描述的情况在现实中是否存在。我只是在自己的经历中曾经隐约有过这样的体验。但是,可以肯定的是,所有比较“酷”的年轻人都在这样做。请把Bob大叔当作自己在敏捷世界中的优秀导师,他的唯一目标就是当你想去体验时,能够让你做好,并且保证每个人都享受其中的乐趣。

[1].Chris Sells是世界知名的.NET技术专家。曾被微软授予“软件传奇人物”(Software Legend)称号。代表著作有《Windows Forms程序设计》(人民邮电出版社,2004)等。——编者注

[2].IDispatch和IMoniker都是微软COM模型中的接口名。——编者注

Erich Gamma序[1]

写这篇序时,我刚刚交付了 Eclipse 开源项目的一个主要版本。我仍然处在恢复阶段,思维还有些模糊。但是有一件事情我却比以往更加清楚,那就是:交付产品的关键因素是人,而不是过程。我们成功的诀窍很简单:和那些全心致力于交付软件的人一起工作,使用适合于自己团队的轻量过程进行开发,并且不断调整。

看看我们团队中的开发人员,他们都将编程视为开发活动的中心。他们不仅编写代码,还努力参悟代码,以保持对系统的理解。使用代码验证设计,从中得到的反馈对于增强设计的信心至关重要。同时,我们的开发人员理解模式、重构、测试、增量交付、频繁构建和其他一些XP(极限编程)最佳实践的重要性。这些实践改变了我们对开发方法的认识。

对于那些具有高技术风险以及需求经常变化的项目来说,熟练地掌握这种开发方式是取得成功的先决条件。虽然敏捷开发不注重形式和文档,但是非常强调日常开发实践。让这些实践付诸实施,正是本书的中心内容。

Robert是面向对象社区的一位活跃分子,对于C++开发、设计模式以及面向对象设计的一般原则贡献颇多,同时他很早就是一位XP和敏捷方法的积极提倡者。本书就以他的众多贡献为基础,全面讲述了敏捷开发实践。这真是一项了不起的成就。不仅如此,Robert在说明每个问题时,还使用了案例和大量的代码,这与敏捷实践完全相符。他实际上是在通过实际编程来阐述编程和设计。

本书中充满了对于软件开发的真知灼见。不管你是想成为一位敏捷(agile)开发人员,还是想进一步提高自己的技能,它都同样有用。我对本书期盼已久,它没有令我失望。

[1].Erich Gamma是IBM公司杰出工程师,面向对象技术大师,《设计模式》一书的第一作者。他与Kent Beck合作开发了测试框架JUnit。在IBM,他领导了Eclipse平台的开发。此外,他还领导团队协助开发了平台项目Jazz。——编者注

前言

可是Bob,你说过去年就能写完这本书的。

——Claudia Frers,1999年UML World大会

Bob的引言

离Claudia说出这句合情合理的抱怨已经7年了,不过我觉得我已经做出了补偿。在这几年里,我出版了3本书,对于一个同时经营着一家咨询公司,并且还得进行大量的代码编写、培训、指导、演讲的工作,以及撰写文章、专栏和博客的人来讲,要每隔一年出一本书是一项很大的挑战,更不要说还得养活并陪伴一个大家庭了。但是,我喜欢这样。

敏捷开发(Agile Development)就是指能够在需求迅速变化的情况下快速开发软件。为了达到这种敏捷性,我们需要使用一些实践提供必要的准则和反馈,需要使用一些设计原则使我们的软件保持灵活且可维护,还需要理解一些已经被证明在特定问题中可以权衡这些原则的设计模式。本书试图将这3个概念融汇起来,使它们成为有机的整体。

本书首先描述了这些原则、模式以及实践,然后通过许多案例来演示如何应用它们。更重要的是,案例给出的并不是最终的结果,而是设计的过程。你会看到设计者犯错误;你会看到他们如何找到错误并最终改正;你会看到他们对问题苦思冥想,面对一些难以权衡的含糊问题的疑惑与探索。是的,你会看到设计的真正历程。

Micah的引言

2005年初,我参与到一个小的开发团队中,该团队正准备用C#开发一个.NET应用程序。使用敏捷开发实践是团队的强制性规定,这也是我参与其中的原因之一。虽然我以前曾经使用过C#,但是我的大部分编程经验都是基于Java和C++的。我认为.NET不会有什么不同,结果也表明确实如此。

项目开始两个月后,我们进行了第一次发布。这是一次部分发布,其中只包含了所有计划特性中的一部分,但却是完全可用的,并且也确实被投入使用。仅仅两个月公司就得到了我们开发的软件带来的好处。管理层非常兴奋,他们要求雇佣更多的人,这样就可以启动更多的项目。

我投身到敏捷社区已经有几年了,我认识很多可以帮助我们的敏捷开发者。我通知了他们每一个人,请求他们加入到我们中来。结果,没有一个敏捷朋友加入我们的团队。为什么?也许最主要的原因是我们是基于.NET进行开发的。

几乎所有敏捷开发者都具有Java、C++或者Smalltalk方面的背景。几乎从来没有听说过有敏捷.NET程序员。也许,当我说我们正在使用.NET进行敏捷软件开发时,我的那些朋友根本就没当回事,也许他们想避免和.NET有什么瓜葛。这是一个严重的问题。我已经不止一次看到这种情况了。

我讲过许多为期一周的关于各种软件主题的课程,有机会见到来自世界各地的具有广泛代表性的开发者。我曾经指导过的很多学生都是.NET程序员,也有很多是Java和C++程序员。恕我直言:在我的经历中,.NET程序员常常要比Java和C++程序员差一些。当然,也并非总是如此。但是,通过在课堂中的再三观察,我只能得出这样的结论:在敏捷软件实践、设计模式、设计原则等方面,.NET程序员往往要弱一些。在我的课堂上,.NET程序员常常从来没有听说过这些基本概念。必须改变这种情况。

本书的另一版本,由我父亲Robert C.Martin撰写的Agile Software Development:Principles,Patterns,and Practices在2002年末出版,并赢得了2003年的Jolt大奖。那是一本很好的书,得到了许多开发者的赞扬。遗憾的是,它对.NET社区几乎没有提供什么帮助。尽管书中的内容同样适用于.NET,但是几乎没有.NET程序员读过它。

我希望这本.NET版本能够充当.NET社区和其他开发者社区之间的桥梁。我希望程序员能够阅读它并看到更好的构建软件的方法。我希望他们开始使用更好的软件实践、创建更好的设计并提升.NET应用的质量标准。我希望.NET程序员可以和其他程序员一样好。我希望.NET程序员能够在软件社区中获得新的地位,这样Java程序员就会以加入.NET团队为荣。

在完成本书的整个过程中,对于是否把我的名字放在一本与.NET有关的图书的封面上,我有过多次思想斗争。我曾问自己是否要把名字和.NET联系在一起,并承担可能由此带来的所有负面后果,但现在我不再迟疑了。我是一名.NET程序员。不!是一名敏捷的.NET程序员。我以此为荣。

关于本书

本书简史

20世纪90年代初,我(Bob)写了一本名为Designing Object-Oriented C++ Application using the Booch Method的书。它曾是我的代表作,其效果和销量都让我非常高兴。

这本书最初想作为Designing一书的第2版,但是结果却并非如此。书中所保留的原书内容非常少,只有3章内容,即便这3章也进行了大量的修改,但书的意图、精神以及许多知识是相同的。自Desinging出版10年以来,在软件设计和开发方面我又学到了非常多的知识,这些将在本书中表现出来。

10年过去了!Designing刚好在因特网大发展之前出版。从那时起,我们使用的缩略词的数量已经翻了一倍,诸如EJB、RMI、J2EE、XML、XSLT、HTML、ASP、JSP、ZOPE、SOAP、C#、.NET以及设计模式、Java、Servelet和应用服务器。我要告诉你,要使这本书的内容跟得上最新技术潮流非常困难。

与Booch的关系

1997年,Booch与我联系,让我帮他撰写其非常成功的Object-Oriented Analysis and Design with Applications一书的第3版。以前,我和他在一些项目中有过合作,并且是他的许多作品(包括UML)的热心读者和参编者。因此,我高兴地接受了,并邀请我的好朋友Jim Newkirk来帮助完成这项工作。

在接下来的两年中,我和Jim为Booch的书撰写了许多章节。当然,这些工作意味着我不可能按照我本来想的那样投入大量精力写作本书,但是我觉得Booch的书值得我这样做。另外,当时这本书完全只是Designing的第2版,并且我的心思也不在其上。如果我要讲些东西的话,我想讲些新的并且是不同的东西。

遗憾的是,Booch著作的这个版本始终没有完成。在正常情况下已经很难抽出空来写书了,在浮躁的.com泡沫时期,就更加不可能了。Grady忙于Rational以及Catapulse等新公司的事务。因此这项工作就停止了。最后,我问Grady和Addison-Wesley公司是否可以把我和Jim撰写的那些章节包含在本书中,他们很慷慨地同意了。于是,一些案例研究和UML的章节就由此而来。

极限编程的影响

1998年后期,极限编程(XP)崭露头角,它有力地冲击了我们所信奉的关于软件开发的观念。我们是应该在编写任何代码前先画许多UML图呢?还是应该不使用任何UML图而仅仅编写大量代码?我们是应该撰写大量描述我们设计的叙述性文档?还是应该努力使代码具有自释义能力以及表达力,免除撰写辅助性文档的必要?我们应该结对编程吗?我们应该在编写产品代码前先编写测试吗?我们应该做什么呢?

这场变革来得正是时候。在20世纪90年代中后期,Object Mentor公司在面向对象设计以及项目管理问题上帮助了许多公司。我们帮助这些公司完成项目,在此过程中,我们慢慢地向这些公司灌输自己的一些观点和做法。遗憾的是,这些观点和做法没有被记录下来,它们只是我们对客户的口述。

到了1998年,我认识到需要把我们的过程和实践写下来,这样就可以更好地把它们传达给我们的客户。于是,我在C++ Report上撰写了许多关于过程的论文[1],但这些文章都没有达到目的。它们提供了丰富的信息并且在某些情况下也很引人入胜,但是它们没有系统地反映我们在项目中实际应用的实践和看法,而是对影响我数十年的价值观念的一种不经意的放弃。Kent Beck向我指出了这一点。

与Kent Beck的关系

1998年末,当我正为整理Object-Mentor过程烦恼时,我偶然看到了Kent在极限编程(XP)方面的一些文字。这些文字散布在Ward Cunningham的wiki[2]中,并且和其他一些人的文字混合在一起。尽管如此,通过努力我还是抓住了Kent 所谈论的要点。这激起了我极大的兴趣,但是仍有一些疑虑。XP中的某些东西和我的开发过程观念完全吻合,但是其他一些东西,比如缺乏明确的设计阶段,却令我迷惑不解。

我和Kent来自完全不同的软件环境。他是一个知名的Smalltalk顾问,而我却是一个知名的C++顾问。这两个领域之间很难相互交流。这之间几乎有一个库恩式的(Kuhnian)[3]范型隔阂。

如果不是看到他的观点,我绝无可能邀请Kent为C++Report撰写论文。但是我们关于过程认识上的一致填补了语言上的隔阂。1999年2月,我在慕尼黑的OOP会议上遇到了Kent。他在进行关于XP的讲演,而我在进行面向对象设计原则的讲演,我们的会场所正好面对面。由于无法听到他的讲演,我就在午餐时找到了Kent。我们谈论了XP,我邀请他为C++Report撰写了一篇论文。这是一篇很棒的论文,其中描述了Kent和一位同事在一小时左右的现场系统开发中所进行的彻底的设计改变。

在接下来的几个月中,我逐渐消除了自己对XP的担心。我最大的担心在于所采用的过程中没有一个明显的预先设计阶段,我对此有些犹豫。我不是一直在教导我的客户以及整个行业,设计非常重要,应该投入时间吗?

最后我认识到,实际上我自己也并不真正需要这样一个阶段。甚至在我撰写的所有关于设计、Booch图和UML图的论文以及图书中,总是把代码作为验证这些图是否有意义的一种方式。在我所有的客户咨询项目中,我会先花费1~2个小时帮助他们绘制一些图,然后会使用代码来指导他们考查这些图。我开始明白,虽然XP关于设计的措词有点陌生(在库恩式的意义上),但是这些措词背后的实践对我来说却很熟悉。

我关于XP的另一个担心相对比较容易解决。我私底下实际上一直是一个结对程序员。XP使我可以光明正大地和同伴沉醉于一起编程的快乐之中。重构、持续集成以及现场客户对我来说都非常熟悉。它们都非常接近于我先前对客户建议的工作方式。

有一个XP实践对我来说是新的发现。当你第一次听到测试驱动开发[4](TDD)时会觉得它似乎很平常。它只是要在编写任何产品代码前先编写测试用例。编写的所有产品代码都是为了让失败的测试用例通过。对于这种方式编写代码所带来的意义深远的结果,我始料未及。这个实践完全改变了我编写软件的方法,并把它变得更好了。

于是,到1999年秋天,我确信Object Mentor应该采用XP作为自己的过程,并且我应该放弃编写自己过程的愿望。Kent在表达XP的实践和过程方面已经做了一项卓越的工作,相比起来我自己那些不充分的尝试就显得苍白无力了。

.NET

在几个大公司之间一直在进行着一场战争。这些公司在为了赢得你的忠诚而战。这些公司相信,如果它们拥有了语言,那么它们将拥有程序员以及雇佣这些程序员的公司。

首先打响这场战争的是Java。Java是第一个由大公司创造的用来赢得程序员的编程语言。结果取得了极大的成功。Java确实深深地扎根于软件开发社团中,并成为现代多层IT应用开发的事实标准。

对此的还击之一来自IBM。IBM通过Eclipse开发环境占领了大部分的Java市场。另外一个对此的重大阻击来自微软的一些追求完美的设计者,他们给我们提供了通用的.NET平台和特定的C#语言。

令人惊异的是,很难对Java和C#做出区分。这两种语言在语义上是等同的,并且语法也非常相似,以至于许多代码片段都可以写得很相似。所有在技术创新上的不足,微软都通过其在能力上的卓越表现进行了超额的补偿,并赶超上来,赢得胜利。

本书的第一版是使用Java和C++作为编程语言编写的。本书使用了C#和.NET平台。不要把这看作是对某一方的支持。我们不会在这场战争中拥护某一方。事实上,我认为当几年后一种更好的语言出现并占领了参与交战的公司花费巨大代价赢得的程序员意向份额时,这场战争就会自行结束。

本书的.NET版本只是为了能够影响到.NET程序员。虽然本书中的原则、模式和实践与语言无关,但是案例研究却与语言相关。正如.NET程序员更加乐于阅读.NET案例研究一样,Java程序员则更加乐于阅读Java示例。

尽在细节中

本书包含了许多.NET代码。希望你能够仔细阅读它们,因为在很大程度上,代码正是本书的精髓。代码是本书所讲内容的实际体现。

本书采用重复讲解的方式,由一系列不同规模的案例研究组成。有些案例非常小,有些案例则需要用几章来描述。每个案例研究之前都有一些预备材料,其中讲述了在该案例研究中将用到的面向对象设计原则和模式。

本书首先讨论了开发实践和过程,其中穿插了许多小的案例研究以及示例。然后,我们转移到设计和设计原则的主题上,接着是一些设计模式、更多管理包的设计原则以及更多的模式。所有这些主题都附有案例研究。

因此,请准备好学习一些代码和UML(统一建模语言)图。你将要学习的图书技术性非常强,其中要教授的知识就像恶魔一样,尽在细节中[5]

本书的内容结构

本书由四个部分和两个附录组成。

第一部分:敏捷开发。本部分描述了敏捷开发的概念。首先介绍了敏捷联盟宣言,然后提供了对极限编程(XP)的概述,接着讨论了许多阐明个别极限编程实践的小案例,特别是那些影响设计和编写代码方式的实践。

第二部分:敏捷设计。本部分中的各章谈论了面向对象软件设计:什么是面向对象软件设计,管理复杂性的问题以及技术,面向对象类设计的一些原则。本部分最后几章讲述UML实用子集。

第三部分:薪水支付案例研究。它描述了一个简单的批量处理薪水支付系统的面向对象设计和C#实现。本部分的前几章描述了该案例研究会用到的一些设计模式。最后一章包含了完整的案例研究,这也是本书中最大和最完整的一个案例。

第四部分:打包薪水支付系统。本部分开始描述面向对象包设计的一些原则。接着,通过增量地打包上一部分中的类来继续阐明这些原则。本部分最后讲述薪水支付应用的数据库和UI设计。

接下来是两个附录:附录A,“双公司记”;附录B,Jack Reeves的文章“什么是软件”。

如何使用本书

如果你是一名开发人员,请从头至尾阅读本书。本书主要是写给开发人员的,它包含以敏捷方式开发软件所需要的信息。从头至尾阅读可以首先学习实践,接着是原则,然后是模式,最后是把它们全部联系起来的案例研究。把所有这些知识整合起来会帮助你完成项目。

如果你是一名管理人员或者业务分析师,请阅读第一部分“敏捷开发”。第1章~第6章提供了对敏捷原则和实践的深入讨论。内容涉及需求、计划、测试、重构以及编程。它会给你一些有关如何构建团队以及管理项目的指导,帮助你完成项目。

如果你想学习UML,请首先阅读第13章~第19章。然后,阅读第三部分“薪水支付案例研究”的所有章节。这种阅读方法在UML语法和使用方面会给你提供一个好的基础,同时也会帮助你在UML和C#语言之间进行转换。

如果你想学习设计模式,请先阅读第二部分“敏捷设计”学习设计原则,然后阅读第三部分“薪水支付案例研究”、第四部分“打包薪水支付系统”。这几部分定义了所有的模式,并且展示了如何在典型的情形中使用它们。

如果你想学习面向对象设计原则,请阅读第二部分“敏捷设计”、第三部分“薪水支付案例研究”以及第四部分“打包薪水支付系统”。这些章节将会描述面向对象设计的原则,并且向你展示如何使用这些原则。

如果你想学习敏捷开发方法,请阅读第一部分“敏捷开发”。这部分描述了敏捷开发,内容涉及需求、计划、测试、重构以及编程。

如果你只想笑一笑,请阅读附录A“双公司记”。

致谢

衷心感谢以下人士:Lowell Lindstrom、Brian Button、Erik Meade、Mike Hill、Michael Feathers、Jim Newkirk、Micah Martin、Angelique Martin、Susan Rosso、Talisha Jefferson、Ron Jeffries、Kent Beck、Jeff Langr、David Farber、Bob Koss、James Grenning、Lance Welter、Pascal Roy、Martin Fowler、John Goodsen、Alan Apt、Paul Hodgetts、Phil Markgraf、Pete McBreen、H.S.Lahman、Dave Harris、James Kanze、Mark Webster、Chris Biegay、Alan Francis、Jessica D’Amico、Chris Guzikowski、Paul Petralia、Michelle Housley、David Chelimsky、Paul Pagel、Tim Ottinger、Christoffer Hedgate以及Neil Roodyn。

非常感谢Grady Booch和Paul Becker允许我在本书中使用原本用于Grady的Object-Oriented Analysis and Design with Applications第3版中的章节。特别感谢Jack Reeves,他慷慨地允许我全文引用他的论文“什么是软件设计”(What Is Software Design?)。

每章开头处美妙的、偶尔还有些炫目的插图是Jennifer Kohnke和我的女儿Angela Brooks绘制的。

[1].这些论文可以在http://www.object.mentor.com的publications部分找到,共有4篇。前3篇名为:Iterative and Incremental Development(I,II,III)。最后一篇名为:C.O.D.E Culled Object Development Process。

[2].http://c2.com/cgi/wiki。这个网站中包含有数量众多的涉及各种各样主题的论文,论文作者成百上千。人们都说,只有Ward Cunningham才能使用几行Perl煽动一场社会革命。

[3].写于1995~2001年的任何可信的学术作品中肯定使用了术语Kuhnian。它指的是The Structure of Scientific Revolutions一书,作者为Thomas S.Kuhn,由芝加哥大学出版社出版于1962年。[库恩是美国著名科学史家和哲学家,在其代表作《科学革命的结构》一书中提出了“范型转换”(paradigm shift)理论。——编者注]

[4].Kent Beck,Test-Driven Development by Example,Addison-Wesley,2003。

[5].原文为The devils are in the details,谚语,相当于韩非子所说的“千里之堤,溃于蚁穴”,比喻细节之重要。——编者注

第一部分 敏捷开发

人与人之间的交互是复杂的,并且其效果从来都难以预期,但是它们却是工作中最为重要的方面。

——Tom DeMarco和Timothy Lister,《人件》

原则(principle)、模式(pattern)和实践(practice)都是重要的,但是使它们发挥作用的是人。正如Alistair Cockburn所说的:“过程和技术对于项目的结果只有次要的影响。首要的影响是人。”[1]

如果把程序员团队看作是由过程驱动的组件(component)所组成的系统,那么就无法对他们进行管理。用Alistair Cockburn的话来说,人不是“插入即兼容的编程装置。”如果想要项目取得成功,我们就必须构建起具有合作精神的、自组织(self-organizing)的团队。

鼓励构建这种团队的公司比认为软件开发组织不过是由无关紧要的、雷同的一群人堆砌起来的公司更具竞争优势。凝聚在一起的软件团队是最强大的软件开发力量。

第1章 敏捷实践

教堂尖顶上的风标,即使由钢铁制成,如果不懂得顺应风势的艺术,一样会很快被暴风所摧毁。

——海因里希·海涅,德国诗人

许多人都经历过由于没有实践的指导而导致的项目噩梦。缺乏有效的实践会导致不可预测性、重复的错误以及努力白白浪费。延期的进度、增长的预算和低劣的质量致使客户对我们丧失信心。更长时间的工作却生产出更加低劣的软件产品,也使得开发人员感到沮丧。

一旦经历了这样的惨败,就会害怕重蹈覆辙。这种恐惧激发我们创建一个过程来约束我们的活动,并要求某些输出和制品(artifact)。我们根据过去的经验来规定这些约束和输出,挑选那些在以前的项目中看起来好像工作得不错的方法。我们希望这些方法这次还会有效,从而消除我们的恐惧。

然而,项目并没有简单到使用一些约束和制品就能够可靠地防止错误的地步。当连续地犯错误时,我们会对错误进行诊断,并在过程中增加更多的约束和制品来防止以后重犯这样的错误。经过多个项目以后,我们就会不堪巨大、笨重的过程的重负,极大地削弱我们完成项目的能力。

一个大而笨重的过程会产生它本来企图去避免的问题。它降低了团队的开发效率,使得进度延期,预算超支。它降低了团队的响应能力,使得团队经常创建错误的产品。遗憾的是,这导致许多团队认为,这种结果是因为他们没有采用足够多的过程方法引起的。因此,在这种失控的过程膨胀中,过程会变得越来越庞大。

用失控的过程膨胀来描述公元2000年前后许多软件公司中的情形是很合适的。虽然有很多团队在工作中并没有使用过程方法,但是采用庞大、重型的过程方法的趋势却在迅速增长,在大公司中尤其如此。

1.1 敏捷联盟

2001年初,由于看到许多公司的软件团队陷入了不断增长的过程的泥潭,一批业界专家聚集在一起概括出了一些可以让软件开发团队具有快速工作和响应变化能力的价值观和原则。他们称自己为敏捷联盟。在随后的几个月中,他们创建出了一份价值观声明,也就是敏捷联盟宣言(The Manifesto of the Agile Alliance)。

敏捷软件开发宣言

我们正在通过亲身实践以及帮助他人实践,揭示更好的软件开发方法。

通过这项工作,我们认为:

人和交互  重于  过程和工具

可以工作的软件  重于  面面俱到的文档

客户合作  重于  合同谈判

随时应对变化  重于  遵循计划

虽然右项也有其价值,但我们认为左项更加重要。

Kent Beck     Mike Beedle   Arie van Bennekum  Alistair Cockburn

Ward Cunningham  Martin Fowler  James Grenning   Jim Highsmith

Andrew Hunt   Ron Jeffries   Jon Kern      Brian Marick

Robert C.Martin  Steve Mellor   Ken Schwaber    Jeff Sutherland

Dave Thomas

1.1.1 人和交互重于过程和工具

人是获得成功的最为重要的因素。如果团队中没有优秀的成员,那么就算是使用好的过程也不能从失败中挽救项目。但是,不好的过程却可以使最优秀的团队成员失去效用。如果不能作为一个团队进行工作,那么即使拥有一批优秀的成员也一样会惨败。

一个优秀的团队成员未必就是一个一流的程序员。一个优秀的团队成员可能是一个具有一般水平的程序员,但是却能够很好地与他人合作。好的合作(沟通以及交互)能力要比单纯的编程能力更为重要。一个由一般水平的、具有良好沟通能力的程序员组成的团队,比那些拥有一批高水平的程序员,但是成员之间却不能进行交流的团队更有可能获得成功。

合适的工具对于成功来说非常重要。像编译器、IDE和源代码控制系统等,对于团队的开发者正确地完成他们的工作至关重要。然而,工具的作用可能会被过分地夸大。使用过多庞大、笨重的工具就像缺少工具一样,都是不好的。

我的建议是从使用小工具开始。尝试一个工具,直到发现它无法适用时才去更换它。不要急着去购买那些先进的、价格昂贵的源代码控制系统,相反应该先使用一个免费的系统,直到能够证明该系统已经不再适用。在决定为团队购买最好的CASE工具许可证前,先使用白板和方格纸,直到明确地知道需要更多的功能。在决定使用庞大的、高性能的数据库系统前,先使用平面文件(flat file)。不要认为更大的、更好的工具可以自动地帮你做得更好。通常,它们造成的障碍要大于带来的帮助。

记住,团队的构建要比环境的构建重要得多。许多团队和管理者就犯了先构建环境,然后期望团队自动凝聚在一起的错误。相反,应该首先致力于构建团队,然后再让团队基于需要来配置环境。

1.1.2 可以工作的软件重于面面俱到的文档

没有文档的软件是一种灾难。代码不是交流系统原理和结构的理想媒介。团队更需要编制容易看懂的文档,来对系统及其设计决策的依据进行描述。

然而,过多的文档比过少的文档更糟。编制众多的文档需要花费大量的时间,并且使这些文档和代码保持同步,要花费更多的时间。如果文档和代码之间失去同步,那么文档就会变成庞大的、复杂的谎言,会造成重大的误导。

对于团队来说,编写并维护一份系统原理和结构方面的文档总是一个好主意,但是那份文档应该短小并且主题突出。短小的意思是说,最多有一二十页。主题突出的意思是说,应该仅论述系统的最高层结构和概括的设计原理。

如果我们拥有的仅仅是一份简短的系统原理和结构方面的文档,那么如何来培训新的团队成员,使他们能够从事系统相关的工作呢?我们会非常密切地和他们一起工作。我们紧挨着他们坐下来帮助他们,把我们的知识传授给他们。我们通过频繁的培训和交互使他们成为团队的一部分。

在向新的团队成员传授知识方面,最好的两份文档是代码和团队。代码真实地表达了它所做的事情。虽然从代码中提取系统的原理和结构信息可能是困难的,但是代码是唯一没有二义性的信息源。在团队成员的头脑中,保存着时常变化的系统的脉络图。人和人之间的交互是比把这份脉络图记在纸上并传授给他人更快、更有效的方式。

许多团队因为注重文档而非软件,从而导致进度拖延。这常常是一个致命的缺陷。有一个简单规则可以预防该缺陷的发生。

Martin文档第一定律(Martin’s First Law of Documentation)

直到迫切需要并且意义重大时,才编制文档。

1.1.3 客户合作重于合同谈判

不能像订购日用品一样来订购软件。你不能够仅仅写下一份关于你想要的软件的描述,然后就让人在固定的时间内以固定的价格去开发它。所有用这种方式来对待软件项目的尝试都将以失败而告终。有时,失败是惨重的。

告诉开发团队想要的东西,然后期望开发团队消失一段时间回来后就能够交付一个满足需要的系统,这对于公司的管理者来说是具有诱惑力的。然而,这种操作模式将导致低劣的质量和失败。

成功的项目需要定期且频繁的客户反馈。不是依赖于合同或者关于工作的陈述,而是让软件的客户和开发团队密切地工作在一起,并尽量经常地提供反馈。

一个指明了需求、进度以及项目成本的合同存在根本的缺陷。在大多数情况下,合同中规定的条款远在项目完成之前(有时甚至是远在合同签署之前)就变得没有意义。那些为开发团队和客户的协同工作方式提供指导的合同才是最好的合同。

我在1994年为一个大型、需要多年才能完成并有50万行代码的项目达成的合同,可以作为一个成功合同的样例。作为开发团队的我们,每个月的报酬相对是比较低的。大部分的报酬要在我们交付了某些大的功能块后才支付。那些功能块没有在合同中详细地指明。合同中仅仅规定在一个功能块通过了客户的验收测试时才支付该功能块的报酬。那些验收测试的细节并没有在合同中指明。

在这个项目开发期间,我们和客户紧密地工作在一起。几乎每个周五,我们都会把软件提交给客户。到下一周的周一或者周二,客户会给我们一份关于软件的变更列表。我们会把这些变更放在一起排定优先级,然后把它们安排在随后几周的工作中。客户和我们如此紧密地工作在一起,以至于验收测试根本就不是问题。因为他们周复一周地观察着每个功能块的演进,所以他们知道何时这个功能块能够满足他们的需要。

这个项目的需求基本处于一个持续变化的状态。大的变更是很平常的。在这期间,也会出现整个功能块被去掉,而另外的功能块被加进来的情况。然而,合同和项目都经受住了这些变更,并获得成功。成功的关键在于与客户的紧密协作,并且合同指导了这种协作,而不是试图去规定项目范围的细节和固定成本下的进度。

1.1.4 随时应对变化重于遵循计划

随时应对变化的能力常常决定着一个软件项目的成败。当我们构建计划时,应该确保计划是灵活的,并且易于适应商务和技术方面的变化。

计划不能考虑得过远。首先,商务环境很可能会变化,这会引起需求的变动。其次,一旦客户看到系统开始运作,他们很可能会改变需求。最后,即使我们知道需求是什么,并且确信它们不会改变,我们仍然不能很好地估算出开发它们需要的时间。

对于一个缺乏经验的管理者来说,创建一张优美的、关于整个项目的PERT或者甘特图,并把它贴到墙上是很有诱惑力的。他们也许觉得这张图赋予了他们对项目的控制权。他们能够跟踪单个人的任务,并在任务完成时将任务从图上去除。他们可以将实际完成的日期和计划完成的日期进行比较,并对出现的任何偏差做出反应。

然而,实际会发生的是:这张图的组织结构不再适用。当团队对于系统有了新认识,当客户对于需求有了新认识,图中的某些任务会变得没有必要。另外一些任务会被发现并增加到图中。简而言之,计划图的形状(shape)将会改变,而不仅仅是日期上的改变。

较好的做计划的策略是:为下一周做详细的计划,为下3个月做粗略的计划,再以后就做极为简略的计划。我们应该清楚地知道下周要完成的任务,粗略地了解一下以后3个月要实现的需求。至于系统一年后将要做什么,有一个模糊的想法就行了。

计划中这种逐渐降低的细致度,意味着我们仅仅对于迫切的任务才花费时间进行详细的计划。一旦制定了这个详细的计划,就很难进行改变,因为团队会根据这个计划启动工作并有相应的投入。然而,由于该计划仅仅支配了一周的时间,计划的其余部分仍然保持着灵活性。

1.2 原则

从上述的价值观中引出了下面的12条原则,它们是敏捷实践区别于重型过程的特征所在。

(1) 我们最优先要做的是通过尽早地、持续地交付有价值的软件来使客户满意。《MIT Sloan管理评论》杂志刊登过一篇论文,分析了对于公司构建高质量产品方面有帮助的软件开发实践[2]。该论文总结了很多对于最终系统质量有重要影响的实践。其中一个实践表明,尽早交付具有部分功能的系统和系统质量之间具有很强的相关性。该论文指出,初期交付的系统中所包含的功能越少,最终交付的系统的质量就越高。该论文的另一项发现是,以逐渐增加功能的方式经常性地交付系统和最终质量之间有非常强的相关性。交付得越频繁,最终产品的质量就越高。

敏捷实践会尽早地、经常地进行交付。我们努力在项目刚开始的几周内就交付一个具有基本功能的系统。然后,我们努力坚持每几周就交付一个功能渐增的系统。如果客户认为目前的功能已经足够了,客户可以选择把这些系统加入到产品中。或者,他们可以只是选择再检查一遍已有的功能,并指出他们想要做的改变。

(2) 我们欢迎需求的变化,即使到了开发后期。敏捷过程能够驾驭变化,为客户创造竞争优势。这是一个关于态度的声明。敏捷过程的参与者不惧怕变化。他们认为改变需求是好事情,因为那些改变意味着团队已经学到了更多如何满足客户需要的知识。

敏捷团队会非常努力地保持软件结构的灵活性,这样当需求变化时,对于系统造成的影响是最小的。在本书的后面部分,我们会学习一些面向对象设计的原则、模式和实践,这些内容会帮助我们维持这种灵活性。

(3) 经常交付可以工作的软件,从几个星期到几个月,时间间隔越短越好。我们交付可以工作的软件,并且尽早地、经常性地交付它。我们不赞成交付大量的文档或者计划。我们认为那些不是真正要交付的东西。我们关注的目标是交付满足客户需要的软件。

(4) 在整个项目开发期间,业务人员和开发人员必须朝夕工作在一起。为了能够以敏捷的方式进行项目的开发,客户、开发人员以及利益相关者之间就必须要进行有意义的、频繁的交互。软件项目不像发射出去就能够自动导航的武器,必须要对软件项目持续不断地进行引导。

(5) 依靠斗志高昂的人构建项目。给他们提供所需的环境和支持,并且信任他们能够完成工作。人是项目取得成功的最重要的因素。所有其他的因素(过程、环境、管理等)都被认为是次要的,当它们对人有负面的影响时,就要对它们进行改变。

(6) 在团队内部,最有效率也最有效果的信息传达方式,就是面对面的交谈。在敏捷项目中,人们之间相互交谈。首要的沟通方式就是人与人之间的交互。书面文档会按照和软件一样的时间安排进行编写和更新,但是仅在需要时才这样做。

(7) 可以工作的软件是进度主要的度量标准。敏捷项目通过度量当前满足客户需求的软件量来度量开发进度。他们不是根据所处的开发阶段、已经编写的文档总量或者已经创建的基础设施代码的数量来度量开发进度。仅当30%的必需功能可以工作时,才可以确定进度完成了30%。

(8) 敏捷过程提倡可持续开发。出资人、开发者和用户应该总是保持稳定的开发速度。敏捷项目不是 米短跑;而是马拉松长跑。团队不是以全速启动并试图在项目开发期间维持那个速度;相反,他们以快速但是可持续的速度行进。

跑得过快会导致团队精力耗尽、抄捷径以致崩溃。敏捷团队会测量他们自己的速度。他们不允许自己过于疲惫。他们不会借用明天的精力来在今天多完成一点工作。他们工作在一个可以保证在整个项目开发期间保持最高质量标准的速度上。

(9) 对卓越技术和良好设计的不断追求有助于提高敏捷性。高的产品质量是获取高的开发速度的关键。保持软件尽可能干净、健壮是快速开发软件的途径。因而,所有的敏捷团队成员都致力于只编写他们能够编写的最高质量的代码。他们不会制造混乱然后告诉自己等有更多的时间时再来清理它们。如果他们在今天制造了混乱,就会在今天把混乱清理干净。

(10) 简单——尽量减少工作量的艺术是至关重要的。敏捷团队不会试图去构建那些华而不实的系统,他们总是更愿意采用和目标一致的最简单的方法。他们并不看重对于明天会出现的问题的预测,也不会在今天就对那些问题进行防卫。相反,他们在今天以最高的质量完成最简单的工作,并深信如果在明天发生了问题,也会很容易进行处理。

(11) 最好的构架、需求和设计都源自自我组织的团队。敏捷团队是自我组织的团队。责任不是从外部分配给单个团队成员,而是分配给整个团队,然后再由团队来确定履行职责的最好方法。

敏捷团队的成员共同来解决项目中所有方面的问题。每一个成员都具有项目中所有方面的参与权力。不存在某个团队成员仅对系统构架、需求或者测试负责的情况。整个团队共同承担那些职责,每一个团队成员都能够影响它们。

(12) 每隔一定时间,团队都要总结如何更有效率,然后相应地调整自己的行为。敏捷团队会不断地对团队的组织方式、规则、约定和关系等进行调整。敏捷团队知道团队所处的环境在不断地变化,并且知道为了保持团队的敏捷性,就必须要随环境一起变化。

1.3 结论

每一个软件开发人员、每一个开发团队的职业目标,都是给他们的雇主和客户交付最大可能的价值。可是,我们的项目令人沮丧地失败了,或者未能交付任何价值。虽然在项目中采用过程方法是出于好意,但是膨胀的过程方法对于我们的失败至少是应该负一些责任的。敏捷软件开发的原则和价值观构成了一个可以帮助团队打破过程膨胀循环的方法,这个方法关注的是可以达到团队目标的一些简单的技术。

在撰写本书的时候,已经有许多的敏捷过程可供选择,包括SCRUM[3]、Crystal[4]、特征驱动软件开发[5](Feature-Driven Development,FDD)、自适应软件开发[6](Adaptive Software Development, ADP)以及极限编程[7](Extreme Programming,XP)。不过,绝大多数成功的敏捷团队都是从所有这些过程方法中汲取知识并调和成最适合自己的方法。常见的做法是把SCRUM和XP结合起来,其中使用SCRUM实践来管理多个使用XP实践的团队。

1.4 参考文献

[Beck99]Kent Beck,Extreme Programming Explained:Embrace Change,Addison-Wesley,1999.

[Highsmith2000]James A.Highsmith,Adaptive Software Development:A Collaborative Approach to Managing Complex Systems,Dorset House,2000.

[Newkirk2001] James Newkirk and Robert C.Martin,Extreme Programming in Practice,Addison-Wesley,2001.

第2章 极限编程概述

作为开发人员,我们应该记住,XP并非唯一选择。

——Pete McBreen,软件技术专家

在第1章中,我们概述了有关敏捷软件开发方法方面的内容,但它没有确切地告诉我们去做些什么;其中给出了一些泛泛的陈述和目标,却没有给出实际的指导方法。本章要改变这种状况。

2.1 极限编程实践

2.1.1 完整团队

我们希望客户、管理者和开发人员紧密地工作在一起,以便于彼此知晓对方所面临的问题,并共同去解决这些问题。谁是客户?XP团队中的客户是指定义产品的特性并排列这些特性优先级的人或者团体。有时,客户是和开发人员同属一家公司的一组业务分析师、质量保证专家和/或者市场专家。有时,客户是用户团体委派的用户代表。有时,客户是真正支付开发费用的人。但是在XP项目中,无论谁是客户,他们都是能够和团队一起工作的团队成员。

最好的情况是客户和开发人员在同一个房间中工作,次一点的情况是客户和开发人员之间的工作距离在100m以内。距离越大,客户就越难成为真正的团队成员。如果客户工作在另外一幢建筑或另外一个城市,那么他将会很难融合到团队中来。

如果确实无法和客户工作在一起,该怎么办呢?我的建议是去寻找能够在一起工作、愿意并能够代替真正客户的人。

2.1.2 用户故事

为了进行项目计划,必须要了解需求,但是却无需了解得太多。对于做计划而言,了解需求只需要到能够估算它的程度就足够了。你可能认为,为了对需求进行估算,就必须要了解该需求的所有细节。其实并非如此。你必须知道存在很多的细节,也必须知道细节的大致分类,但是你不必知道特定的细节。

需求的具体细节很可能会随时间而改变,一旦客户开始看到集成到一起的系统,就更会如此。看到新系统的问世是关注需求的最好时刻。因此,不要去捕获某个在很长一段时间之后才会实现的需求的特定细节,否则很可能会导致无用功以及对需求不成熟的关注。

在XP中,我们和客户反复讨论,以获取对于需求细节的理解,但是不去记录那些细节。我们更愿意客户在索引卡片上写下一些共识的言语,这些只言片语可以提醒我们记起这次交谈。基本上在客户进行书写的同一时刻,开发人员在该卡片上写下对应于卡片上需求的估算。估算是基于在和客户进行交谈期间所得到的对于细节的理解进行的。

用户故事(user story)就是正在进行的关于需求的谈话的助记符。它是一个计划工具,客户可以使用它并根据需求的优先级和估算代价来安排实现该需求的时间。

2.1.3 短交付周期

XP项目每两周交付一次可以工作的软件。每两周的迭代都实现了利益相关者的一些需求。在每次迭代结束时,会给利益相关者演示迭代生成的系统,以得到他们的反馈。

迭代计划

每次迭代通常耗时两周。迭代是一次较小的交付,可能会加入到产品中,也可能不会。迭代计划由一组用户故事组成,这些用户故事是客户根据开发人员确定的预算选出来的。

开发人员通过度量在以前的迭代中所完成的工作量来为本次迭代设定预算。只要估算成本的总量不超过预算,客户就可以为本次迭代选择任意数量的用户故事。

一旦迭代开始,客户就同意不再修改当次迭代中用户故事的定义和优先级别。迭代期间,开发人员可以自由地将用户故事分解成任务(task),并依据最具技术和商业意义的顺序来开发这些任务。

发布计划

XP团队通常会创建一个发布计划来规划出随后大约6次迭代的内容。这就是所谓的发布计划。一次发布通常需要3个月的工作。它表示了一次较大的交付,通常此次交付会被加入到产品中。发布计划是由客户根据开发人员给出的预算所选择的、排好优先级别的一组用户故事组成。

开发人员通过度量在以前的发布中所完成的工作量来为本次发布设定预算。只要估算成本的总量不超过预算,客户就可以为本次发布选择任意数目的用户故事。客户同样可以决定在本次发布中用户故事的实现顺序。如果开发团队强烈要求的话,客户可以通过指明哪些用户故事应该在哪次迭代中完成的方式,制订出发布中最初几次迭代的内容。

发布计划不是一成不变的。客户可以随时改变发布的内容。他可以取消用户故事,编写新的用户故事,或者改变用户故事的优先级别。但是,客户应该尽量不去更改一次迭代。

2.1.4 验收测试

可以以客户指定的验收测试的形式来记录有关用户故事的细节。用户故事的验收测试是在就要实现该用户故事之前,或者在实现该用户故事的同时才开始编写的。验收测试使用脚本语言编写,这样它们可以自动、反复地运行[8]。这些测试共同来验证系统是否按照客户指定的行为运转。

验收测试是由业务分析师、质量保证专家以及测试人员在迭代期间编写的。编写验收测试使用的语言对于程序员、客户以及业务人员来说都很容易阅读和理解。程序员就是从这些测试中了解他们正在实现的故事的真实工作细节。这些测试成为真正的项目需求文档。验收测试描述了每个特性的所有细节,并用作验证这些特性是否被正确完成的决定性依据。

一旦通过一项验收测试,就将该测试加入到已经通过的验收测试集合中,并决不允许该测试再次失败。这个不断增长的验收测试集合每天会多次运行,每当系统被创建时,都要运行这个验收测试集。如果一项验收测试失败了,那么系统创建就宣告失败。因而,一项需求一旦被实现,就再不会遭到破坏。系统从一种工作状态迁移到另一种工作状态,期间,系统的不能工作状态时间决不允许超过几个小时。

2.1.5 结对编程

代码都是由结对的程序员使用同一台工作站共同完成的。结对人员中,一个控制键盘并输入代码。另一个观察着输入的代码,寻找着代码中的错误和可以改进的地方[9]。两个人认真地进行着交互。他们都全身心地投入到软件的编写中。

两人频繁互换角色。控制键盘的可能累了或者遇到了困难,他的同伴会取得键盘的控制权。在一个小时内,键盘可能在他们之间来回传递好几次。最终生成的代码是由他们两人共同设计、共同编写的,两人功劳均等。

结对的关系要经常变换。每天至少要改变一次,这样每个程序员在一天中可以在两个不同的结对中工作。在一次迭代期间,每个团队成员应该和所有其他的团队成员在一起工作过,并且他们应该参与了本次迭代中所涉及的每项工作。

结对编程会极大地促进知识在团队中的传播。仍然会需要一些专业知识,那些需要一定专业知识的任务通常需要合适的专家去完成,但是那些专家几乎将会和团队中的所有其他人结对。这将加快专业知识在团队中的传播。这样,在紧要关头,其他团队成员就能够代替所需要的专家。Williams[10]和Nosek[11]的研究表明,结对非但不会降低编程人员的效率,反而会大大减少缺陷率。

2.1.6 测试驱动开发

第4章会详细地讨论这个主题。在此,我们仅进行大致的介绍。

编写所有产品代码的目的都是为了使失败的单元测试能够通过。首先编写一个单元测试,由于它要测试的功能还不存在,所以它会运行失败。然后,编写代码使测试通过。

编写测试用例和代码之间的更迭速度是很快的,基本上在几分钟左右。测试用例和代码共同演化,其中测试用例循序渐进地对代码的编写进行指导(参见第6章中的例子)。

作为结果,一个非常完整的测试用例集就和代码一起发展起来。程序员可以使用这些测试来检查程序是否正确地工作。如果结对的程序员对代码进行了小的更改,那么他们可以运行测试,以确保更改没有对程序造成任何的破坏。这会非常有利于重构(在本章后面介绍)。

当为了使测试用例通过而编写代码时,那么所编写的代码天生就是可测试的。更重要的是,这样做会强烈地激发你去解除各个模块间的耦合,以便能够独立地对它们进行测试。因而,以这种方式编写的代码的设计往往具有更弱的耦合。面向对象设计的原则在进行这种解耦方面具有巨大的帮助作用(参见本书第二部分)。

2.1.7 集体所有

每一对编程者都具有签出(check out)任何模块并对它进行改进的权力。每个程序员都不会对任何一个特定的模块或技术单独负责。每个人都参与GUI方面的工作[12],每个人都参与中间件方面的工作,每个人都参与数据库方面的工作。任何人都不会比其他人在一个模块或者技术上具有更多的权威。

这并不意味着XP不需要专业知识。如果你的专业领域是有关GUI的,那么你最有可能去从事GUI方面的任务,但是你也将会被邀请去和别人结对从事有关中间件和数据库方面的任务。如果你决定去学习另一门专业知识,那么你可以承担相关的任务,并和能够传授你这方面知识的专家一起工作。你不会被限制在自己的专业领域。

2.1.8 持续集成

程序员每天会多次签入(check in)他们的代码并进行集成。规则很简单:第一个签入的只要完成签入就可以了,所有后面签入的人负责代码的合并工作。

XP团队使用非阻塞的源代码控制工具。这就意味着程序员可以在任何时候签出任何模块,而不管是否有其他人已经签出了这个模块。当程序员完成了对于模块的修改并把该模块签入时,他必须把他所做的改动和在他前面签入该模块的程序员所作的任何改动进行合并。为了避免合并的时间过长,团队的成员会非常频繁地检查他们的模块。

结对人员会在一项任务上工作一到两个小时。他们创建测试用例和产品代码。在某个适当的间歇点,也许远在这项任务完成之前,他们决定把代码签入回去。他们首先确保所有的测试都能够通过,然后把新的代码集成进当前的代码库中。如果需要,他们会对代码进行合并。如果有必要,他们会和在签入上有冲突的其他程序员协商。一旦集成进了他们的更改,他们就构建新的系统。他们运行系统中的每一个测试,包括当前所有有效的验收测试。如果他们破坏了原先可以工作的部分,他们会进行修正。一旦所有的测试都通过了,他们就算完成了此次签入工作。

因而,XP团队每天会进行多次系统构建。他们会从头开始创建整个系统[13]。如果系统的最终结果是一张CD,他们就刻录该CD。如果系统的最终结果是一个可以访问的Web站点,他们就安装该Web站点,或许会把它安装在一个测试服务器上。

2.1.9 可持续的开发速度

软件项目不是全速短跑,它是马拉松长跑。那些一跃过起跑线就开始尽力狂奔的团队将会在远离终点前就筋疲力尽。为了快速地完成开发,团队必须要以一种可持续的速度前进。团队必须保持旺盛的精力和敏锐的警觉。团队必须要有意识地保持稳定、适中的速度。

XP的规则不允许团队加班工作。在版本发布前的一个星期是该规则的唯一例外。如果发布目标就在眼前并且能够一蹴而就,则允许加班。

2.1.10 开放的工作空间

团队在一个开放的房间中一起工作。房间中有一些桌子。每张桌子上摆放了两到三台工作站。每台工作站前有两把椅子。墙壁上挂满了状态图表、任务明细表、UML图,等等。

房间里充满了交谈的嗡嗡声,结对编程的两人坐在互相能够听得到的距离内,每个人都可以得知另一人是否遇到了麻烦,每个人都了解对方的工作状态,程序员们都处在适合于激烈地进行讨论的位置上。

可能有人认为这种环境会分散人的注意力。很容易会让人担心由于持续的噪音和干扰而一事无成。事实上并非如此。而且,密歇根大学的一项研究表明,在“充满积极讨论的屋子”(war room)里工作,生产率非但不会降低,反而会成倍地提高[14]

2.1.11 计划游戏

第3章中会详细介绍XP的计划游戏。在这里,仅做简要介绍。

计划游戏(planning game)的本质是划分业务和开发之间的职责。业务人员(也就是客户)决定特性的重要性,开发人员决定实现一个特性所花费的代价。

在每次发布和迭代的开始,开发人员向客户提供一个预算。客户选择那些所需的代价合计起来小于等于该预算的用户故事。开发者所提供的预算是基于他们在最近一次迭代或者发布中所完成的工作量进行的。

依据这些简单的规则,采用短周期迭代和频繁的发布,很快客户和开发人员就会适应项目的开发节奏。客户会了解开发人员的开发速度。基于这种了解,客户能够确定项目会持续多长时间,以及会花费多少成本。

2.1.12 简单设计

XP团队使他们的设计尽可能的简单、有表达力。此外,他们仅仅关注于计划在本次迭代中要完成的用户故事,而不会考虑那些未来的用户故事。团队更愿意在一次次的迭代中不断地变迁系统的设计,使之对正在实现的用户故事而言始终保持在最优状态。

这意味着XP团队的工作可能不会从基础设施开始。他们并不先去选择数据库或者中间件,而是先以最简单的可能方式实现第一批用户故事。只有当出现一个用户故事迫切需要基础设施时,他们才会引入该基础设施。

下面3条XP指导原则(mantra)可以对开发人员进行指导。

(1) 考虑能够工作的最简单的事情。XP团队总是尽可能寻找能实现当前用户故事的最简单的设计。在实现当前的用户故事时,如果能够使用平面文件,就不去使用数据库;如果能够使用简单的socket连接,就不去使用ORB或者Web Service;如果能够不使用多线程,就别去用它。我们尽量考虑用最简单的方法来实现当前的用户故事。然后,选择一种我们能够实际得到的和该简单性最接近的解决方案。

(2) 你不需要它。是的,但是我们知道总有一天会需要数据库,会需要ORB,也总有一天得去支持多用户。所以,我们现在就需要为那些东西做好准备,不是吗?

如果在确实需要基础设施前拒绝引入它,那么会发生什么呢?XP团队会对此进行认真的考虑。他们开始时假设将不需要那些基础设施。只有在有证据,或者至少有十分明显的迹像表明现在引入这些基础设施比继续等待更加合算时,团队才会引入这些基础设施。

(3) 一次,并且只有一次。极限编程者不能容忍重复的代码。无论在哪里发现重复的代码,他们都会消除这些重复。

导致代码重复的因素有许多。最明显的是通过鼠标选中一段代码,然后四处进行粘贴。当发现那些重复的代码时,我们会通过创建一个函数或基类的方法来消除它们。有时两个或多个算法非常相似,但是它们之间又存在有微妙的差别,我们会把它们变成函数,或者使用TEMPLATE METHOD模式(请参见第22章)。无论重复代码源于何处,一旦发现,就必须被消除。

消除重复最好的方法就是抽象。毕竟,如果两种事物相似的话,必定存在某种抽象能够统一它们。这样,消除重复的行为会迫使团队提炼出许多的抽象,并进一步减少代码间的耦合。

2.1.13 重构

第5章会对重构进行详细的讨论[15],下面只是一个简单的介绍。

代码往往会腐化。随着我们添加一个又一个的特性,处理一个又一个的错误,代码的结构会逐渐退化。如果对此置之不理的话,这种退化最终会导致纠结不清、难于维护的混乱代码。

XP团队通过经常性的代码重构来扭转这种退化。重构就是在不改变代码行为的前提下,对其进行一系列小的改造,旨在改进系统结构的实践活动。每个改造都是微不足道的,几乎不值得去做。但是所有的这些改造叠加在一起,就形成了对系统设计和构架显著的改进。

在每次细微改造之后,我们都会运行单元测试以确保改造没有造成任何破坏,然后再去做下一次改造。如此往复,周而复始。通过这种方式,我们可以在改造系统设计的同时,保持系统可以工作。

重构是持续进行的,而不是在项目结束时、发布版本时、迭代结束时甚至每天快下班时才进行的。重构是我们每隔一个小时或者半个小时就要去做的事情。通过重构,我们可以持续地保持代码尽可能干净、简单并且具有表达力。

2.1.14 隐喻

隐喻(metaphor)是唯一一个不具体、不直接的XP实践,也是所有XP实践中最难理解的一个。极限编程者在本质上都是务实主义者,隐喻这个缺乏具体定义的概念使我们觉得很不舒服。的确,一些XP的支持者经常讨论把隐喻从XP的实践中去除。然而,在某种意义上,隐喻却是XP所有实践中最重要的实践之一。

想象一下智力拼图玩具。你怎样知道如何把各个小块拼在一起?显然,每一块都与其他块相邻,并且它的形状必须与相邻的块完美地吻合。如果你眼睛看不见但是具有很好的触觉,那么通过锲而不舍地筛选每个小块,不断地尝试它们的位置,也能够拼出整个图形。

但是,相对于各个小块的形状而言,还有一种更为强大的力量把这些复杂的小块拼装在一起。这就是整张拼图的图案。图案是真正的向导。它的力量是如此之大,以至于如果图案中相邻的两块不具有互相吻合的形状,那么你就可以断定拼图玩具的制作者把玩具做错了。

这就是隐喻。它是将整个系统联系在一起的全局视图。它是系统的愿景,是它使得所有单独模块的位置和外观变得明显直观。如果模块的外观与整个系统的隐喻不符,那么你就知道该模块是错误的。

隐喻通常可以归结为一个名字系统。这些名字提供了一个系统组成元素的词汇表,并且有助于定义它们之间的关系。

例如,我曾经开发过一个以每秒60个字符的速度将文本输出到屏幕的系统。以这样的速度,字符充满整个屏幕需要一段时间。所以我们让产生文本的程序把产生的文本放到一个缓冲区中。当缓冲区满了的时候,我们把该程序交换到磁盘上。当缓冲区快要变空时,我们把该程序交换回来并让它继续运行。

我们用装卸卡车拖运垃圾来比喻整个系统。缓冲区是小卡车。屏幕是垃圾场。程序是垃圾制造者。所有的名字相互吻合,这有助于我们从整体上去考虑系统。

举另一个例子,我曾经开发过一个分析网络流量的系统。每30分钟,系统会轮询几十个网络适配器,并从中获取监控数据。每个网络适配器为我们提供一小块由几个单独变量组成的数据。我们称这些数据块为“面包切片”。这些面包切片是待分析的原始数据。分析程序“烤制”这些切片,因而被称为“烤面包机”。我们把数据块中的单个变量称为“面包屑”。总之,它是一个有用并且有趣的隐喻。

当然,隐喻不仅仅是一个名字系统。隐喻是系统的愿景,它指导着所有开发者去选择合适的名字,把函数放到合适的位置,创建出新的合适的类和方法,等等。

2.2 结论

极限编程是一组简单、具体的实践,这些实践结合在一起形成了一个敏捷开发过程。极限编程是一种优良、通用的软件开发方法。对于大多数项目团队来说,可以拿来直接采用,也可以增加一些实践,或者对其中的一些实践进行修改后再采用。

2.3 参考文献

[ARC97]Alistair Cockburn,“The Methodology Space,”Humans and Technology,technical report HaT TR.97.03 (dated 97.10.03),http://members.aol.com/acockburn/papers/methyspace/methyspace.htm.

[Beck99]Kent Beck,Extreme Programming Explained:Embrace Change,Addison-Wesley,1999.

[Beck2003]Kent Beck,Test-Driven Development by Example,Addison-Wesley,2003.

[Cockburn2001] Alistair Cockburn and Laurie Williams,“The Costs and Benefits of Pair Programming,”XP2000 Conference in Sardinia,reproduced in Giancarlo Succi and Michele Marchesi,Extreme Programming Examined,Addison-Wesley,2001.

[DRC98]Daryl R.Conner,Leading at the Edge of Chaos,Wiley,1998.

[EWD72] D.J.Dahl,E.W.Dijkstra,and C.A.R.Hoare,Structured Programming,Academic Press,1972.

[Fowler99]Martin Fowler,Refactoring:Improving the Design of Existing Code,Addison-Wesley,1999.

[Newkirk2001] James Newkirk and Robert C.Martin,Extreme Programming in Practice,Addison-Wesley,2001.

[Nosek98]J.T.Nosek“,The Case for Collaborative Programming,”Communications of the ACM,1998,pp.105-108.

[Williams2000] Laurie Williams,Robert R.Kessler,Ward Cunningham,Ron Jeffries,“Strengthening the Case for Pair Programming,”IEEE Software,July-Aug.2000.

第6章 一次编程实践

设计和编程都是人的活动。忘记了这一点,将会失去一切。

——Bjarne Stroustrup,1991,C++之父

为了演示一下敏捷编程实践,Bob Koss(RSK)和Bob Martin(RCM)将在一个小型的应用程序中使用结对编程(pair programming)的方法,你可以在一边观看。在创建该应用程序的过程中,会使用测试驱动开发以及大量的重构。接下来的一幕是这两个Bob于2000年末在一家旅馆中实际编程情景的真实再现。

在创建这个程序的过程中,我们犯了很多的错误,有些是代码方面的,有些是逻辑方面的,有些是设计方面的,还有些是需求方面的。在学习本章时,会看到我们围绕所有这些方面所进行的活动:识别出错误和误解,然后对它们进行处理。

过程是混乱的——过程中只要有人参与都是这样。结果……唔,令人吃惊,竟然能够从这样一个混乱的过程中重建秩序。

这个程序是计算保龄球比赛得分的,所以如果知道保龄球比赛的规则,会有助于理解本章内容。如果对保龄球比赛的规则不了解的话,可以察看本章末尾的规则简介。

6.1 保龄球比赛

{

RCM:可以帮忙编写一个保龄球记分小程序吗?

RSK:(自言自语,“XP中结对编程的实践规定:当有人请求帮助时不能够说‘不’。若请求的人是你的老板,就更不能拒绝了。”)当然可以,Bob,非常高兴帮助你。

RCM:太好了,我想编写一个应用程序来记录一届保龄球联赛。需要记录下所有的比赛,确定团队的等级,确定每次周赛的优胜者和失败者,并且准确地记录每场比赛的成绩。

RSK:棒极了。我曾经是个很好的保龄球选手。这件事情很有趣。你已经列出了一些用户故事,想先做哪一个呢?

RCM:先来实现记录一场比赛成绩的功能吧。

RSK:好。它指的是什么呢?该故事的输入和输出是什么呢?

RCM:在我看来,输入只是一个投球(throw)的序列。一次投球仅仅是一个整数,表明了此次投球所击倒的木瓶数目。输出就是每一轮(frame)的得分。

RSK:如果你在这个练习中扮演客户的角色,会希望什么形式的输入和输出呢?

RCM:好,我扮演客户。我们需要一个函数,调用它可以添加投球,还需要另外的函数用来获取得分。有点像下面的样子:

ThrowBall(6);

ThrowBall(3);

Assert.AreEqual(9,GetScore());

RSK:好,我们需要一些测试数据。我来画一张记分卡的小草图(参见图6-1)。

RCM:这名选手发挥得很不稳定。

RSK:或许喝醉了,但是可以作为一个相当好的验收测试用例。

RCM:我们还需要其他的验收测试用例,稍后再考虑吧。该如何开始呢?要做一个系统设计吗?

RSK:我不介意用UML图来说明从记分卡中得到的一些问题领域概念。从中会发现一些候选对象,可以在随后编码时使用。

RCM:(戴上他那顶强大的对象设计者的帽子)好,显然,Game对象由一系列共10个Frame对象组成,每个Frame对象可以包含1个、2个或者3个Throw对象。

RSK:好主意。这也正是我所想的。我立刻把它画出来(参见图6-2)。

RSK:好,来选取一个要测试的类。从依赖关系链的尾部开始,依次往后如何?这样会容易测试一些。

RCM:当然可以。我们来创建Throw类的测试用例。

RSK:(开始键入代码)

//ThrowTest.cs--------------------------------

using NUnit.Framework;

[TestFixture]

public class ThrowTest

{

[Test]

public void Test???

}

RSK:Throw对象应该具有什么行为呢?

RCM:它保存着比赛者所击倒的木瓶数。

RSK:好,你只用了寥寥数语,可见它确实没做什么事情。也许我们应该重新审视一下,来关注具有实际行为的对象,而不是仅仅存储数据的对象。

RCM:嗯,你的意思是说实际上Throw这个类也许不必存在?

RSK:是的,如果它不具有任何行为,能有多重要呢?我还不知道它是否应该存在。我只是觉得如果我们去关注那些不仅仅只有设置方法和获取方法的对象的话,会更有效率。但是如果你想控制的话……(将键盘推到RCM面前。)

RCM:好吧,我们上移至依赖链上的Frame类,看看是否能在编写该类的测试用例时,完成Throw类。(把键盘推回给RSK。)

RSK:(想知道RCM是在通过这种方式将我引入一个死胡同来教育我呢,还是他确实同意我的观点)好,新的文件,新的测试用例。

//FrameTest.cs------------------------------------

using NUnit.Framework;

[TestFixture]

public class FrameTest

{

[Test]

public void Test???

}

RCM:嗯,这是第二次键入这样的代码了。现在,你想到一些针对Frame类的有趣的测试用例吗?

RSK:Frame类可以给出它的分数,每次投球击倒的木瓶数,以及是否为全中或者补中……

RCM:好,用代码来说明问题。

RSK:(键入)

//FrameTest.cs-----------------------------

using NUnit.Framework;

[TestFixture]

public class FrameTest

{

[Test]

public void TestScoreNoThrows()

{

Frame f = new Frame();

Assert.AreEqual(0,f.Score);

}

}

//Frame.cs---------------------------------------

public class Frame

{

public int Score

{

get { return 0; }

}

}

RCM:好,测试用例通过了,但是Score实际上是一个愚蠢的属性。如果向Frame中加入一次投球的话,它就会失败。所以我们来编写这样的测试用例,它会加入一些投球,然后检查得分。

//FrameTest.cs---------------------------------

[Test]

public void TestAddOneThrow()

{

Frame f = new Frame();

f.Add(5);

Assert.AreEqual(5,f.Score);

}

RCM:这不能编译通过。Frame类中没有Add方法。

RSK:我打赌如果你定义这个方法,就会编译通过 ;-)

RCM:(键入)

//Frame.cs---------------------------------------

public class Frame

{

public int Score

{

get { return 0 };

}

public void Add(Throw t)

{

}

}

RCM:(自言自语)这不可能编译通过的,因为还没有编写Throw类。

RSK:和我说说,Bob。在测试中传给Add方法的是一个整数,而该方法期望一个Throw对象。Add方法不能具有两种形式。在我们再次关注Throw类前,你能描绘一下Throw类的行为吗?

RCM:哦,我甚至都没有注意到我写的是f.Add(5)。我应该写f.add(new Throw(5)),但那太不优雅了。我真正想写的就是f.Add(5)。

RSK:先不管是否优雅,我们暂时把美学的考虑放到一边。你能描绘一下Throw对象的行为吗?是二进制表示吗,Bob?

RCM:101101011010100101。我不知道Throw是否具有一些行为。我现在觉得Throw就是int。不过,我们不必再考虑它了,因为我们可以让Frame.Add接受一个int。

RSK:那么我觉得这样做的根本原因就是简单。当出现问题时,可以使用一些更复杂的方法。

RCM:同意。

//Frame.cs---------------------------------------

public class Frame

{

public int Score

{

get { return 0};

}

public void Add(int pins)

{

}

}

RCM:好,编译通过而测试失败了。现在,我们来让测试通过。

//Frame.cs---------------------------------------

public class Frame

{

private int score;

public int Score

{

get { return score; }

}

public void Add(int pins)

{

score += pins;

}

}

RCM:编译和测试都通过了,这明显太简单了。下一个测试用例是什么?

RSK:先休息一会儿好吗?

[------------------------------休息中----------------------------]

RCM:不错。但Frame.Add是一个脆弱的函数。如果用11作为参数去调用它会怎样呢?

RSK:如果发生这种情况,会引发异常。但是谁会去调用它呢?这个程序会成为数千人使用的应用程序框架以至于我们必须防备这种情况,还是仅仅被你一人使用呢?如果是后者,只要调用它时不传入11就没问题了。(暗笑)

RCM:好主意,系统的其他测试会捕获无效的参数。如果我们遇到麻烦,再把这个检查加进来也不迟。目前,Add函数还不能处理全中和补中的情况。我们编写一个测试用例来说明这种情况。

RSK:嗯。如果调用Add(10)来表示一个全中,那么GetScore()应该返回什么值呢?我不知道该如何写这个断言,也许我们提出的问题是错误的。也就是,我们选择提问的对象是错误的。

RCM:如果调用了Add(10),或者调用了Add(3)后又调用了Add(7),那么随后调用Frame的Score方法是没有意义的。当前的Frame对象必须要根据随后几个Frame实例的得分才能计算自己的得分。如果后面的Frame实例还不存在,那么它会返回一些令人讨厌的值,像−1。我不希望返回−1。

RSK:是的,我也不喜欢返回−1这个想法。你刚刚引入了一个概念,就是Frame之间要互相知晓。谁持有这些不同的Frame对象呢?

RCM:Game对象。

RSK:那么Game依赖于Frame,而Frame反过来又依赖于Game。我不喜欢这样。

RCM:Frame不必依赖于Game,可以把它们放置在一个链表中。每个Frame持有指向它前面以及后面Frame的指针。要获取一个Frame的得分,该Frame会获取前一个Frame的得分;如果该Frame中有补中或者全中的情况,它会从后面Frame中获取所需的得分。

RSK:好的,不过不太形象,我感觉有些不清楚。写一些代码看看吧。

RCM:好。我们首先要编写一个测试用例。

RSK:是针对Game呢,还是另一个针对Frame的呢?

RCM:我认为应该针对Game,因为是Game构建了Frame并把它们互相连接起来。

RSK:你是想停下我们正在做的有关Frame的工作跳到Game上,还是只想要一个MockGame对象来完成Frame正常运转所需要的工作呢?

RCM:我们停止在Frame上的工作,转到Game上来吧。Game的测试用例应当可以证明我们需要Frame链表。

RSK:我不知道它们是怎样证明的。我需要代码。

RCM:(键入代码)

//GameTest.cs------------------------------------------

using NUnit.Framework;

[TestFixture]

public class GameTest

{

[Test]

public void TestOneThrow()

{

Game game = new Game();

game.Add(5);

Assert.AreEqual(5,game.Score);

}

}

RCM:看上去合理吗?

RSK:当然合理,但是我仍然在寻找需要Frame链表的证据。

RCM:我也是,我们继续使用这些测试用例,看看会有什么结果。

//Game.cs----------------------------------

public class Game

{

public int Score

{

get { return 0; }

}

public void Add(int pins)

{

}

}

RCM:好,编译通过,而测试失败。现在我们来让测试通过。

//Game.cs----------------------------------

public class Game

{

private int score;

public int Score

{

get { return score; }

}

public void Add(int pins)

{

score += pins;

}

}

RCM:测试通过了,很好。

RSK:是不错,但是我仍在寻找需要Frame对象链表的重要证据。最初就是它使我们认为需要Game。

RCM:是的,这也是我正在寻找的。我肯定一旦加进了有关补中和全中的测试用例,就必须得构建Frame,并把它们用链表链接在一起。但是在代码迫使这样做前,我不想这样做。

RSK:好主意。我们来继续逐步完成Game。编写另外一个关于有两次投球但没有补中的情况的测试怎么样?

RCM:好,这应该会立刻通过测试。我们来试试。

//GameTest.cs--------------------------------

[Test]

public void TestTwoThrowsNoMark()

{

Game game = new Game();

game.Add(5);

game.Add(4);

Assert.AreEqual(9,game.Score);

}

RCM:是的,这个测试通过了。现在我们来试一下有4次投球但没有补中和全中的情况。

RSK:嗯,这个测试也能通过。但这不是我所期望的,我们可以一直增加投球数,甚至根本不需要Frame。但是我们还不曾考虑补中或者全中的情况。也许到那时我们就会需要一个Frame。

RCM:这也是我正在考虑的。不管怎样,考虑一下这个测试用例:

//TestGame.cs------------------------------------------

[Test]

public void TestFourThrowsNoMark()

{

Game game = new Game();

game.Add(5);

game.Add(4);

game.Add(7);

game.Add(2);

Assert.AreEqual(18,game.Score);

Assert.AreEqual(9,game.ScoreForFrame(1));

Assert.AreEqual(18,game.ScoreForFrame(2));

}

RCM:这看上去合理吗?

RSK:当然合理。我忘了必须要能显示每轮的得分。啊,我把咱们画的记分卡草图当可乐杯垫来用了。对,这就是我忘记的原因。

RCM:(叹气)好,首先我们给Game加入scoreForFrame方法使测试失败。

//Game.cs----------------------------------

public int ScoreForFrame(int frame)

{

return 0;

}

RCM:好极了,编译通过,测试失败了,现在,怎样通过测试呢?

RSK:我们可以定义Frame对象了,但这是通过测试的最简单方法吗?

RCM:不是,事实上,我们只需要在Game中简单地创建一个整数数组。每次对Add的调用都会在这个数组里添加一个新的整数。每次对ScoreForFrame的调用只需要前向遍历这个数组并计算出得分。

//Game.cs----------------------------------

public class Game

{

private int score;

private int[] throws = new int[21];

private int currentThrow;

public int Score

{

get { return score; }

}

public void Add(int pins)

{

throws[currentThrow++] = pins;

score += pins;

}

public int ScoreForFrame(int frame)

{

int score = 0;

for(int ball = 0;

frame < 0 && ball < currentThrow;

ball+=2,frame--)

{

score += throws[ball] + throws[ball + 1];

}

return score;

}

}

RCM:(对自己很满意)看,可以工作了。

RSK:为什么要用21这个魔数(magic number)呢?

RCM:它表示一场比赛中最多可能的投球数。

RSK:讨厌。让我猜猜,你年轻时,是一个UNIX黑客(hacker),并自豪于把整个应用程序编写成没人能够理解的一条语句。

需要重构scoreForFrame,这样可以更好地理解它。但是在考虑重构前,我来问另外一个问题。Game是放置这个方法的最好地方吗?我认为Game违反了单一职责原则。[请参见第8章] 它接收投球并且知道如何计算每轮的得分。你觉得增加一个Scorer对象如何?

RCM:(粗鲁地摆了一下手) 目前我还不知道这个函数该放在哪里,现在我感兴趣的只是让记分程序工作起来。完成所需的功能后,然后我们再来讨论SRP的价值。不过,我明白你所说的UNIX黑客指的是什么。我们尝试来简化这个循环。

public int ScoreForFrame(int theFrame)

{

int ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

score += throws[ball++] + throws[ball++];

}

return score;

}

RCM:好了一点,但是score+=这个表达式具有副作用。不过,这个表达式中两个加数表达式的求值顺序无关紧要,所以这里不会造成问题。(是这样吗?两个增量操作会不会在某一个数组运算前完成呢?)

RSK:我认为可以做个实验来证明这里不会有任何副作用,但是这个函数还不能处理补中和全中的情况。我们是应该继续使它更易读些呢,还是应该给它添加更多的功能呢?

RCM:实验可能只对特定的编译器有意义。其他的编译器可能采用不同的求值顺序。我不知道这是不是一个问题,但是我们还是先来去除这种可能的顺序依赖,然后再编写更多的测试用例来添加功能。

public int ScoreForFrame(int theFrame)

{

int ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

int firstThrow = throws[ball++];

int secondThrow = throws[ball++];

score += firstThrow + secondThrow;

}

return score;

}

RSK:好,下个测试用例。我们来试试补中的情况。

[Test]

public void TestSimpleSpare()

{

Game game = new Game();

}

RCM:我已经厌倦总是写这个了,我们来重构一下测试,把Game对象的创建放到SetUp函数中吧。

//GameTest.cs--------------------------------

using NUnit.Framework;

[TestFixture]

public class GameTest

{

private Game game;

[SetUp]

public void SetUp()

{

game = new Game();

}

[Test]

public void TestOneThrow()

{

game.Add(5);

Assert.AreEqual(5,game.Score);

}

[Test]

public void TestTwoThrowsNoMark()

{

game.Add(5);

game.Add(4);

Assert.AreEqual(9,game.Score);

}

[Test]

public void TestFourThrowsNoMark()

{

game.Add(5);

game.Add(4);

game.Add(7);

game.Add(2);

Assert.AreEqual(18,game.Score);

Assert.AreEqual(9, game.ScoreForFrame(1));

Assert.AreEqual(18,game.ScoreForFrame(2));

}

[Test]

public void TestSimpleSpare()

{

}

}

RCM:好多了,现在来编写关于补中的测试用例。

[Test]

public void TestSimpleSpare()

{

game.Add(3);

game.Add(7);

game.Add(3);

Assert.AreEqual(13,game.ScoreForFrame(1));

}

RCM:好,测试失败了,现在我们要让它通过。

RSK:我来写吧。

public int ScoreForFrame(int theFrame)

{

int ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

int firstThrow = throws[ball++];

int secondThrow = throws[ball++];

int frameScore = firstThrow + secondThrow;

// 分瓶时需要知道后面一轮的第一投

if ( frameScore == 10 )

score += frameScore + throws[ball++];

else

score += frameScore;

}

return score;

}

RSK:啊哈,可以工作了。

RCM:(抢过键盘)不错,但是我认为在frameScore==10时不应该对变量ball递增。有个测试用例可以证明我的观点。

[Test]

public void TestSimpleFrameAfterSpare()

{

game.Add(3);

game.Add(7);

game.Add(3);

game.Add(2);

Assert.AreEqual(13,game.ScoreForFrame(1));

Assert.AreEqual(18,game.Score);

}

RCM:哈,看,测试失败了。现在,如果把这个讨厌的额外递增操作去掉……

if ( frameScore == 10 )

score += frameScore + throws[ball];

RCM:呃,测试还是失败了。是Score方法错了吗?把测试用例改为使用ScoreForFrame(2)试试。

[Test]

public void TestSimpleFrameAfterSpare()

{

game.Add(3);

game.Add(7);

game.Add(3);

game.Add(2);

Assert.AreEqual(13,game.ScoreForFrame(1));

Assert.AreEqual(18,game.ScoreForFrame(2));

}

RCM:唔。测试通过了。肯定是Score属性出问题了。我们来看一下。

public int Score

{

get { return score; }

}

public void Add(int pins)

{

throws[currentThrow++] = pins;

score += pins;

}

RCM:是的,是错了。score属性只是返回木瓶数的和,而不是正确的得分。我们要让Score做的是用当前轮作为参数去调用ScoreForFrame()。

RSK:我们不知道当前是哪轮。我们来把这个信息加到现有的每个测试中,当然,每次一个。

RCM:好的。

//GameTest.cs--------------------------------

[Test]

public void TestOneThrow()

{

game.Add(5);

Assert.AreEqual(5,game.Score);

Assert.AreEqual(1,game.CurrentFrame);

}

//Game.cs----------------------------------

public int CurrentFrame

{

get { return 1; }

}

RCM:不错,可以工作了。但是没什么意义。我们完成下一个测试用例。

[Test]

public void TestTwoThrowsNoMark()

{

game.Add(5);

game.Add(4);

Assert.AreEqual(9,game.Score);

Assert.AreEqual(1,game.CurrentFrame);

}

RCM:同样无趣,再来下一个。

[Test]

public void TestFourThrowsNoMark()

{

game.Add(5);

game.Add(4);

game.Add(7);

game.Add(2);

Assert.AreEqual(18,game.Score);

Assert.AreEqual(9,game.ScoreForFrame(1));

Assert.AreEqual(18,game.ScoreForFrame(2));

Assert.AreEqual(2,game.CurrentFrame);

}

RCM:这一个失败了。现在我们来让它通过。

RSK:我认为算法很简单,因为每轮有两次投球,所以只要将投球数除以2就可以了。除非有全中的情况。但是我们至今还没有考虑过全中的情况,暂且还是忽略了吧。

RCM:(在+1和-1之间胡乱调整,直到可以正常工作)[33]

public int CurrentFrame

{

get { return 1 + (currentThrow - 1) / 2; }

}

RCM:这不太令人满意。

RSK:如果不是每次都去计算它,会怎么样呢?如果每次投球后去调整currentFrame成员变量,会怎么样呢?

RCM:不错,我们来试试。

//Game.cs----------------------------------

private int currentFrame;

private bool isFirstThrow = true;

public int CurrentFrame

{

get { return currentFrame; }

}

public void Add(int pins)

{

throws[currentThrow++] = pins;

score += pins;

if (isFirstThrow)

{

isFirstThrow = false;

currentFrame++;

}

else

{

isFirstThrow=true;;

}

}

RCM:好,可以工作了。但是这也意味着当前轮指的是最近一次投球所在轮,而不是下一次投球所在轮。只要我们记住这一点,就没有问题。

RSK:我没有这么好的记忆力,我们来把程序修改得更易读些。但是在调整前,我们先把代码从Add()中提取出来,放到一个称为AdjustCurrentFrame ()或者其他名字的私有成员函数中。

RCM:好,听起来不错。

public void Add(int pins)

{

throws[currentThrow++] = pins;

score += pins;

AdjustCurrentFrame();

}

private void AdjustCurrentFrame()

{

if (isFirstThrow)

{

isFirstThrow = false;

currentFrame++;

}

else

{

isFirstThrow=true;;

}

}

RCM:现在,我们把变量和函数的名字改得更清晰些。我们该如何称呼currentFrame呢?

RSK:我挺喜欢这个名字。但我认为对它递增的位置不对。在我看来,当前轮是正在进行的投球所在轮。所以应该在该轮最后一次投球完毕后,才对它递增。

RCM:我同意。我们来修改测试用例以体现这一点,然后再去修正AdjustCurrentFrame。

//GameTest.cs--------------------------------

[Test]

public void TestTwoThrowsNoMark()

{

game.Add(5);

game.Add(4);

Assert.AreEqual(9,game.Score);

Assert.AreEqual(2,game.CurrentFrame);

}

[Test]

public void TestFourThrowsNoMark()

{

game.Add(5);

game.Add(4);

game.Add(7);

game.Add(2);

Assert.AreEqual(18,game.Score);

Assert.AreEqual(9, game.ScoreForFrame(1));

Assert.AreEqual(18,game.ScoreForFrame(2));

Assert.AreEqual(3,game.CurrentFrame);

}

//Game.cs----------------------------------

private int currentFrame = 1;

private void AdjustCurrentFrame()

{

if (isFirstThrow)

{

isFirstThrow = false;

}

else

{

isFirstThrow=true;

currentFrame++;

}

}

RCM:不错,可以工作了。现在我们来为CurrentFrame编写两个具有补中情况的测试用例。

[Test]

public void TestSimpleSpare()

{

game.Add(3);

game.Add(7);

game.Add(3);

Assert.AreEqual(13,game.ScoreForFrame(1));

Assert.AreEqual(2,game.CurrentFrame);

}

[Test]

public void TestSimpleFrameAfterSpare()

{

game.Add(3);

game.Add(7);

game.Add(3);

game.Add(2);

Assert.AreEqual(13,game.ScoreForFrame(1));

Assert.AreEqual(18,game.ScoreForFrame(2));

Assert.AreEqual(3,game.CurrentFrame);

}

RCM:通过了。现在,回到原先的问题上。我们要让Score能够工作。现在可以让score去调用

ScoreForFrame (CurrentFrame -1)。

[Test]

public void TestSimpleFrameAfterSpare()

{

game.Add(3);

game.Add(7);

game.Add(3);

game.Add(2);

Assert.AreEqual(13,game.ScoreForFrame(1));

Assert.AreEqual(18,game.ScoreForFrame(2));

Assert.AreEqual(18,game.Score);

Assert.AreEqual(3,game.CurrentFrame);

}

//Game.cs----------------------------------

public int Score()

{

return ScoreForFrame(CurrentFrame - 1);

}

RCM:TestOneThrow测试用例失败了,我们来看看。

[Test]

public void TestOneThrow()

{

game.Add(5);

Assert.AreEqual(5,game.Score);

Assert.AreEqual(1,game.CurrentFrame);

}

RCM:只有一次投球,第1轮是不完整的。score方法调用了ScoreForFrame (0)。这真讨厌。

RSK:也许是,也许不是。这个程序是写给谁的?谁会去调用Score呢?假定不会针对不完整轮调用该方法合理吗?

RCM:是的,但是它让我觉得不舒服。为了解决这个问题,我们要从TestOneThrow测试用例中去掉Score?这是我们要做的吗?

RSK:可以这样做,甚至可以去掉整个TestOneThrow测试用例。它用来把我们引到所关心的测试用例上。但现还有实际用处吗?在所有其他测试用例中都涉及这个问题。

RCM:是的,我明白你的意思。好,去掉它。(编辑代码,运行测试,出现绿色的指示条)啊,很好。

现在最好来关注关于全中的测试用例。毕竟,我们想看到所有这些Frame对象被构建成一个链表,不是吗?(窃笑)

[Test]

public void TestSimpleStrike()

{

game.Add(10);

game.Add(3);

game.Add(6);

Assert.AreEqual(19,game.ScoreForFrame(1));

Assert.AreEqual(28,game.Score);

Assert.AreEqual(3,game.CurrentFrame);

}

RCM:好,不出所料,编译通过,测试失败了。现在要通过测试。

//Game.cs----------------------------------

public class Game

{

private int score;

private int[] throws = new int[21];

private int currentThrow;

private int currentFrame = 1;

private bool isFirstThrow = true;

public int Score

{

get { return ScoreForFrame(GetCurrentFrame() - 1); }

}

public int CurrentFrame

{

get { return currentFrame; }

}

public void Add(int pins)

{

throws[currentThrow++] = pins;

score += pins;

AdjustCurrentFrame(pins);

}

private void AdjustCurrentFrame(int pins)

{

if (isFirstThrow)

{

if(pins == 10) //全中

currentFrame++;

else

isFirstThrow = false;

}

else

{

isFirstThrow=true;

currentFrame++;

}

}

public int ScoreForFrame(int theFrame)

{

int ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

int firstThrow = throws[ball++];

if(firstThrow == 10) //全中

{

score += 10 + throws[ball] + throws[ball+1];

}

else

{

int secondThrow = throws[ball++];

int frameScore = firstThrow + secondThrow;

// 分瓶时需要知道下轮的第一投

if ( frameScore == 10 )

score += frameScore + throws[ball];

else

score += frameScore;

}

}

return score;

}

}

RCM:不错,不是特别难,我们来看看能否为满分比赛记分。

[Test]

public void TestPerfectGame()

{

for (int i=0; i<12; i++)

{

game.Add(10);

}

Assert.AreEqual(300,game.Score);

Assert.AreEqual(10,game.CurrentFrame);

}

RCM:奇怪,它说得分是330。怎么会是这样?

RSK:因为当前轮一直累加到了12。

RCM:噢!要把它限定到10。

private void AdjustCurrentFrame(int pins)

{

if (isFirstThrow)

{

if(pins == 10) //全中

currentFrame++;

else

isFirstThrow = false;

}

else

{

isFirstThrow=true;

currentFrame++;

}

if(currentFrame > 10)

currentFrame = 10;

}

RCM:该死,这次它说得分是270。出什么问题了?

RSK:Bob,Score属性把GetCurrentFrame减了1,所以它给出的是第9轮的得分,而不是第10轮的。

RCM:什么,你是说,应该把当前轮限定到11而不是10?我再试试。

if(currentFrame > 11)

currentFrame = 11;

RCM:好,现在得到了正确的得分,但是却因为当前轮是11,不是10而失败了。烦人!当前轮真是个难办的事情。我们希望当前轮指的是比赛者正在进行的投球所在的轮,但是在比赛结束时,这意味着什么呢?

RSK:也许我们应当回到原先的观点,认为当前轮指的是最后一次投球所在轮。

RCM:或者,我们也许要提出最近已完成轮这样一个概念?毕竟,在任何时间点上比赛的得分都是最近已完成轮的得分。

RSK:一个已完成轮指的是可以为之计算得分的轮,对吗?

RCM:是的,如果一轮中有补中的情况,那么要在下一个球投出后该轮才算完成。如果一轮中有全中的情况,那么要在下两个球投出后该轮才算完成。如果一轮中没有上述两种情况出现,那么该轮中第二球投出后就算完成了。

等一会。我们正尝试使Score属性可以工作,对吗?我们所需要做的就是在比赛结束时让Score调用ScoreForFrame (10)。

RSK:你怎么知道比赛结束了呢?

RCM:如果AdjustCurrentFrame对currentFrame的增加超过10,那么比赛就是结束了。

RSK:等等。你的意思是说如果CurrentFrame返回了11,比赛就算结束了。可程序现在就是这样做的呀!

RCM:嗯。你的意思是我们应该修改测试用例,使它和程序一致?

[Test]

public void TestPerfectGame()

{

for (int i=0; i<12; i++)

{

game.Add(10);

}

Assert.AreEqual(300,game.Score);

Assert.AreEqual(11,game.CurrentFrame);

}

RCM:不错,通过了。可是我还是觉得不太舒服。

RSK:也许后面后有好的方法。现在,我发现了一个bug。我来吧。(抢过键盘。)

[Test]

public void TestEndOfArray()

{

for (int i=0; i<9; i++)

{

game.Add(0);

game.Add(0);

}

game.Add(2);

game.Add(8); // 第10轮分瓶

game.Add(10); // 最后一投全中

Assert.AreEqual(20,game.Score);

}

RSK:嗯。没有失败。我以为既然数组的第21个元素是一个全中,计分程序会试图把数组的第22个和第23个元素加进去。但是我想它可能没有这么做。

RCM:嗯,你还在想着记分对象,不是吗?无论如何,我都明白你的意思,但是由于score决不会用大于10的参数去调用ScoreForFrame,所以这最后一次全中实际上没有作为全中处理。只是为了最后一轮中补中的完整性才把它作为10分计算的。我们决不会越过数组的边界。

RSK:好的,我们来把原先记分卡上的数据输入到程序中。

[Test]

public void TestSampleGame()

{

game.Add(1);

game.Add(4);

game.Add(4);

game.Add(5);

game.Add(6);

game.Add(4);

game.Add(5);

game.Add(5);

game.Add(10);

game.Add(0);

game.Add(1);

game.Add(7);

game.Add(3);

game.Add(6);

game.Add(4);

game.Add(10);

game.Add(2);

game.Add(8);

game.Add(6);

Assert.AreEqual(133,game.Score);

}

RSK:不错,测试通过了。你还能想到其他的测试用例吗?

RCM:是的,我们来多测试一些边界情况。这个如何?一个可怜的家伙投出了11次全中,而最后一次仅击中了9个。

[Test]

public void TestHeartBreak()

{

for (int i=0; i<11; i++)

game.Add(10);

game.Add(9);

Assert.AreEqual(299,game.Score);

}

RCM:通过了。好的,再来测试一下第10轮是补中的情况如何?

[Test]

public void TestTenthFrameSpare()

{

for (int i=0; i<9; i++)

game.Add(10);

game.Add(9);

game.Add(1);

game.Add(1);

Assert.AreEqual(270,game.Score);

}

RCM:(高兴地盯着绿色的指示条)也通过了。我再也想不出更多的测试用例了,你呢?

RSK:我也想不出了,我认为已经覆盖了所有的情况。此外,我实在想重构这个混乱的程序。我还是认为应该有scorer对象。

RCM:是的,ScoreForFrame函数确实很乱,我们来看一下。

public int ScoreForFrame(int theFrame)

{

int ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

int firstThrow = throws[ball++];

if(firstThrow == 10) //全中

{

score += 10 + throws[ball] + throws[ball+1];

}

else

{

int secondThrow = throws[ball++];

int frameScore = firstThrow + secondThrow;

// 分瓶时需要知道下一轮的第一投

if ( frameScore == 10 )

score += frameScore + throws[ball];

else

score += frameScore;

}

}

return score;

}

RCM:我很想把else子句的实现体提取出来作为一个名为HandleSecondThrow的单独方法,但是因为它使用了ball、firstThrow以及secondThrow这些局部变量,所以不行。

RSK:我们可以把这些局部变量变为成员变量。

RCM:是的,这对你认为的把记分部分剥离出来放到它自己的scorer对象中去的看法又多了几分支持。好,我们来试试。

RSK:(抢过键盘)

private int ball;private int firstThrow;

private int secondThrow;

public int ScoreForFrame(int theFrame)

{

ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

firstThrow = throws[ball++];

if(firstThrow == 10) //全中

{

score += 10 + throws[ball] + throws[ball+1];

}

else

{

secondThrow = throws[ball++];

int frameScore = firstThrow + secondThrow;

// 分瓶时需要知道下一轮的第一投

if ( frameScore == 10 )

score += frameScore + throws[ball];

else

score += frameScore;

}

}

return score;

}

RSK:可以工作。这样就可以把else子句剥离到它自己的函数中去。

public int ScoreForFrame(int theFrame)

{

ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

firstThrow = throws[ball++];

if(firstThrow == 10) //全中

{

score += 10 + throws[ball] + throws[ball+1];

}

else

{

score += HandleSecondThrow();

}

}

return score;

}

private int HandleSecondThrow()

{

int score = 0;

secondThrow = throws[ball++];

int frameScore = firstThrow + secondThrow;

// 分瓶时需要知道下一轮的第一投

if ( frameScore == 10 )

score += frameScore + throws[ball];

else

score += frameScore;

return score;

}

RCM:看一下ScoreForFrame方法的结构!用伪代码来描述,看起来像这样:

if 全中

score += 10 + 下两球击倒的瓶数;

else

HandleSecondThrow.

RCM:如果把它改成下面的形式会怎样?

if 全中

score += 10 +下两球击倒的瓶数;

else if 分瓶

score += 10 +下一球击倒的瓶数;

else

score += 本轮两球击倒的瓶数;

RSK:好极了!这正是保龄球的记分规则,不是吗?好的,我们来看看能否在实际的函数中实现这个结构。首先,我们来改变一下增加ball变量的方式,使得在上面三种情况中可以独立地对它进行操作。

public int ScoreForFrame(int theFrame)

{

ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

firstThrow = throws[ball];

if(firstThrow == 10) //全中

{

ball++;

score += 10 + throws[ball] + throws[ball+1];

}

else

{

score += HandleSecondThrow();

}

}

return score;

}

private int HandleSecondThrow()

{

int score = 0;

secondThrow = throws[ball + 1];

int frameScore = firstThrow + secondThrow;

// 分瓶时需要知道下一轮的第一投

if ( frameScore == 10 )

{

ball += 2;

score += frameScore + throws[ball];

}

else

{

ball += 2;

score += frameScore;

}

return score;

}

RCM:(抢过键盘)好,现在我们来去掉firstThrow和secondThrow变量,并用适当的函数来替代它们。

public int ScoreForFrame(int theFrame)

{

ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

firstThrow = throws[ball];

if(Strike())

{

ball++;

score += 10 + NextTwoBalls;

}

else

{

score += HandleSecondThrow();

}

}

return score;

}

private bool Strike()

{

return throws[ball] == 10;

}

private int NextTwoBalls

{

get { return (throws[ball] + throws[ball+1]); }

}

RCM:这一步成功了。继续。

private int HandleSecondThrow()

{

int score = 0;

secondThrow = throws[ball + 1];

int frameScore = firstThrow + secondThrow;

// 分瓶时需要知道下一轮的第一投

if (Spare())

{

ball += 2;

score += 10 + NextBall;

}

else

{

ball += 2;

score += frameScore;

}

return score;

}

private bool Spare()

{

return throws[ball] + throws[ball+1] == 10;

}

private int NextBall

{

get { return throws[ball]; }

}

RCM:好,也成功了。现在来处理frameScore。

private int HandleSecondThrow()

{

int score = 0;

secondThrow = throws[ball + 1];

int frameScore = firstThrow + secondThrow;

// 分瓶时需要知道下一轮的第一投

if ( IsSpare() )

{

ball += 2;

score += 10 + NextBall;

}

else

{

score += TwoBallsInFrame;

ball += 2;

}

return score;

}

private int TwoBallsInFrame

{

get { return throws[ball] + throws[ball+1]; }

}

RSK:Bob,你没有用一致的方式去增加变量ball。在补中和全中的情况中,你是在记分前去增加它的。而在调用TwoBallsInFrame的情况中,你却在记分后增加它。这样代码就依赖于这个顺序了!怎么回事?

RCM:对不起,我本应当解释一下的。我打算把这个增量操作移到Strike、Spare和TwoBallsInFrame中去。这样的话,它们就会从ScoreForFrame方法中消失,并且该方法看上去就很像伪码形式了。

RSK:好,再让你做几步。不过要记住,我可看着呢。

RCM:好的,现在不会再使用firstThrow、secondThrow和frameScore了,可以把它们去掉了。

public int ScoreForFrame(int theFrame)

{

ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

if(Strike())

{

ball++;

score += 10 + NextTwoBalls;

}

else

{

score += HandleSecondThrow();

}

}

return score;

}

private int HandleSecondThrow()

{

int score = 0;

// 分瓶时需要知道下一轮的第一投

if ( Spare() )

{

ball += 2;

score += 10 + NextBall;

}

else

score += TwoBallsInFrame;

ball += 2;

}

return score;

}

RCM:(从他的眼神可以看出出现了绿色的指示条)现在,因为唯一耦合这3种情况的变量是ball,而ball在每种情况下都是独立处理的,所以可以把这3种情况合并在一起。

public int ScoreForFrame(int theFrame)

{

ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

if(Strike())

{

ball++;

score += 10 + NextTwoBalls;

}

else if ( Spare() )

{

ball += 2;

score += 10 + NextBall;

}

else

{

score += TwoBallsInFrame;

ball += 2;

}

}

return score;

}

RSK:好,现在可以使ball增加的方式一致,并重新为该函数起一个更清楚一些的名称。(抢过键盘)

public int ScoreForFrame(int theFrame)

{

ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

if(Strike())

{

score += 10 + NextTwoBallsForStrike;

ball++;

}

else if ( Spare() )

{

score += 10 + NextBallForSpare;

ball += 2;

}

else

{

score += TwoBallsInFrame;

ball += 2;

}

}

return score;

}

private int NextTwoBallsForStrike

{

get { return (throws[ball+1] + throws[ball+2]); }

}

private int NextBallForSpare

{

get { return throws[ball+2]; }

}

RCM:看一下ScoreForFrame方法!这正是保龄球记分规则的最简洁描述。

RSK:但是,Bob,Frame对象的链表呢?(窃笑,窃笑)

RCM:(叹气)我们被过度图示设计的恶魔迷惑了。我的天,3个画在餐巾纸背面的小方框,Game、Frame,还有Throw,看来还是太复杂了,并且是完全错误的。

RSK:以Throw类开始就是错误的。应该先从Game类开始!

RCM:确实是这样!所以,下次我们试着从最高层开始往下进行。

RSK:(喘气)自上而下设计!??!?!?

RCM:更正一下,是自上而下测试优先设计。坦白地说,我不知道这是不是一个好的规则。只是这次,它帮了我们。所以下次,我会再次尝试看看会发生什么。

RSK:是的,无论如何,我们仍然还要做些重构。ball变量只是ScoreForFrame和它的附属方法的一个私有的迭代器。它们都应当被移到另外一个对象中去。

RCM:哦,是的,就是你所说的Scorer对象。终究还是你对了。我们来完成这项工作。

RSK:(抢过键盘,进行了几个小规模的更改,期间进行了一些测试)

//Game.cs----------------------------------

public class Game

{

private int score;

private int currentFrame = 1;

private bool isFirstThrow = true;

private Scorer scorer = new Scorer();

public int Score

{

get { return ScoreForFrame(GetCurrentFrame() - 1); }

}

public int CurrentFrame

{

get { return currentFrame; }

}

public void Add(int pins)

{

scorer.AddThrow(pins);

score += pins;

AdjustCurrentFrame(pins);

}

private void AdjustCurrentFrame(int pins)

{

if (isFirstThrow)

{

if(pins == 10) //全中

currentFrame++;

else

isFirstThrow = false;

}

else

{

isFirstThrow = true;

currentFrame++;

}

if(currentFrame > 11)

currentFrame = 11;

}

public int ScoreForFrame(int theFrame)

{

return scorer.ScoreForFrame(theFrame);

}

}

//Scorer.cs----------------------------------

public class Scorer

{

private int ball;

private int[] throws = new int[21];

private int currentThrow;

public void AddThrow(int pins)

{

throws[currentThrow++] = pins;

}

public int ScoreForFrame(int theFrame)

{

ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

if(Strike())

{

score += 10 + NextTwoBallsForStrike;

ball++;

}

else if ( Spare() )

{

score += 10 + NextBallForSpare;

ball += 2;

}

else

{

score += TwoBallsInFrame;

ball += 2;

}

}

return score;

}

private int NextTwoBallsForStrike

{

get { return (throws[ball+1] + throws[ball+2]); }

}

private int NextBallForSpare

{

get { return throws[ball+2]; }

}

private bool Strike()

{

return throws[ball] == 10;

}

private int TwoBallsInFrame

{

get { return throws[ball] + throws[ball+1]; }

}

private bool Spare()

{

return throws[ball] + throws[ball+1] == 10;

}

}

RSK:好多了。现在Game只知晓轮,而Scorer对象只计算得分。完全符合单一职责原则!

RCM:不管怎样,确实好多了。你注意到score变量已经不再使用了吗?

RSK:哈,你说的对。去掉它。(极为高兴地开始删除。)

public void Add(int pins)

{

scorer.AddThrow(pins);

AdjustCurrentFrame(pins);

}

RSK:不错。现在,我们可以整理AdjustCurrentFrame了吗?

RCM:可以。我们来看看。

private void AdjustCurrentFrame(int pins)

{

if (isFirstThrow)

{

if(pins == 10) //全中

currentFrame++;

else

isFirstThrow = false;

}

else

{

isFirstThrow = true;

currentFrame++;

}

if(currentFrame > 11)

currentFrame = 11;

}

RCM:好,首先把增量操作移到一个单独的函数中,并在该函数中把轮限定到11。(呵,我还是不喜欢那个11。)

RSK:Bob,11意味着游戏的结束。

RCM:是的。呵。(抓过键盘,做了些变动,其间也进行了一些测试。)

private void AdjustCurrentFrame(int pins)

{

if (isFirstThrow)

{

if(pins == 10) //全中

AdvanceFrame();

else

isFirstThrow = false;

}

else

{

isFirstThrow = true;

AdvanceFrame();

}

}

private void AdvanceFrame()

{

currentFrame++;

if(currentFrame > 11)

currentFrame = 11;

}

RCM:好了一点。现在我们来把关于全中情况的判断取出来作为一个独立的函数。(做了几步改进,每次都运行测试。)

private void AdjustCurrentFrame(int pins)

{

if (isFirstThrow)

{

if(AdjustFrameForStrike(pins) == false)

isFirstThrow = false;

}

else

{

isFirstThrow = true;

AdvanceFrame();

}

}

private bool AdjustFrameForStrike(int pins)

{

if(pins == 10)

{

AdvanceFrame();

return true;

}

return false;

}

RCM:真是不错,现在,来看看那个11。

RSK:你确实不喜欢它,是吗?

RCM:是的,看一下Score属性。

public int Score

{

get { return ScoreForFrame(GetCurrentFrame() - 1); }

}

RCM:这个-1怪怪的。我们只在该方法中使用了CurrentFrame,可是我们还得调整它的返回值。

RSK:该死,你是对的。我们在这上面反复多少次了?

RCM:太多了。但是现在好了。代码希望currentFrame表示的是最后一次投球所在轮,而不是将要进行的投球所在轮。

RSK:唉,这样会破坏很多测试用例。

RCM:事实上,我觉得可以把CurrentFrame从所有的测试用例中去掉,并把CurrentFrame函数本身也去掉。其实它是不会被用到的。

RSK:好的,我明白你的意思。我来完成它。这就像把一匹跛马从痛苦中解救出来一样。(抓过键盘)

//Game.cs----------------------------------

public int Score

{

get { return ScoreForFrame(currentFrame); }

}

private void AdvanceFrame()

{

currentFrame++;

if(currentFrame > 10)

currentFrame = 10;

}

RCM:哦。你是想说我们一直为之困扰,而我们所要做的就是把限制从11改到10,并且移走-1。天哪!

RSK:是的,Bob大叔,我们实在是不值得为之苦恼。

RCM:我讨厌AdjustFrameForStrike()中的副作用。我想去掉它。这样如何?

private void AdjustCurrentFrame(int pins)

{

if ((isFirstThrow && pins == 10) || (!isFirstThrow))

AdvanceFrame();

else

isFirstThrow = false;

}

RSK:我喜欢这个想法,也通过了测试,但是我不喜欢那个长长的if语句。这样如何?

private void AdjustCurrentFrame(int pins)

{

if (Strike(pins) || (!isFirstThrow))

AdvanceFrame();

else

isFirstThrow = false;

}

private bool Strike(int pins)

{

return (isFirstThrow && pins == 10);

}

RCM:是的,很好。甚至可以更进一步。

private void AdjustCurrentFrame(int pins)

{

if (LastBallInFrame(pins))

AdvanceFrame();

else

isFirstThrow = false;

}

private bool LastBallInFrame(int pins)

{

return Strike(pins) || (!isFirstThrow);

}

RSK:好极了!

RCM:好,看起来好像已经完成了。我们来浏览一下整个程序,看看是否已经做到了尽可能的简单和易于理解。

//Game.cs----------------------------------

public class Game

{

private int currentFrame = 0;

private bool isFirstThrow = true;

private Scorer scorer = new Scorer();

public int Score

{

get { return ScoreForFrame(currentFrame); }

}

public void Add(int pins)

{

scorer.AddThrow(pins);

AdjustCurrentFrame(pins);

}

private void AdjustCurrentFrame(int pins)

{

if (LastBallInFrame(pins))

AdvanceFrame();

else

isFirstThrow = false;

}

private bool LastBallInFrame(int pins)

{

return Strike(pins) || (!isFirstThrow);

}

private bool Strike(int pins)

{

return (isFirstThrow && pins == 10);

}

private void AdvanceFrame()

{

currentFrame++;

if(currentFrame > 10)

currentFrame = 10;

}

public int ScoreForFrame(int theFrame)

{

return scorer.ScoreForFrame(theFrame);

}

}

//Scorer.cs----------------------------------

public class Scorer

{

private int ball;

private int[] throws = new int[21];

private int currentThrow;

public void AddThrow(int pins)

{

throws[currentThrow++] = pins;

}

public int ScoreForFrame(int theFrame)

{

ball = 0;

int score=0;

for (int currentFrame = 0;

currentFrame < theFrame;

currentFrame++)

{

if(Strike())

{

score += 10 + NextTwoBallsForStrike;

ball++;

}

else if ( Spare() )

{

score += 10 + NextBallForSpare;

ball += 2;

}

else

{

score += TwoBallsInFrame;

ball += 2;

}

}

return score;

}

private int NextTwoBallsForStrike

{

get { return (throws[ball+1] + throws[ball+2]); }

}

private int NextBallForSpare

{

get { return throws[ball+2]; }

}

private bool Strike()

{

return throws[ball] == 10;

}

private int TwoBallsInFrame

{

get { return throws[ball] + throws[ball+1]; }

}

private bool Spare()

{

return throws[ball] + throws[ball+1] == 10;

}

}

RCM:行,看起来确实不错。我想不出来还有什么需要做的。

RSK:是的,确实不错。为了保险起见,我们来查看一下测试代码。

//GameTest.cs--------------------------------

using NUnit.Framework;

[TestFixture]

public class GameTest

{

private Game game;

[SetUp]

public void SetUp()

{

game = new Game();

}

[Test]

public void TestTwoThrowsNoMark()

{

game.Add(5);

game.Add(4);

Assert.AreEqual(9,game.Score);

}

[Test]

public void TestFourThrowsNoMark()

{

game.Add(5);

game.Add(4);

game.Add(7);

game.Add(2);

Assert.AreEqual(18,game.Score);

Assert.AreEqual(9, game.ScoreForFrame(1));

Assert.AreEqual(18,game.ScoreForFrame(2));

}

[Test]

public void TestSimpleSpare()

{

game.Add(3);

game.Add(7);

game.Add(3);

Assert.AreEqual(13,game.ScoreForFrame(1));

}

[Test]

public void TestSimpleFrameAfterSpare()

{

game.Add(3);

game.Add(7);

game.Add(3);

game.Add(2);

Assert.AreEqual(13,game.ScoreForFrame(1));

Assert.AreEqual(18,game.ScoreForFrame(2));

Assert.AreEqual(18,game.Score);

}

[Test]

public void TestSimpleStrike()

{

game.Add(10);

game.Add(3);

game.Add(6);

Assert.AreEqual(19,game.ScoreForFrame(1));

Assert.AreEqual(28,game.Score);

}

[Test]

public void TestPerfectGame()

{

for (int i=0; i<12; i++)

{

game.Add(10);

}

Assert.AreEqual(300,game.Score);

}

[Test]

public void TestEndOfArray()

{

for (int i=0; i<9; i++)

{

game.Add(0);

game.Add(0);

}

game.Add(2);

game.Add(8); // 10th frame spare

game.Add(10); // Strike in last position of array.

Assert.AreEqual(20,game.Score);

}

[Test]

public void TestSampleGame()

{

game.Add(1);

game.Add(4);

game.Add(4);

game.Add(5);

game.Add(6);

game.Add(4);

game.Add(5);

game.Add(5);

game.Add(10);

game.Add(0);

game.Add(1);

game.Add(7);

game.Add(3);

game.Add(6);

game.Add(4);

game.Add(10);

game.Add(2);

game.Add(8);

game.Add(6);

Assert.AreEqual(133,game.Score);

}

[Test]

public void TestHeartBreak()

{

for (int i=0; i<11; i++)

game.Add(10);

game.Add(9);

Assert.AreEqual(299,game.Score);

}

[Test]

public void TestTenthFrameSpare()

{

for (int i=0; i<9; i++)

game.Add(10);

game.Add(9);

game.Add(1);

game.Add(1);

Assert.AreEqual(270,game.Score);

}

}

RSK:几乎覆盖了所有的情况。你还能想出其他有意义的测试用例吗?

RCM:想不出来了,我认为这是一套完整的测试用例集。从中去掉任何一个都不好。

RSK:那我们就完成了。

RCM:我也这么认为。非常感谢你的帮助。

RSK:别客气,这很有趣。

6.2 结论

完成本章后,我把它发布在Object Mentor的Web站点上[34]。许多人阅读了它并给出了自己的意见。一些人认为这篇文章不好,因为其中几乎没有涉及面向对象设计方面的任何内容。我认为这种回应很有趣。必须在每一个应用、每一个程序中都要进行面向对象的设计吗?本例就是一个不太需要面向对象设计的情形。事实上,仅有Scorer类稍微有一点面向对象的味道,不过那也只是一个简单的分割,而不是真正的面向对象的设计。

另有一些人认为确实应该有Frame类。有人竟然创建了一个包含Frame类的程序版本,该程序比上面所看到的要大的多,也复杂的多。

一些人觉得我们对UML有失公正。毕竟,在开始前我们没有做一个完整的设计。餐巾纸背面的有趣的小UML图(图6-2)不是一个完整的设计;其中没有包括顺序图(sequence diagram)。我认为这种看法更加奇怪。就我而言,即使在图6-2中加入顺序图,也不会使我们放弃Throw类和Frame类。事实上,那样做反而会使我们觉得这些类是必需的。

图示是不需要的吗?当然不是。嗯,实际上,对于某些我所碰到的情形是不需要的。就本章中的程序而言,图示就没有任何帮助。它们甚至分散了我们的注意力。如果遵循这些图示,所得到的程序就会具有很多不必要的复杂性。你也许会说同样也会得到一个非常易于维护的程序,但是我不同意这种说法。我们刚刚浏览的程序是因为易于理解所以才易于维护的,其中没有会导致该程序僵化或者脆弱的不当依赖关系。

所以,是的,图示有时是不需要的。何时不需要呢?在创建了它们而没有验证它们的代码就打算去遵循它们时,图示就是无益的。画一幅图来探究一个想法是没有错的。然而,画一幅图后,不应该假定该图就是相关任务的最好设计。你会发现最好的设计是在你首先编写测试,一小步一小步前进时逐渐形成的。

作为对于这个结论的支持,在此附上艾森豪威尔将军的话:“在准备战役时,我发现计划本身总是无用的,但是做计划却是绝对必要的。”

保龄球规则概述

保龄球是一种比赛,比赛者把一个哈蜜瓜大小的球顺着一条窄窄的球道投向10个木瓶。目的是在每次投球中击倒尽可能多的木瓶。

一局比赛由10轮组成。每轮开始,10个木瓶都是竖立摆放的。比赛者可以投球两次,尝试击倒所有木瓶。

如果比赛者在第一次投球中就击倒了所有木瓶,称之为“全中”,并且本轮结束。

如果比赛者在第一次投球中没有击倒所有木瓶,但在第二次投球中成功击倒了所有剩余的木瓶,称之为“补中”。一轮中第二次投球后,即使还有未被击倒的木瓶,本轮也宣告结束。

全中轮的记分规则为:10,加上接下来的两次投球击倒的木瓶数,再加上前一轮的得分。

补中轮的记分规则为:10,加上接下来的一次投球击倒的木瓶数,再加上前一轮的得分。

其他轮的记分规则为:本轮中两次投球所击倒的木瓶数,加上前一轮的得分。

如果第10轮为全中,那么比赛者可以再多投球两次,以完成对全中的记分。同样,如果第10轮为补中,那么比赛者可以再多投球一次,以完成对补中的记分。因此,第10轮可以包含3次投球而不是2次。

上面的记分卡展示了一场虽然不太精彩,但具有代表性的比赛的得分情况。第1轮,比赛者第一次投球击倒了1个木瓶,第二次投球又击倒了4个。于是第一轮的得分是5。第2轮,比赛者第一次投球击倒了4个木瓶,第二次投球又击倒了5个。本轮中共击倒了9个木瓶,再加上前一轮的得分,本轮的得分是14。

第3轮,比赛者第一次投球击倒了6个木瓶,第二次投球击倒了剩余的所有木瓶,因而是一个补中。只有到下一次投球后才能计算本轮的得分。

第4轮,比赛者第一次投球击倒了5个木瓶。此时可以完成第3轮的记分。第3轮的得分为10,加上第2轮的得分(14),再加上第4轮中第一次投球击倒的木瓶数(5),结果是29。第4轮的最后一次投球为补中。

第5轮为全中。此时计算第4轮的得分为:29+10+10 = 49。

第6轮的成绩很不理想。第一个球滚入了球道旁的槽中,没有击倒任何木瓶。第二个球仅击倒了一个木瓶。第5轮中全中的得分为:49+10+0+1 = 60。

其余轮次得分可以自行计算。

[1].他在与我私人交流中如是说。

[2].“Product-Development Practices That Work:How Internet Companies Build Software”MIT Sloan Management Review,Winter 2001,reprint number 4226。

[3].www.controlchaos.com。

[4].crystalmethodologies.org。

[5].Peter Coad,Eric Lefebvre,and Jeff De Luca,Java Modeling in Color with UML:Enterprise Components and Process,Prentice Hall,1999。

[6].[Highsmith2000]。

[7].[Beck99],[Newkirk2001]。

[8].参见www.fitnesse.org。

[9].我曾经见过这样的结对编程的情景,其中一个成员控制键盘,另一个成员控制鼠标。

[10].[Williams2000],[Cockburn2001]。

[11].[Nosek98]。

[12].这里我不是在提倡3层构架。我只是选择了软件技术的3个常见部分。

[13].Ron Jeffries讲到,“End to end is farther than you think.”

[14].http://www.sciencedaily.com/releases/2000/12/001206144705.htm。

[15].[Fowler99]。

[16].[Beck99],[Newkirk2001]。

[17].www.AgileAlliance.org。

[18].www.controlchaos.com。

[19].[Cockburn2005]。

[20].Peter Coad,Eric Lefebvre,and Jeff De Luca,Java Modeling in Color with UML:Enterprise Components and Process,Prentice Hall,1999。

[21].[Highsmith2000]。

[22].Scientific Wild-Assed Guess。

[23].许多开发人员发现使用“理想编程时间”作为他们的任务点数是有用的。

[24].如果总用户故事保持不变,速度图中的柱状图高度应为余量图中本次与上次柱状图高度之差。——编者注

[25].[Jeffries2001]。

[26].[Mackinnon2000]。

[27].了解并依赖于被测模块内部结构的测试。

[28].不了解并且不依赖于被测模块内部结构的测试。

[29].www.fitnesse.org。

[30].FitNesse调用这些API函数的方式超出了本书的范围。更多的信息请参考FitNesse文档,也可以参见[Mugridge2005]。

[31].[Fowler99],p.xvi。

[32].我曾经看过Kent Beck重构该程序。他根本就没有使用平方根。他认为平方根很难理解,并且如果从头至尾遍历数组的话,那么没有测试会失败。但是我不能使自己放弃效率方面的考虑。这一点显现出了我的汇编语言根源。

[33].Dave Thomas和Andy Hunt称之为基于巧合编程(programming by coincidence)。

[34].www.objectmentor.com。

相关图书

程序员的制胜技
程序员的制胜技
C#开发案例精粹
C#开发案例精粹
C#完全自学教程
C#完全自学教程
C#从入门到精通(第2版)
C#从入门到精通(第2版)
 C#初学者指南
C#初学者指南
C#本质论(第4版)
C#本质论(第4版)

相关文章

相关课程