内容


能写好代码就能写好需求

Comments

Illustration许多软件开发团队没有需求工程师;开发人员捕获、编写和管理所有的需求。这在资源效率方面是有意义的:开发人员可以在正式编码之前,在系统停机时间收集和编写需求。然而这一做法的缺点是,通常程序员没有在编写需求方面受过技术和工具的培训,结果他们总是费力和低效地工作,而且有时做出的需求规约不符合规范。

为了写出好的代码,开发人员必须知道很多事情:诸如控制结构和调用约定之类的基本概念;至少一门程序设计语言,包括它的语法和结构;操作系统基础;以及如何使用诸如编译器、调试器、集成环境这类的技术。好在他们能以所有这些知识为跳板写出好的需求来。通过应用许多与他们编写代码时相同的原则和概念,开发人员可以有效地担当起需求工程师的职责。

让我们看一些开发人员可以利用的编程概念。

遵循结构

所有的程序设计语言都有一种结构。这种结构指出程序的不同部分如何定义,彼此之间形成何种关系。Java程序由类来形成结构,COBOL程序有不同的“区段”,C程序有一个主程序以及多个子程序。

程序有一个特定的结构,需求也是如此。设想你把一个C程序的所有代码都塞到主程序里——它会变得不可读而且无法维护。与此相似,如果你的需求规约只是一张毫无规则的大列表,你也没办法使用它。不管你意识到没有,一组需求总有一个结构。获得需求中结构的最佳方法是把它们按不同类型来组织,而这些类型常常对应着不同的级别。

为理解不同类型之间的差别,我们来看看用于保险索赔处理的四条需求样例:

  1. 我们必须有能力处理积压下来的索赔单据。
  2. 系统必须能自动检查索赔表单以获得适用条款。
  3. 对索赔者,系统要根据其社会保险号码确定其是否是注册用户。
  4. 系统要支持处理多至100个并发的索赔请求。

你的直觉或许会告诉你其中每条需求都有一些不同的东西。第一条需求是级别很高的;它在表达业务需要的时候甚至没有提到系统本身。第二条需求表达了系统应该做什么,但仍在较高的级别上;它仍然太宽泛,不能直接翻译成代码。第三条是低级别的需求;它确实为软件必须完成的功能提供了足够的细节,使你能写成代码。第四条需求虽然非常详细,但并没有告诉你系统必须做什么;它只是规定了系统必须有多快。当你跟用户和其他涉众打交道时,这些需求是非常典型的。也许你已经看到为什么把它们放在一个大而无组织的表上会导致混乱。

为使需求更为可用,你可以把它们按范畴或类型分开,比如:

  • 业务需要
  • 特性
  • 功能性软件需求
  • 非功能性软件需求

这是IBM Rational Unified Process(RUP)中建议的类型。它们绝非唯一可能的类型,但它们表达了一种有用的方法。在你的项目的早期,就要决定用什么类型。这样,在你从涉众那里收集信息时,要确定他们描述的是何种需求类型,然后写成需求。

注意你可以用两种格式之一指定功能性软件需求:声明形式和用例形式。上述第三条需求是声明形式的;它是粗线条的,用了一个“要……”的句式。另一个形式是用例的,它也指定了系统应该做什么,级别足够低,能直接写成代码,不过它提供了更多的上下文,告诉用户和系统应该如何交互以执行一些有价值的东西。(关于用例的更多细节见下文。)在你着手收集项目需求之前,你应该确定哪一类功能型需求是你要用的,以后就不要改变。

用习惯保证质量

你知道写出好的代码和坏的代码都是可能的。有很多途径导致坏的代码,其中之一就是使用非常抽象的函数名和变量名,比如routineX48PerformDataFunctionDoItHandleStuffdo_args_method。这样取名没有给这些方法和过程的功能提供任何有用的信息,迫使读者钻进代码里去搞清楚。另一个糟糕的做法是用单字母的变量名如i,j,k。你用一个简单的文本编辑器无法搜索到这些变量,其功能也不清晰。

当然,你也有很多途径写出坏的需求。可能最糟的错误是二义性。如果一条需求让两个人按两种方式解释,这条需求就有二义性。比如,这是一个从实际需求规约里抽出来的需求:

应用程序在多个并发用户访问时必须极其稳定,且不能牺牲速度。

