- 注重实效的团队:一旦参与项目的人员超过一个,你就需要建立一些基本原则,并相应的派任务。我们将说明怎样在遵循注重实效哲学的同时做到这一点。
- 无处不在的自动化:使项目级活动保持一致和可靠的一个最重要的因素是使你的各种工作流程自动化。
- 无情的测试:进一步讨论项目范围的测试哲学和工具——特别是在你没有大量QA人员可以随叫随到时。
- 全都是写:与测试相比,开发者更不喜欢的唯一一件事情是撰写文档。不管你是有技术文档撰写者帮助,还是要自己撰写。
- 极大的期望:成功取决于旁观者——项目出资人——的眼睛,重要的是成功的感觉,在“极大的期望”中,我们将向你说明一些能使每个项目的出资人高兴的诀窍
- 傲慢与偏见:鼓励你在作品上签名,并未你所作的事情而自豪。
质量是一个团队问题。最勤勉的开发者如果被派到不在乎质量的团队里,会发现自己很难保持修正琐碎问题所需的热情。如果团队主动鼓励开发者不要把时间花费在这样的修正上,问题就会进一步恶化
团队作为一个整体,不应该容忍破窗户——那些小小的、无人修正的不完美。团队必须为产品的质量负责,支持那些了解我们在“软件的熵”中描述的“不要留破窗户”哲学的开发者,并鼓励那些还不了解这种哲学的人。
质量只可能源于全体团队成员都做出自己的贡献。
还记得“石头汤与煮青蛙”一节中,汤锅中那只可怜的青蛙吗?它没有注意到周围环境的渐变,最终被煮熟了。同样的事情也会发生在不警醒的人身上。在项目开发高涨的热度里,很难用一只眼睛注意周围的环境。
作为整体的团队甚至更容易被煮熟。大家认为,另外有人在处理某个问题,或是团队领导一定已经批准了用户要求做出的某项改动。即使是目的最明确的团队对项目中的重大改动可能也是健忘的。
与之战斗。确保每个人都主动地监视环境的变化。可以指定一个“首席水清检测员”。让这个人持续地检查范围的扩大、时间标度的缩减、新增特性、新环境——任何不在最初的约定中的东西。对新需求进行持续额度量。团队无需拒绝无法控制的变化——你只需要注意到他们正在发生。否则,你就会置于热水中。
显然,团队中的开发者必须相互交谈。我们在“交流!”中给出了一些促进交流的建议。但是,人们很容易忘记,团队本身也存在于更大的组织中。团队作为实体需要同外界明晰地交流。
对外界而言,看上去沉闷寡言的项目团队是最糟糕的团队。他们举行无章次的会议,在回上没有人想说话。他们的文档混乱:没有两份文档有相同的外观,每一份都使用不同的术语。
在“重复的危害”中我们讨论了消除团队成员之间的重复工作的困难这样的重复会造成工作的浪费,并且可能会带来维护的噩梦。显然,良好的交流可以有所帮助,但有时还需要另外的一些东西。
有些团队指定某个成员担任项目资料的管理员,负责协调文档和代码仓库。其他团队成员在查早资料时,可以首先找这个人。通过阅读正在处理的材料,好的资料管理员还能发现正在迫近的重复。
当项目对一个资料管理员来说太大时(或是在无人愿意担任这一职务时),可以指定多人负责工作的各个方面。如果有人想要讨论日期处理,他们知道应该去找Mary。如果有数据库schema问题,去找Fred。
有些开发文化把事情推向极端,实施严格的责任划分:编码员不许与测试员交谈、后者又无须与首席架构师交谈,等等。于是有些组织通过让不同的子团队沿不同的管理链进行报告,对问题进行隔离。
认为项目的各种活动——分析、设计、编码、测试——会孤立地发生,这是一个错误,它们不会孤立发生。它们是看待同一问题的不同方式,人为地分割它们会带来许多麻烦。离代码的用户有两三层远的程序员不太可能注意到他们的工作的应用语境。他们将无法做出有见识的决策。
围绕功能、而不是工作职务进行组织
我们喜欢按照功能划分团队。把你的人划分成小团队,分别负责最终系统的特定方面的功能。让各团队按照各人的能力,在内部自行进行组织。每个团队都按照他们约定的承诺,对项目中的其他团队负有责任。承诺的确切内容随项目而变化,团队间的人员分配也是如此。
确保一致和准确的一种很好是使团队所做的每件事情自动化。如果你的编辑器能自动在你输入时安排代码的布局,为什么要手工进行呢?如果夜间构建能够自动运行各种测试,为什么要手工完成测试表单呢?
自动化是每个项目团队的必要组成部分——重要得足以让我们从下一页开始,专用一节加以讨论。为了确保事情得以自动化,指定一个或多个团队成员担任工具构建员,构造和部署使项目中的苦差事自动化的工具。让它们制作makefile、shell脚本、编辑器模板,使用程序等等。
要记住,团队是由个体组成。让每个成员都能以他们自己的方式闪亮。给他们足够的空间,以支持他们,并确保项目的交付能够符合需求。然后,要像“最够好的软件”中的画家一样,抵抗不断画下去的诱惑。
在汽车时代的破晓时分,启动一辆T型福特车的操作说明有两页还不止。而驾驶现代汽车,你只需要转动钥匙——启动过程是自动的,十分简单。遵循一串指令进行操作的人可能会撑掉引擎,而自动启动器却不会。
尽管计算行业仍处在T型福特车的阶段,我们无法承受为一些常用操作而反复阅读两页长的操作说明。无论是构建和发布流程、是书面的代码复查工作、还是其他任何在项目中反复出现的任务,都必须是自动的。我们也许在一开始就构建启动器和喷油,但他们一旦完成,我们从此只需要转动钥匙就可以了。
此外,我们想要确保项目的一致性和可重复性。人工流程不能保证一致性,也无法保证可重复性,特别是在不同的人对流程的各个方面有不同解释时。
在某个客户的开发现场,我们曾看到所有开发者都在使用同一IE。他们的系统管理员给了每个开发者一套说明,告诉他们怎样把附加软件包安装到IDE上。说明有许多页——到处都写着点击这里,卷动那里,拖这个、双击那个、以及再做一遍。
并不奇怪,每个开发者的机器里的内容有着轻微的不同。当不同的开发者运行相同的代码时,应用的行为会出现微妙的差异,bug会在一台机器上出现,在其他机器上却不出现,追踪任何一个组件的版本差异通常会揭示出一个令人意想不到的情况
不要使用手工流程
人的可重复性并不像计算机那么好。我们也不应期望他们能那样。shell脚本或批处理文件能以相同的次序、反复执行相同的指令,他们能被置于源码控制之下,你因而也可以检查历程的修改历史。
另一个受欢迎的自动化工具是cron。它允许我们安排无人照管的任务周期性地运行——通常是在午夜。例如,下面的crontab文件指定,每天午夜过后5分钟运行项目的nightly命令,每个工作日的凌晨3:15进行备份,每个月第一天的午夜运行expense——reports
使用cron,我们可以自动安排备份,夜间构建、网站维护、以及其它任何可以无人照管地完成的事情。
项目编译是一件应该可靠、可重复地进行琐碎工作。我们通常通过makefile编译项目、即使是在使用ide环境时。使用makefile有若干好处。它是脚本化、自动化的流程。我们可以增加挂钩,让其为我们生成代码,并自动运行回归测试。IDE有自身的优势,但只是IDE,可能很难获得我们寻求的自动化程度。我们想用一条命令就完成签出、构建、测试和发布。
在“重复的危害”中,我们提倡生成代码,以根据公共来源派生知识我们可以利用make的依赖分析机制,让这一过程变得更容易。给makefile增加规则,根据其他来源自动生成文件,是一件相当简单的事情。例如,假定我们想要根据一个XML文件生成Java文件,并编译所得
敲入make test.class ,make 就会自动查找名为test.xml的文件,通过运行Prel脚本构建一个.java文件,然后编译该文件,产生 test.class
我们还可以用同样的一组规则,根据其他形式的文件,自动生成源代码、头文件、或是文档。
你还可以让makefile为你运行回归测试,或是针对整个子系统。只要在源码树顶部发出一条命令,你就可以情锁地测试整个项目;也可在单个目录中发出同样的命令,测试单个模块。关于回归测试。
构建是这样一个过程:取一个空目录(和一个已知的编译环境),从头开始构建项目,产生所有你希望产生的、最终交付的东西——例如,CD-ROM主映像或自解开的存档。在典型情况下,项目构建包括以下几个步骤:
对于大多数项目,这一层面的构建是在每天夜间自动运行的。在典型情况下,与在构建项目的某个具体部分运行的测试相比,在这样的夜间构建中运行的测试要更完整。要点在于,要让完全构建(full build)运行所有可用的测试。你想要知道今天对代码做出的一处改动是否是某个回归测试失败的原因。通过在靠近问题源头的地方确定问题,你更有可能找到并修正它。
如果泥没有定期运行测试,你可能会发现,应用因为三个月之前的改动失败,但愿你有足够的好运气找到它。
你想要作为产品发运的最终构建,可能具有与常规的夜间构建不同的需求,最终构建可能需要锁住仓库,或是标上发布号;要求设置不同的优化和调试标志,等等。我们喜欢使用一个单独的make目标(比如 make final),一次完成所有这些参数设置。
如果程序员能够把他们所有的时间投入实际编程,那不是很好吗?遗憾的是,难得有这样的情况。有e-mail 要回复,有书面工作要完成,有文档要发布到Web上等等。你可以决定创建一个shell脚本来完成一些烦人的工作,但你仍然要记得在需要运行这个脚本。
因为记忆是随着你年龄的增长而丧失的第二种东西,我们不想过分依赖它。我们可以运行脚本,让它们基于源码和文档的内容,自动为我们完成各种流程。我们的目标是维持自动、无人照管、内容驱动的工作流。
许多开发团队用内部网站来进行项目交流,我们认为这是一个很好的想法。但我们不想花费太多的时间去维护网站,也不想让它变得陈旧或过时。误导人的信息比完全没有信息还要糟糕。
从代码、需求分析、设计文档中提取的文档以及任何图片、图表或图形都需要定期发布到网站上。
无论它是怎样完成的,Web内容都应该根据仓库中的信息自动生成,并且无需人的干预就发布出去,这其实是DRY原则的另一种应用:信息以签入代码和文档这一种形式存在,从Web浏览器的角度看出来的视图——只是视图。你应该不必手工维护该视图
夜间构建生成的任何信息都应该在开发网站上访问:构建自身的结果。构建结果可以通过一页摘要的方式给出,其中包括编译警告,错误,以及当前状态)回归测试,性能统计,编码度量,以及任何其他的静态分析,等等。
有些项目具有各种必须遵循的管理工作流。例如,需要安排代码或设计复查,需要批准,等等。我们可以使用自动化——特别是网站——帮助减轻书面工作负担
假定你想要使代码复查和批准自动化,你可以在每个源文件里放置一个特殊标记:
可以用一个简答的脚本检查所有源码,并查找具有needs——review状态的所有文件——这表明它们已经准备好接受复查。随后你可以把这些文件的列表作为网页发布出去、或是自动发送email 给适当的人员、甚或是使用某种日程软件自动安排一次会议
你可以在网页上设置一个表单,用于让复查者登记文件是否通过了复查。在复查之后,状态可自动变为reviewed。是否与所有参与者一起进行检查取决于你,你仍然可以自动完成书面工作。
鞋匠的孩子没鞋穿。软件开发人员常常会使用最糟糕的工具来完成工作。
但我们有制作更好额工具所需的所有原材料,我们有cron。在Windows和Unix平台上我们都有make。我们还有Perl和其他一些高级脚本语言,可以用于快速开发自制的工具、网页生成器、代码生成器、测试装备、等等。
让计算机去做重复、庸常的事情——它会做得比我们更好。我们有更重要、更困难得事情要做。
大多数开发者都讨厌测试。他们往往会温和地测试,下意识地知道代码会在哪里出问题,并避开那些薄弱的地方。注重实效的程序员与此不同。我们受到驱迫,现在就要找到我们的bug,以免以后经受由别人找到我们的bug所带来的羞耻。
寻找bug有点像是用网捕鱼。我们用纤小的网(单元测试)捕捉小鱼,用粗大的网(集成测试)捕捉吃人的鲨鱼。有时鱼会设法逃跑,所以为了抓住我们的项目池塘里游动的、越来越狡猾的缺陷,我们要补上我们发现的任何漏洞。
早测试、常测试、自动测试
一旦我们有了代码,我们就想开始进行测试,那些小鱼苗有飞快地变成吃人的大鲨鱼的可恶习惯,而抓住鲨鱼会困难许多。但沃恩又不想手工进行所有这些测试。
许多团队都会为了他们项目精心制订测试计划,有时他们甚至会使用这些计划。但我们发现,使用自动测试的团队成功机会要打的多。与呆在架子上的测试计划相比,随每次构建运行的测试要有效的多。
bug被发现的越早,进行修补的成本就越低“编一点,测一点”是Smalltalk世界流行的一句话,我们可以把这句话当作我们自己的座右铭,在编写产品代码的同时编写测试代码。
事实上,好的项目拥有的测试代码可能比产品代码还要多。编写这些测试代码所花的时间是值得的。长远来看,他最后会便宜得多,而你实际上有希望制作出近零缺陷的产品。
此外,知道你通过了测试将给你高度的自信:一段代码已经“完成”了。
要到通过全部测试,编码才算完成
你写出了一段代码,并不意味着你可以告诉你的老板或客户,说它已经完成。不是这样的。首先,代码从不会真正完成。更重要的是,在它通过所有可用的测试之前,你不能声称它已经可供使用。
我们须要查看项目范围测试的三个主要方面:测试什么、怎样测试、以及何时测试。
你需要进行的测试的主要类型有:
你使用的所有模块都必须通过他们自己的单元测试,然后才能继续前进。
集成测试说明组成项目的主要子宫能系统工作,并且能很好地协同。如果在适当的地方有好的合约,并且进行了良好的测试,我们就可以轻松地检测到任何集成问题。否则,集成就会变成肥沃的bug繁殖地。事实上,它常常是系统的bug来源中最大的一个。
一旦你有了可执行的用户界面或原型,你需要回答一个最重要的问题:用户告诉了你他们的需要什么,但那是他们需要的吗?
它满足系统的功能需求吗?这也需要进行测试。没有bug、但回答的问题本身是错误的,这样的系统不会太有用。要注意用户的访问模式、以及这些模式与开发者所用的测试数据不同。
现在你已经很清楚,系统在理想条件下将会正确运行,你要知道的是,它在现实世界的条件下将如何运行。在现实世界中,你的程序没有无线资源;它们会把资源耗尽。你的代码可能会遇到一些限制包括:
性能测试、压力测试或负载测试也可能是项目的一个重要方面。
问问你自己,软件是否能满足现实世界的条件下的性能需求——预期的用户数、连接数、或每秒事务数。它可以伸缩吗?
对于有些应用,你可能需要用专门的测试硬件或软件模拟现实情况下的负载。
可用性测试与到目前为止讨论过的其他测试类型不同。它是真正的用户、在真是的情况下进行的。
我们已经考察了要测试什么。现在我们将把注意力转向怎样测试,包括:
回归测试把当前测试输出与先前的值进行对比。我们可以确定我们今天对bug的修正没有破坏昨天可以工作的代码。这是一个重要的安全网,它能减少令人不快的意外。
我们到目前为止提到过的所有测试都可以作为回归测试运行,确保我们在开发新代码没有损失任何领地。我们可以运行回归测试,对性能、合约、有效性等进行校验。
我们从何处获取运行所有这些数据所需的数据?数据又两种:现实世界的数据和合成的数据,实际上我们两者都需要,因为这两类数据的不同性质将会揭示出我们软件中的不同bug
现实世界的数据来自某种实际来源。它可能收集自己有系统、竞争者的系统、或是某种原型。它代表典型的用户数据。当你发现典型意味着什么时,你会大吃一惊,这最有可能揭示出需求分析中的缺陷和误解。
合成数据是人工生成的数据——或许受限于特定的统计约束。处于以下任何原因,你都有可能使用合成数据。
用专门写好的脚本进行GUI测试
因为我们不可能编写完美的软件,所以我们也不可能编写完美的测试软件。我们需要对测试进行测试。
通过蓄意破坏测试你的测试
一旦你自信你的测试是正确的,并且正在找出你制造的bug,你怎么知道你已经彻底地对代码库进行了测试呢?
最简单的回答是“你不知道”。
测试状态覆盖,而不是代码覆盖
许多项目往往会把测试留到最后一分钟——最后期限马上来临时,我们需要比早得多地开始测试。任何产品一旦存在,就需要进行测试。
大多数测试应该自动完成。注意到这一点很重要:我们所说的“自动”意味着对测试结果也进行自动解释。
如果有bug漏过了现有的测试,你需要增加新的测试,以在下一次抓住它。
每个bug只出现一次,以后最好不要再让出现
好记性不如烂笔头
在典型情况下,开啊者不会太关注文档。在最好的情况下,它是一件倒霉的差事;在最坏的情况下,它就会被会当作低优先级的任务,希望管理部门会在项目结束时忘掉它。
注重实效的程序员会把文档当作整个开发过程的完整组成部分加以接受。不进行重复劳动,不浪费时间,并且把文档放在手边——如果可能,就放在代码本身当中,文档的撰写就可以变得更容易。
这些并不完全是独创的或新奇的想法;把代码和文档紧密地结合在一起的思想早已出现。
把英语当作又一种编程语言
为项目制作的文档基本上有两种:内部文档和外部文档。内部文档包括源码注释、设计与测试文档,等等。外部文档是发运或发布到外界的任何东西,比如用户手册,但不管目标受众是谁,也不管作者的角色是什么。所有的文档都是代码的反映,如果有歧义,代码才是最要紧——无论好坏。
把文档建在里面,不要拴在外面
根据源码中的注释和声明产生格式化文档相当直接了当,但首先我们必须确保在代码中确实有注释。代码应该有注释,但太多的注释可能和太少一样糟。
一般而言,注释应该讨论为何要做某事、它的目的和目标。代码已经说明了它是怎样完成的,所以再为此加上注释是多余的——而且违反了DRY原则。
下列是不应该出现在源码注释中的一些内容:
假定我们有一个规范,要列出某个数据库表中的各个列。然后我们要用另一组SQL命令在数据库中创建实际的表,还可能创建某种编程语言的记录结构,用以存放表中的一行的内容。同样的信息重复了三次。更改这三个信息源中的任何一个,其他两个就会立刻过时。这显然违反了DRY原则。
到现在为止,我们谈论的只是内部文档——由程序员自己撰写的文档。但如果在项目中有专业的技术文档撰写者又会怎样呢?情况常常是,程序员只是把材料“扔过隔板”,给那边的技术文档撰写者,闭关让他们自己设法制作用户手册、宣传材料,等等。
这是一个错误,程序员不撰写这些文档,并不意味着我们可以放弃注重实效的原则。我们想让文档撰写者和注重实效的程序员一样,接受同样的基本原则——特别是遵守DRY原则、正交性、模型——视图概念、以及自动化和脚本的使用。
某公司宣布利润创纪录,其股价却下跌了20%,当晚的金融新闻解释说,该公司没有实现分析家预期的业绩。一个小孩打开昂贵的圣诞礼物,却大哭起来——这不是他想要的廉价洋娃娃。某个项目团队奇迹般地实现了一个极其复杂的应用,但却遭到用户的抵制,因为该应用没有帮助系统。
在抽象的意义上,应用如果能正确实现其规范,就是成功的。遗憾的是,这只能付抽象的帐。
在现实中,项目的成功是由它在多大程度啥满足了用户的期望来衡量的。不符合用户预期的项目注定是失败的,不管交付的产品在绝对的意义上有多好。但是,像希望得到廉价洋娃娃的小孩的父母一样,你走得太远也会失败。
温和地超出用户的期望
但是,执行这条提示需要做一些工作。
用户在一开始就会带着他们对所需要的东西的想象来到你面前。那可能不完整、不一致、或是在技术上不可能做到,但那时他们的,而且,就像过圣诞节的小孩一样,他们也在其中投入了一些感情。你不能简单地忽视它。
随着你对他们的需要的理解的发展,你会发现他们的有些期望无法满足,或是他们的有写期望过于保守。你的部分角色就是要此进行交流。与你的用户一同工作,以使它们正确地理解你将要交付的产品。并且要在整个开发过程中进行这样的交流,决不要忘了你的应用要解决的商业问题。
有些顾问称这一过程称为“管理期望”——主动控制用户对他们能从系统中得到什么应该抱什么应该抱有的希望。我们认为这是一个有点高人一等的想法。我们的角色不是控制用户的希望,而是要与他们一同工作,达成对开发过程和最终产品、以及他们尚未描述出来的期望的共同理解,如果团队能与外界通畅地交流,这个过程就几乎是自动的;每个人都应该理解所期望的是什么以及它将被怎样构建出来。
有一些重要技术可用于促进这一过程。其中“曳光弹”和“原型与便笺”是最为重要的技术。两者都让团队构造用户能看见的东西。两者都是与用户交流你对他们的需求的理解的理想途径。并且两者都让你和用户习惯于相互交流。
如果你和用户紧密协作,分享他们的期望,并同他们交流你正在做的事情,那么当项目交付时,就不会发生多少让人吃惊的事情了。
这是一件糟糕的事情。要设法让你的用户惊讶。请注意,不是惊吓他们,而是要让他们高兴。
给他们的东西要比他们期望的多一点。给系统增加某种面向用户的特性所需的一点额外努力将一次又一次在商誉上带来回报。
随着项目的进展,听取你的用户的意见,了解什么特性会使他们真的高兴。你可以相对容易地增加、并让一般用户觉得很好的特性包括:
注重实效的程序员不会逃避责任。相反,我们乐于接受挑战,乐于使我们的专业知识广为人知。如果我们在负责一项设计,或是一段代码,我们是在做可以引以为自豪的工作。
在你的作品上签名
过去时代的手艺人为能在他们的作品上签名而自豪,你应该也如此。
但是,项目团队仍然是由人组成的,这条规则可能会带来麻烦。在有些项目里,代码所有权的概念可能会造成协作上的问题。人们可能会变得有地盘意识,或是不愿再公共的基础设施上工作。项目最后可能会变得像一些相互隔绝的小“采邑”。你变得怀有偏见,只欣赏自己的代码,排斥自己的同事.
那不是我们想要的。你不应该怀着猜忌心阻止要查看你的代码的人;处于同样的原则,你应该带着尊重对待他人的代码。“黄金法则”和开发者间的相互尊重是使上面的提示行之有效的关键所在。
匿名(尤其是在大型项目中)可能会为邋遢、错误、懒惰和糟糕的代码提供繁殖地。只把自己 看作齿轮上的一个齿、在无休无止的状况报告中制造蹩脚的借口、而不去编写优良的代码,那太容易了。
我们想要看到对所有权的自豪。“这是我编写的,我对自己的工作负责。”你的签名应该被视为质量的保证。当人们在一段代码上看到你的名字时,应该期望它是可靠的、用心编写的、测试过的和有文档的,一个真正的专业作品,由真正的专业人员编写。