多个极其这样的词有多种解释,所以这一需求是有二义性的。事实上,为了获得清晰性,你必须把它表达成这样三条非常具体的需求:

  1. 系统故障的平均间隔时间不能大于每星期一次。
  2. 系统应支持1000个并发用户同时查询数据库,而不会发生拥塞或数据丢失。
  3. 系统的平均响应时间在多至1000个并发用户时应小于一秒。

质量需求还有更多的属性,详见IEEE的指南。1

详细编写注释

风格良好的程序包含注释,这些注释为代码提供了额外的信息,解释这段代码在做什么或者它为什么用这种方法写。好的注释不解释代码怎样做某件事情——这一点代码本身显然已经说清楚了——而只提供必要的信息帮助用户、维护人员和复审人员理解代码做了什么,以此保证质量。类似地,需求也有属性——这是使需求更为可读和可用的信息。当你捕获需求时你也应该寻求属性信息。例如,一个重要的属性是来源:这条需求是从哪里来的?记下你的信息的来路将在你需要回溯以获得更多信息时节约大量时间。另一个属性是用户优先级。如果一个用户给你五十条需求,他也应该让你知道其中每一条跟其他相关的比起来重要度如何。这样在项目生存周期的后期,时间越来越紧迫,你意识到已经不可能满足所有的需求时,至少你还知道哪些是最重要的。

正如没有哪条规则告诉你代码里的注释一定要怎么写才正确,也不存在“正确”属性的一个普适的列表。来源和优先级几乎总是有用的,但你必须定义适合你的项目的其他属性。当你搜集需求时,试着预计一下当你着手设计系统和编码时整个团队可能需要什么信息。

熟悉语言

显然,开发人员必须熟悉他们用来编码的语言,不管是Java,COBOL,C++,C,Visual Basic,Fortran还是其他什么语言。为了写出好的代码,你必须了解语言之间的细微差别。虽然所有语言里基本的程序设计概念都是一样的,但在具体某个操作时它们会使用不同的方式。比如,Java的循环结构用“for”;Fortran则用“DO”。C语言里你以子程序名带上参数来调用一个子程序;Fortran里你用一个CALL语句。

为了写好需求你也得熟悉语言。多数需求是用自然语言写成的(法语、英语等等)。自然语言非常强大,但也非常复杂;未受过写作训练的开发人员在写作中表达复杂想法的时候有时会碰上困难。我们这里没有足够的篇幅留给一个完整的写作课程,但有些指导原则是有用的。

首先,对声明形式的需求使用完整的句子。(例如,以“应”或者类似的结构表达的语句。)在每个句子里检查主语和动词。

第二,使用简单句。一个语句只包含一个独立子句,只传达一个想法时,更易于理解、检验和测试。如果你的需求太复杂,难以用简单句表述,试着把它分解成几条更小、更易于定义的需求。并列句和复合句会引入依赖关系(分支);换言之,它们可能描述那些依赖某些动作的变量,结果常常产生一条不必要的复杂需求,增加测试的困难。

简单句:系统应能显示车沿跑道绕行一圈花费的时间。
并列句:系统应能显示车沿跑道绕行一圈花费的时间,且时间的格式应是hh:mm:ss。(这是两条需求,一条是功能性需求,指定系统要做什么,另一条是用户界面需求,指定时间格式。)
复合句:系统应能在车沿跑道绕行一圈后5秒之内显示这一圈花费的时间。(这也是两条需求,一条功能性需求和一条性能需求。)

为了给并列句和复合句写出合适的测试,你只能把其中的两条需求分开。既然如此为什么不干脆从一开始就这样做呢?以下是把上述复合句拆分成简单句的一种方案:

系统应能显示车沿跑道绕行一圈花费的时间。
绕行时间的显示格式应为hh:mm:ss。
绕行时间应在一圈结束后5秒内显示出来。

注意,为使需求更易测试,它们写得更易阅读。

还有一个写好需求的技巧:使用一致的文档格式。你已经有了一个编码的格式或模板。编写需求的时候也可以利用它。一致性是关键;每个规约文档应该用相同的标题、字体、缩进等等。模板有助于做到这点。实际上,它们起到表单的作用;编写需求的开发人员编写好的规约就无需从草稿开始,做重新发明车轮的工作。如果你需要模板的样例,RUP上面有很多。

遵循指南

许多开发团队使用类似这样的编码指南:

  • 将模块的定义和实现放在不同的文件里(C++)。
  • 在一个代码块的范围内作缩进(Java)。
  • 将频繁调用的数据元素放在每组工作存储区变量的开头(COBOL)。

在编写需求时你也应该遵循某种指南。例如,你如果决定用用例来规约软件需求,你的指南就应该告诉你如何写出事件流程。用例事件流程解释了系统和用户(参与者)如何通过交互完成工作。你的指南应当描述在流程里发生什么(成功场景),在备用流程里发生什么(意外场景),也应该描述如何组织这些流程的结构。你的指南也应该提示两个流程的长度以及其中的独立步骤。如果你决定使用传统的声明式需求,则指南应当解释如何编写需求。好在许多这样的指南已经在RUP和其他相关的资源里有了, 所以你不必自己撰写。2

理解操作环境

为开发好的代码,你必须熟悉那台运行你的系统的机器,也必须熟悉怎样使用其操作系统。如果是Windows,你必须熟悉MFC和.Net。如果是Linux,你必须熟悉UNIX系统调用。

为写出好的需求,你要理解的是操作人员而不是操作系统。你也必须理解用户而不是用户界面。Java开发人员考虑类路径;需求编写人员考虑通向类(或工作组)的正确途径。

需求捕获是一项以人为中心的工作。你不能虚构需求,只能从其他人那里获得需求。这对内向的开发人员来说或许是个挑战,但如果能正确地应用已有的技能,他们是能成功的。

用户常常不知道他们要的是什么,或者知道是什么却不知道怎么描述它。开发人员却拥有这样的能力来改善这一点:他们常常不得不破解编译器给出的费解和难懂的错误信息。比如,Java开发人员在写一个小应用程序时可能碰上这样的信息:

load: com.mindprod.mypackage.MyApplet.class can't be instantiated.
java.lang.InstantiationException: com/mindprod/mypackage/MyApplet

这是什么意思呢?如果开发人员不确定,他会去检查代码,查询编译器的文档,甚至利用Google这样的搜索引擎,来弄清问题出在哪儿。最后他将发现,他写的小应用程序代码缺少缺省的构造函数。

如果你正在为一个天气预报系统收集需求,涉众之一告诉你系统应能“用标准的带小尾巴的箭头显示200平方英里的一块区域之上大气层不同高度的风速和风向”,你就需要深度挖掘一下。你可以要一个类似的系统给出的报告,求助于气象学的书籍,也可以请另一名涉众把要求描述得更精确一些。你应当继续研究,直到掌握足够多的细节来描述期望的功能。这样你就可以重述需求,使之清晰和无二义性,提供足够的细节以支持设计。

另一个捕获需求的技巧是避免问诱导性的问题。虽然你对用户的需要可能已经有了想法,但如果你把这些和盘托出,你恐怕无法获知整体上他们到底需要什么。反之,问一些开放性的问题如“你要让分开的数据怎样显示?”而不要问“你要不要把气压和温度合起来显示在一张图表里?”

遵循既定原则

设计和编写优秀程序的基本原则中,有信息隐藏、耦合和内聚。在编写需求中也有与之相应的原则。

信息隐藏

这个原则是说一段代码的使用者/调用者不能访问甚至不能获知数据的内部细节。所有对数据的访问与修改必须通过函数调用来完成。这样,你在改变内部数据结构时不会影响到调用它的外部程序。

对于需求这也是一个好的原则,特别是在用例表达的情形。如我们所说过的,用例具有事件流程。写得不好的用例常有塞满了数据定义的事件流程。考虑这个管理购买请求用例的基本事件流程:

基本事件流程

  1. 系统显示所有未决的购买请求。
  2. 每个未决请求包含该请求的如下信息(限制为char型):
    • 授权ID (仅内部使用)
    • PO #
    • 引用ID
    • 发布者帐户缩写名
    • 经销商帐户名(前10个)
    • 经销商帐号
    • 购买原因码
    • 请求购买量
    • 请求日期
    • 分配给内部
    • 注释指针
  3. 经授权的管理员可以做以下几件事之一:1)批准 2)拒绝 3)取消 或 4)分配请求。他选择1)批准。
  4. ……如此等等直到所有步骤完成。

以上十五行里,十一行用来说明哪些数据与一个未决请求在一起处理。这些信息很重要,但却使用例中发生的事情变得不清晰。更好的解决方案是将数据隐至别处。这些步骤就变成这样:

基本事件流程

  1. 系统显示所有未决的请求。
  2. 经授权的管理员可以做以下几件事之一:1)批准 2)拒绝 3)取消 或 4)分配请求。他选择1)批准。
  3. ……如此等等直到所有步骤完成。

未决的购买请求用了斜体,指出数据在别处定义(通常在用例的特殊需求段或在词汇表中定义)。这使表达真实功能性需求的事件流程易于阅读和理解。

耦合与内聚

对编码人员,耦合原则是指程序中的单独模块应当尽可能地互不相关。 一个模块内部的处理应与其他模块的内部机制无关。内聚原则是指一个模块内部的所有代码应该只完成一个目标。这些原则使程序易于理解和维护。

这些原则对需求同样适用,尤其是用例。用例应当独立(即极少或没有耦合)。3 每个用例应指定一个有意义的功能块,说明系统如何为参与者提供有价值的东西。参与者焦点很重要,你可以指定系统为参与者做什么,而不必担心用例按序排列的问题。

一个用例中所有的功能性成分应当只用于完成参与者的一个目标(高度内聚)。在一个典型的自动取款机(ATM)系统中,一个用例是“取款”,另一个是“转帐”。每个用例集中于单一的目标。如果你把这些功能合并到一个用例里,就成为低内聚的(和不合适的)依赖关系。

然而要小心,许多用例的初学者走过了头,建立了太多的低层用例。我看到过一个用于银行债务收集系统的模型,它拥有150个用例,用例的起名诸如“修改数据”。这个项目有一个十人团队,计划持续了差不多一年。然而,由于这些用例太琐碎,整个团队在推进的时候碰上了无数的麻烦。他们大量描述了对用户毫无价值的底层功能,这些功能既难以理解也难以使用。每个用例极为内聚,但也因此造成用例间的高度耦合。把层次提升到更明确的活动如“收集债务信息”,就能在耦合和内聚之间保持合适的平衡。

使用自动化工具

开发人员使用软件工具完成工作。手工编译代码绝无可能,所以他们一定要用到编译器。集成开发环境(IDE)发展得更加集成化,因其组织效率的不断提高而越来越受欢迎。用于代码生成和逆向工程的UML建模工具也得到了广泛的应用。

自动化工具对编写和管理需求同样非常有用。自动化的第一步是使用字处理器。使用Microsoft Word来记录需求,比起把它们写在白板或餐巾纸上,或者记在脑子里,已经是跨出了一大步。字处理器提供了拼写检查、格式化和作为需求“容器”的文档模板。你可以把它们保存在一处,然后发出去复审。然而Word文档不能帮你排序和过滤大型的需求列表,也不能建立可追踪性。

诸如Microsoft Excel的电子表格软件使较为复杂的排序和过滤变得容易,但这是以丢失文档提供的上下文关系为代价的。当然你也能用这些软件建立可追踪性,但是操作还是手工的。

针对某个数据库单独开发的工具有着与电子表格软件相同的一些优点,此外它们还在过滤和可追踪性方面性能优异。但这样的工具其功能常常只针对一个特定项目的结构,所以难以移植。而且它可能缺少完备的和最新的文档。

专用于需求管理的工具(RM工具)通常比Word或Excel稍微复杂一些,但比起编译器或IDE来还是简单的。它们有一些重要的优点:

  • 几乎所有的RM工具都允许你导入已有的需求文档。如果你用的工具足够好,你的文档写得也很好,文档中的实际需求能被自动地识别出来。IBM Rational RequisitePro在文档中的需求和工具(或后台数据库)中存储的需求之间提供动态链接,所以需求总是“活”的。
  • RM工具使你易于创建需求类型并赋予属性。这可以用于排序和过滤,给用户一个灵活的查询机制,易于找到感兴趣的需求。用这些工具你可以把需求按属性排序,比如,假如你的需求类型特性里有“优先级”和“风险”的属性,你可以建立这条查询:“显示所有高优先级和高风险的特性。”这有助于你在项目早期的迭代时就考虑哪些特性是需要实现的,确保你不遗漏重要的功能,在项目的早期就遏制住风险。
  • 好的RM工具具有需求间的可追踪性;一个真正好的工具具有与其他工具和工件(如设计和测试)间的可追踪性。可追踪性的重要性在于能够帮助你检测和校验系统。
  • 需求管理的最好习惯是追踪每条需求的历史。RM工具也能帮你完成这点。它不仅能告诉你需求的来源,也能告诉你所有的决策是谁为什么制订的。
  • RM工具的另一有用特性是基线:你在某个特定时间点为你的需求拍一个“快照”,这样就可以与将来的快照相比。基线提供了一个能用于工作的稳定的需求集。假如你需要把你的需求复制到一个新的开发工作中去,它也能记录项目生存周期的分叉点供你引用。

所以,像编译器和IDE那样,RM工具为开发人员完成那些不易(或根本不可能)手工完成的工作,帮助他们大幅度提高效率。

管理变更

好的开发小组管理代码变更。开发人员根据设计和规格说明书编写代码;他们绝不擅自增加特性。此外,代码处于源代码控制中;需要变更代码时,开发人员指明为什么要改。他们还周期性地建立代码的基线,集成代码,并测试代码用于发布。

需求也需要控制变更。变更不可避免,为此制订计划十分重要。项目一开始,需求通常处于一种不断变化的状态(这也很合情理)。但在某些点上,在编写大量代码之前,必须在沙滩上划出一条线来,建立需求的基线。之后,需求变更必须经过批准,典型的情况下通过一个变更控制委员会(CCB)来完成。但是,某些企业只指派一两个人来定期复审变更请求。

那些没有需求变更控制流程的团队只好把来自各个角落的变更请求全部公布出来,进而常常难于否决这些请求。如果你想避免这一点,避免跟着需求的变更不断地被迫重写代码,那就请启用一个CCB或者类似的东西。一个变更复审流程有助于确保做出的变更能够产生业务价值,并让每个人明白变更的效果。无业务价值的的变更只是消耗资源,几乎没有回报。同样,有业务价值的变更也可能因为对现有的需求、设计、编码和测试影响过大而得不偿失。

变更的潜在价值和可行性的另一种评估方式是通过可追踪性。你可以通过追踪需求的合理性来理解相关的工件。通过向更高层次的业务或者说用户需求来追踪软件需求,你可以确认它的价值。如果你这样追踪追不下去,软件需求很可能就没有业务合理性。此外,从高层次向低层次追踪需求,追到设计、编码、测试,你很容易看到需求变更的影响。一个可追踪性矩阵——或更好的,一个RM工具——能清晰地显示所有的相关工件,并提供必要的信息以确定变更请求是否值得接受。

计划

大多数成功的软件开发项目都有一个项目指导计划,规定谁做什么、怎么做、里程碑是什么。架构师通常总是要建一个文档,全面概述其系统架构。这就使架构师与各个项目团队成员在架构的重大决策方面保持交流,指导开发人员实现该系统。

跟这些计划文档一样,需求管理(RM)计划对项目有很大的好处。对编写需求的开发人员,该计划描述了必要的需求工件,以及需求类型及相应的属性。它规约了开发人员必须收集的信息和控制需求变更的机制。

正如我们前面看到的,需求类型可能包括业务需要、特性以及功能性和非功能性的软件需求。可能还有用户需求和市场需求。计划鼓励你考虑和规约你需要的需求类型,它反过来确保需求编写的一致性与可读性。

也如我们前面注意到的,属性提供了补充信息,使你更加有效地理解和使用需求规约。

RM计划也描述了你要用到的文档。RUP推荐分为三类文档:一个前景文档,多个用例文档和一个补充规约文档,最后一种描述了不能根据用例描述的需求。

RM计划还描述了变更管理流程,以使项目中的所有人都能理解它。

如果你已经在一个未采用RM计划的项目中工作,你可以自己写一份。无需很长:一两页纸也许就能包含你要用来提升创建一致和高质量的需求的全部信息。

好的开发人员一定能写出好的需求

当开发人员必须从事需求捕获和记录的工作时,他们通常用于创建代码的原则和经验能够很好地为他们服务。如果你作为开发人员,感到由于缺乏足够的背景和训练,难于写出有效的需求,我希望本文已说服你改变了观点。只要简单地应用你每天都会用到的那些知识和原则,你一定会在这项新的工作中获得成功。

注释

1IEEE Recommended Practice for Software Requirements Specifications, Software Engineering Standards Committee of the IEEE Computer Society. Approved 25 June 1998

2在RUP中,见Artifacts -> Requirements Artifact Set -> Use-Case Model -> Use-Case Modeling Guidelines。亦见IEEE Recommended Practice for Software Requirements Specifications, Software Engineering Standards Committee of the IEEE Computer Society. Approved 25 June 1998

3一致建模语言,或UML,包括扩展包含的概念。你可以在IBM Rational Unified Process里学习进一步的主题。另一本好的参考读物是Kurt Bittner与Ian Spence的Use Case Modeling (Addison-Wesley 2003)。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Rational
ArticleID=58328
ArticleTitle=能写好代码就能写好需求
publish-date=04012005