卓越代码之道第三卷-全-

卓越代码之道第三卷(全)

原文:zh.annas-archive.org/md5/b8099fceef0cc9eeb319cd774b5f6662

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Image

在 20 世纪 60 年代末,计算机软件的需求远远超过了技术学校、大学和高等院校培养训练有素的计算机专业人员来开发这些软件的能力——这一现象被称为软件危机。提高大学和院校的输出并不是一种切实可行的方法;当时,计算机科学专业的合格学生人数太少,无法满足需求。研究人员当时认为,更好的解决方案是提高现有程序员的生产力。注意到软件开发和其他工程活动之间的相似性,这些研究人员得出结论,其他工程学科行之有效的程序和政策可以解决软件危机。于是,软件工程应运而生。

在软件工程领域繁荣之前,软件开发是一项神秘的技艺,由能力和成就各异的高手们所实践。在那时,软件项目的成功完全依赖于一两位关键程序员的能力,而不是整个团队的能力。软件工程旨在平衡软件团队的技能,使它们更具生产力,减少对那一两位高才的依赖。

在很大程度上,软件工程的实践是成功的。由程序员团队构建的大型项目,过去使用临时组织方法是无法完成的。但与此同时,重要的品质却丧失了。软件工程鼓励团队的生产力,却以牺牲个人的创造力、技能和成长为代价。尽管软件工程技术有可能将差劲的程序员培养成优秀的程序员,但它们也可能限制杰出程序员发挥最佳能力。世界上优秀程序员太少了。我们最不希望做的就是让程序员失去发挥潜力的动力;然而,这正是软件工程模式常常做的事情。

《写出优秀的代码》系列的目标是恢复一些丧失的个人创造力、技能和成长。它讲述了我所称之为个人软件工程的内容,即程序员如何提高代码质量。具体来说,它描述了如何从平庸的代码中编写出优秀的代码——一种易于维护、增强、测试、调试、文档化、部署,甚至退休的代码。优秀的代码没有那些往往是工程师或管理层由于不合理压力或糟糕计划所产生的杂乱和临时解决办法。优秀的代码是你可以为之自豪的代码。

当我完成了《写出优秀的代码,第 2 卷:低层次思维,高层次写作》WGC2)时,我原本打算在这本书中加入更多的内容。在WGC2的最后一章中,我写下了以下内容:

[写出优秀代码,第三卷:工程软件]开始讨论编程的个人软件工程方面。软件工程领域主要关注大型软件系统的管理。而个人软件工程则涵盖了那些与个人层面上写出优秀代码相关的话题——工艺、艺术和对工艺的自豪感。因此,在工程软件中,我们将通过讨论软件开发隐喻、软件开发者隐喻以及系统文档 [重点强调]等话题,来考虑这些方面。

系统文档(包括需求、测试程序、设计文档等)是软件工程的一个重要部分。因此,关于这一主题的书籍至少必须提供这些内容的概述。好吧,在本书写到第七章时,我意识到单一的一本书无法涵盖所有这些内容。最终,我将本卷《工程软件》拆分成四卷。这四卷中的第一卷就是本书,它是《写出优秀代码》系列的第三卷。它集中讨论软件开发模型和系统文档。系列的第四卷将讲解软件设计;第五卷将进一步发展优秀编码的主题;第六卷将涉及测试。

当我写这篇文章时,距离我完成《写出优秀代码》系列第二卷已经过去了 10 年。是时候完成第三卷了,即使这意味着将原本的信息分成两卷或更多卷。如果你读过我之前的书,你会知道我喜欢深入探讨课题;我不感兴趣写那些仅仅触及主题的书。因此,我面临着将这项工作分成多个卷并尽快发布,或者制作一本 2000 页的巨著,而历史常常证明,这本书可能永远无法完成。我为那些期待本书涵盖更多内容的人道歉。别担心——这些信息会在未来的卷中出现。你只是提前在本书中获得了第一部分内容。

假设与前提条件

为了专注于工程软件,本书必须做出一些假设。虽然我尽力将这些假设保持在最低限度,但如果你的个人技能集符合某些前提条件,你将从本书中受益最多。

你应该至少对一种命令式(过程式)或面向对象的编程语言有相当的熟练度。这包括 C 和 C++、C#、Swift、Pascal、BASIC、Java 以及汇编语言。你应该知道如何根据一个小问题的描述,进行设计和实现其软件解决方案。一门典型的大学课程或几个月的自学经验应该足以让你使用本书。

你还应该具备计算机组织和数据表示的基本知识。例如,你应该理解十六进制和二进制数字系统,以及计算机如何在内存中表示各种高级数据类型,如有符号整数、字符和字符串。如果你在这方面的知识较弱,Write Great Code, Volume 1: Understanding the Machine (WGC1) 完整地覆盖了计算机组织。尽管我可能会参考WGC1中的内容,但你应该能够独立阅读本书,而不依赖于那本书。

什么是优秀的代码?

优秀的代码是遵循一套规则的软件,这些规则指导程序员在将算法实现为源代码时所做的决策。优秀的代码是以其他程序员为考虑对象编写的——其文档使其他人能够阅读、理解并维护该软件。我称之为软件开发黄金法则,它是软件工程的关键。

降低一个层次,优秀的代码:

  • 高效且使用 CPU、系统资源和内存

  • 文档完善,易于阅读、维护和扩展

  • 遵循一致的风格指南

  • 使用明确的设计,遵循已建立的软件工程惯例

  • 经过充分测试且健壮

  • 按时完成并且预算内

虽然Write Great Code系列的第 1 卷和第 2 卷涉及许多与优秀代码相关的效率方面,系列中的其他书籍,从这一卷开始,专注于创建符合前述标准的代码。

程序员分类

为了理解是什么让一个程序员变得伟大,让我们首先考虑业余程序员、各级程序员和软件工程师之间的差异。

业余程序员

业余程序员是自学成才,经验有限,因此是伟大程序员的对立面。在计算机的早期,这些程序员被称为黑客。这个术语今天已经演变成几种不同的含义,不一定指没有足够教育或经验做专业软件工程的程序员。

业余程序员编写的代码问题在于,他们通常是为自己或朋友编写代码;因此,这些代码通常不符合当代软件工程项目的标准。然而,业余程序员可以通过一些教育提升自己的水平(WGC系列可以提供帮助)。

程序员

计算机程序员拥有广泛的经验和职责,这通常反映在职称中,如初级程序员、编码员、程序员 I 和 II、分析师/系统分析师和系统架构师。我们在这里探讨其中一些角色及其差异。

实习生

通常,实习生是兼职的学生,他们被分配所谓的苦力活——如运行一套固定的测试程序或编写软件文档。

初级程序员

应届毕业生通常担任初级程序员职务。通常,他们从事测试或维护任务。很少有机会参与新项目;相反,他们大部分编程时间都用于重写现有代码或处理遗留代码。

编码员

程序员在获得足够经验后晋升为编码员,管理层也开始信任他们进行项目的新代码开发。一名更资深的程序员将(较不复杂的)子组件分配给编码员,以帮助加快项目的完成速度。

程序员 I 和 II

随着程序员积累更多经验,并能独立处理复杂的实现任务,他们从编码员晋升为程序员 I,再到程序员 II。系统分析师通常可以为程序员 I 或 II 提供一个大致的需求,程序员能够填补细节并生成符合系统分析师预期的应用程序。

系统分析师

系统分析师研究问题并确定最佳的解决方案实现方式。通常,系统分析师选择要使用的主要算法并创建最终应用的组织结构。

系统架构师

系统架构师决定系统分析师在大型系统中设计的各个组件如何协同工作。通常,系统架构师会指定流程、硬件及其他与软件无关的部分作为整体解决方案的一部分。

完整的程序员

一名完整的 程序员是这些子领域的融合。也就是说,一名完整的程序员能够研究问题、设计解决方案、使用编程语言实现解决方案并测试结果。

程序员分类问题

实际上,大多数程序员分类都是人为设定的;它们的存在仅仅是为了为初级程序员和有经验的程序员设定不同的薪酬标准。例如,系统分析师设计特定应用的算法和整体数据流,然后将设计交给编码员,由他们用特定的编程语言实现。我们通常将这两个任务与编程联系在一起,但初级程序员没有足够的经验从零开始设计大型系统,尽管他们完全有能力将设计转化为适当的编程语言。系统分析师和架构师通常拥有处理整个项目的经验和能力。然而,管理层通常认为让他们处理那些需要经验的项目部分比让他们做低级别的编码更具成本效益,而后者是应届毕业生也能做的(且成本更低)。

软件工程师

在工程领域,工程师通过遵循一套规定的规则来处理特定问题,利用预定解决方案的组合来构建定制的解决方案。这种方法使得即使是技术不太出众的工程师也能在不从零开始开发系统的情况下,产出有效的解决方案。软件工程作为一种尝试,通过将传统工程概念应用于软件开发,最大化整个编程团队的价值而产生。总体来看,软件工程革命是成功的。拥有正确培训和领导的工程师可以在比以往更短的时间和更少的资金投入下,编写出高质量的代码。

纯粹的软件工程不鼓励发散性思维,因为它可能会浪费时间,并使工程师走上失败的道路(导致更高的开发成本和更长的开发时间)。一般来说,软件工程更关注按时和按预算开发应用程序,而不是以最佳方式编写代码。但是,如果软件工程师从不尝试新事物,他们通常会错失机会,无法产生优秀的设计,永远不会开发出新的实践,也无法成为伟大的程序员。

伟大的程序员

伟大的程序员意识到预算问题,但他们也明白,探索新想法和方法对于推动这个领域的发展非常重要。他们知道何时必须遵循规则,但也知道何时可以打破(或至少弯曲)规则。但最重要的是,伟大的程序员充分利用自己的技能集,取得仅凭思维局限无法实现的成果。黑客天生如此,软件工程师是后天培养的,而伟大的程序员则是两者的结合体。他们有三个主要特点:对工作有真正的热爱、持续的教育和培训,以及在解决问题时能够跳出框架思考的能力。

热爱你所做的,做你所爱的

人们往往在自己喜欢的任务上表现出色,而在自己不喜欢的活动上表现不佳。底线是,如果你讨厌计算机编程,你将不会成为一个很好的程序员。如果你天生没有解决问题和克服挑战的欲望,再多的教育和培训也无法改变你的性格。因此,成为一名伟大的程序员最重要的前提是你真正热爱编写计算机程序。

优先考虑教育和培训

伟大的程序员喜欢这个领域所要求的任务,但他们还需要其他东西——正规教育和培训。我们将在后续章节中更深入地讨论教育和培训,但现在可以说,伟大的程序员受过良好的教育(或许拥有大专以上学位),并且在整个职业生涯中持续接受教育。

跳出框架思考

如前所述,按照预定的规则生成代码是软件工程师的典型期望。然而,正如你将在第一章中看到的,要成为一名伟大的程序员(“大宗师程序员”),你需要愿意并能够设计出新的编程技巧,这些技巧来源于发散性思维,而不是盲目遵循规则。伟大的程序员天生有推动边界并探索新解决方案的渴望。

所以你想成为一名伟大的程序员

总结来说,如果你想成为一名真正伟大的程序员并赢得同行的敬佩,你需要具备以下几点:

  • 对计算机编程和问题解决的热爱

  • 基于大学或高等院校学位的广泛计算机科学知识^(1)

  • 对教育和培训的终身承诺

  • 在探索解决方案时,具备跳出框框思考的能力和意愿

  • 个人对卓越表现的渴望和动力,以及始终力求做到最好

具备这些特质,阻碍你成为一名伟大程序员的唯一因素就是更多的知识。这就是本书的作用所在。

关于伦理与品格的最终说明

软件工程师的工作是根据冲突的需求,通过在系统设计中做出适当的妥协来创造最佳的产品。在这个过程中,工程师必须优先考虑需求,并在项目的限制下选择最佳解决方案。伦理和个人品格常常会影响个体在处理复杂项目时,尤其是压力较大的项目时做出的决策。做出不诚实的智力决策(例如,虚报项目估算或声称某个软件在未经充分测试的情况下就可以工作),盗版软件开发工具(或其他软件),在软件中引入未经文档化的功能(如后门)而未获得管理层批准,或采纳精英主义态度(认为自己比其他团队成员更优秀)都是软件工程伦理失范的例子。践行正确的道德判断和良好的伦理将使你成为更好的人,也会让你成为更优秀的程序员。

更多信息

Barger, Robert N. 《计算机伦理学:基于案例的方法》。剑桥,英国:剑桥大学出版社,2008 年。

Floridi, Luciano, ed. 《剑桥信息与计算机伦理学手册》。剑桥,英国:剑桥大学出版社,2006 年。

Forester, Tom, 和 Perry Morrison. 《计算机伦理:计算中的警示故事与伦理困境》。第二版。剑桥,马萨诸塞州:麻省理工学院出版社,1993 年。

Parker, Donn B. “信息处理中的伦理规则。” 《ACM 通讯》 11,第三期(1968 年):198-201。dl.acm.org/doi/10.1145/362929.362987

Wiener, Norbert. 《人类对人类的使用:控制论与社会》。波士顿:霍顿·米夫林·哈考特出版社,1950 年。

WikiWikiWeb. “大师级程序员。”最后更新时间为 2014 年 11 月 23 日。c2.com/cgi/wiki?GrandMasterProgrammer/

第一章:**第一部分

个人软件工程**

第二章:软件开发隐喻

Image

我们如何定义软件开发过程?这个问题看起来可能有些傻。为什么不直接说“软件开发就是软件开发”然后就算了呢?其实,如果我们能够将软件开发任务与其他专业领域进行类比,就能更深入地理解软件开发过程。然后我们可以通过研究相关领域的过程改进来优化软件开发过程。为此,本章将探讨一些理解软件开发的常见方式。

1.1 什么是软件?

为了更好地理解程序员如何创建软件,我们可以将软件与其他人类创造的事物进行比较。这样做将为我们提供重要的见解,帮助我们理解为什么某些创意隐喻适用于软件开发,或者为什么不适用。

在他的著作《软件工程:初学者的入门》中,罗伯特·普雷斯曼列出了软件的几个特点。本节将探讨这些特点,以阐明软件的本质以及它如何定义计算机程序员的工作。

1.1.1 软件不是制造出来的

软件是开发或工程化的;它不像传统意义上的产品那样被制造出来。

—罗伯特·普雷斯曼

与硬件产品相比,软件产品的制造成本非常低:刻录一张 CD 或 DVD 的费用仅为几分钱,再加上一小部分运输和处理费用(而电子分发的成本更低)。此外,软件设计对制造的 CD/DVD 的质量或最终成本几乎没有影响。假设制造厂有合理的质量控制,计算机程序员在设计软件应用时很少需要考虑制造问题。^(1) 与其他工程专业相比,工程师必须考虑如何设计产品的可制造性

1.1.2 软件不会磨损

软件和硬件都可能因为设计不良而在产品生命周期早期出现故障。然而,如果我们能够消除产品中的设计缺陷(即交付一款没有缺陷的软件或硬件),那么二者之间的区别就变得明显了。一旦软件是正确的,它就不会出现故障或“磨损”。只要基础计算机系统正常运行,软件将继续工作。^(2) 与硬件工程师不同,软件工程师不需要担心设计出能够轻松更换随时间失效的组件。

1.1.3 大多数软件是定制开发的

大多数软件是定制开发的,而不是从现有的[标准]组件中组装而成。

—罗伯特·普雷斯曼

尽管有很多尝试旨在创建类似的标准化软件组件,以供软件工程师将其组装成大型应用程序,但软件集成电路(即电子集成电路的等效物)的概念从未实现。软件库和面向对象编程技术鼓励重用预先编写的代码,但从较小的预组装组件构建大型软件系统的前提未能产生接近硬件设计所能实现的成果。

1.1.4 软件可以轻松升级

在许多情况下,完全可以在现场用新版本(甚至是完全不同的应用程序)替代现有的一个软件应用程序,而不会产生巨大的成本。^(3) 应用程序的最终用户只需用新的软件替换旧的,并享受升级版本带来的好处。事实上,大多数现代软件系统和应用程序在正常运行过程中都会通过互联网自动更新。

1.1.5 软件不是独立存在的实体

软件不是独立的产品。电气工程师可以设计一个完全独立运行的硬件设备。然而,软件依赖于其他东西(通常是计算机系统)才能正常运行。因此,软件开发者在设计和实现软件应用程序时,必须遵循外部系统(计算机系统、操作系统、编程语言等)所施加的限制。

1.2 与其他领域的类比

计算机程序员常被与艺术家、工匠、工程师、建筑师和技术员相比。虽然计算机编程与这些职业没有完全相同的对应关系,但我们可以从这些领域中汲取有益的类比,获得它们所采用技术的启发。

1.2.1 程序员作为艺术家

在计算机编程的早期,软件开发被视为一种艺术。编写软件的能力——即将大量杂乱无章的东西整理成一个可工作的程序——似乎是一种上天赋予的天赋,只有少数人具备,就像大师级画家或音乐天才一样。(事实上,许多轶事证据表明,音乐家和计算机程序员在进行创作活动时,使用的是大脑相同的区域,而且相当一部分程序员曾是或现在是音乐家。^(4))

那么,软件开发是否真的是一种艺术形式?艺术家通常被定义为拥有某些天赋并能够以创造性方式使用这些天赋的人。这里的关键字是天赋,即一种与生俱来的能力。由于不是每个人天生都具备相同的天赋,所以并不是每个人都能成为艺术家。根据这个类比,似乎如果你想成为一名程序员,你必须天生具备这种能力;事实上,有些人似乎天生就具有编程的天赋或才能。

"程序员作为艺术家"的比喻似乎适用于那些最优秀的程序员。尽管艺术家遵循自己的一套规则来创作高质量的艺术作品,但他们往往在突破规则、探索新的创作领域时,创造出最具非凡性的作品。类似地,最优秀的程序员熟悉良好的软件开发规则,但也愿意尝试新技术,以期改进开发过程。就像真正的艺术家不满足于复制现有的作品或风格一样,"程序员作为艺术家"更愿意创造新的应用程序,而不是重复做一个旧版的程序。

注意

计算机科学中最受尊敬的教科书系列之一是 Donald Knuth 的《计算机程序设计的艺术》。显然,编程作为一种艺术形式的概念在计算机科学领域中根深蒂固。

1.2.2 程序员作为架构师

艺术家比喻非常适用于小型项目,在这些项目中,艺术家创造理念并实现一件艺术作品,就像程序员设计并实现一个小型软件系统一样。然而,对于更大的软件系统,"程序员作为架构师"的类比可能更为贴切。建筑师设计结构,但将实施工作留给他人(因为通常一个人无法完成所有建造工作)。在计算机科学中,那些为他人设计系统以便实施的人通常被称为程序员/分析师

建筑师对项目行使大规模的创意控制。例如,设计一座豪华建筑的建筑师定义它的外观、使用的材料以及施工工人需要遵循的指导方针,但不负责施工本身。建筑师可能会监督建筑(就像程序员/分析师审查他人添加到软件系统中的模块一样);然而,建筑师并不拿起锤子或操作起重机。

这个类比似乎不适用于小型项目,但如果允许个人"更换角色",它同样适用。也就是说,在项目的第一阶段,程序员戴上他们的架构师/程序员/分析师帽子,创造系统的设计。然后,程序员换上他们的程序员/编码员帽子来实现该系统。

"程序员作为架构师"这一范式在"程序员作为艺术家"模型之上增加了验证和安全措施。当艺术家绘画、创作音乐或雕塑时,他们通常不会担心这件作品是否符合除他们自己外的任何要求。此外,他们也不必担心这件艺术品可能会对生命或财产造成物理伤害^(5)。另一方面,建筑师必须考虑物理现实,且糟糕的设计可能导致伤害或损害。"程序员作为架构师"的范式为程序员的任务引入了个人责任、审查(测试)和安全性。

1.2.3 程序员作为工程师

1968 年北约会议挑战了“优秀的程序员天生就有,而非通过培养”的观点。正如本书介绍中所提到的,世界正面临软件危机——新软件应用的需求远远超过了程序员能够被训练出来的速度。因此,北约赞助了 1968 年的会议,创造了软件工程这一术语,来描述通过将工程学原理应用于计算机编程的混乱世界,来解决这一问题。

工程师关注的是以具有成本效益的方式解决实际问题,无论是在设计工作量方面,还是在生产成本方面。正因为如此,再加上工程职业历史悠久(特别是机械和化学工程领域),因此多年来为工程师们创造了大量程序和政策,以简化他们的工作。

在许多今天的工程领域中,工程师的任务是将大型系统从较小的预设计构件中构建出来。一位想要设计计算机系统的电气工程师不会从设计定制晶体管或其他小型部件开始;相反,他们会使用预设计的中央处理器(CPU)、存储单元和输入输出设备,将它们组装成一个完整的系统。同样,一位机械工程师可以使用预设计的桁架和基座来设计一座新桥。设计重用是工程职业的标志。它是生产安全、可靠、功能齐全且具成本效益的设计的关键要素之一,也是尽可能快速完成设计的重要因素。

软件工程师还遵循一套明确的程序和政策,从较小的预定义系统构建出大型系统。事实上,电气和电子工程师协会(IEEE)将软件工程定义如下:

采用一种系统化、纪律化、量化的方式进行软件的开发、操作和维护;也就是将工程学应用于软件。

1.2.4 程序员作为工匠

工匠模型介于艺术家和工程师之间。这个范式的核心思想是程序员是独立的个体;也就是说,软件工匠的比喻认识到人是重要的。增加人手和制定严格的规则并不会提高软件的质量,而是更好地培训个体,允许他们发挥自己的天赋和技能,才能做得更好。

传统工匠的开发过程与软件工匠的过程之间有许多相似之处。像所有工匠一样,软件工匠从学徒实习生开始。学徒在另一位工匠的密切指导下工作。学会了基本技能后,学徒程序员成为熟练工,通常在其他程序员的团队中工作,且由一位软件工匠进行监督。最终,程序员的技能提升到一定程度后,他们会成为大师工匠

工匠模型为那些有志成为优秀程序员的程序员提供了最好的比喻。我将在本章稍后的部分中回到这一比喻,具体在第 13 页的“软件工艺”部分。

1.2.5 艺术家、建筑师、工程师还是工匠?

要写出优秀的代码,你必须理解什么才是优秀的代码。在编写代码时,你需要使用最好的工具、编码技巧、程序、流程和政策。此外,你还必须不断增加自己的知识,改进所使用的开发流程,从而提升你所开发软件的质量。这就是为什么考虑不同的软件开发方法、理解软件产品并选择最佳方法如此重要的原因。

你需要努力学习如何编写优秀的代码,然后再努力去编写它。一个优秀的软件开发者会采纳各个领域中行之有效的思想,摒弃那些行不通的部分。总结一下:

  • 优秀的艺术家通过练习自己的技能来发展他们的才能。他们进行发散性思维,探索传递信息的新方式。

  • 优秀的建筑师懂得如何利用现有设计和标准组件,构建定制对象。他们理解成本限制、安全问题、需求,以及为了确保可靠运行所需的过度设计。优秀的建筑师理解形式与功能之间的关系,以及满足客户需求的重要性。

  • 优秀的工程师认识到一致性的重要性。他们记录并自动化开发步骤,以避免遗漏任何步骤。像建筑师一样,工程师鼓励重用现有设计,提供更强大且具成本效益的解决方案。工程学提供了帮助克服项目中个人局限性的程序和政策。

  • 优秀的工匠在大师的指导下训练和练习技能,最终目标是成为一名大师级工匠。这一比喻强调了个体的素质,如他们解决问题的能力和组织能力。

1.3 软件工程

自 20 世纪 60 年代末软件工程兴起以来,它已经成为一个无可争议的成功。今天,几乎没有专业程序员会接受那个时代“标准程序”中的编程噩梦。现代程序员理所当然接受的概念——如结构化编程、适当的程序布局(如缩进)、注释和良好的命名政策——都源于软件工程的研究。事实上,数十年的这类研究极大地影响了现代编程语言和其他编程工具。

软件工程已经存在了很长时间,并且对计算机编程的各个方面产生了深远影响,以至于许多人认为软件工程师一词与计算机程序员同义。毫无疑问,任何专业的软件工程师应该也是一名合格的计算机程序员,但计算机编程仅构成软件工程的一小部分。软件工程主要涉及经济学和项目管理。有趣的是,那些负责管理项目、保持进度、选择使用的方法论等的人并不叫软件工程师;他们被称为经理、项目负责人以及其他表示权威职位的头衔。同样,我们所称的软件工程师实际上并不做软件工程工作——他们只是编写由真正的软件工程师(经理和项目负责人)指定的代码。这或许就是为什么“软件工程”一词如此令人困惑的原因。

1.3.1 正式定义

没有一个软件工程的定义能够满足所有人的需求。不同的作者会加入他们自己的“见解”,使他们的定义与其他文本中的定义略有不同(或大不相同)。本书之所以命名为工程软件,是因为我希望避免再为这一概念添加另一个定义。作为提醒,IEEE 将软件工程定义为:

对软件的开发、运营和维护应用一种系统化、规范化、可量化的方法;也就是说,将工程学应用于软件。

我使用的原始软件工程定义是:

软件工程是关于大型软件系统的开发与管理的研究。

这里的关键术语是大型。软件工程的进展大多由国防合同等资助,因此软件工程与大型系统几乎是同义词。IEEE 的定义可以适用于几乎任何规模的系统,但由于大多数关于软件工程的研究都涉及非常大型的系统,我更倾向于采用第二种定义。

注意

为了避免与通用的软件工程一词混淆,我使用一个更为专业的术语,个人软件工程,来描述那些适用于单个程序员在一个小型项目或大型项目的小部分上工作的过程和方法论。我的目的是描述计算机程序员认为的软件工程的本质,而不涉及那些与编写优秀代码无关的多余细节。

在软件开发中,人们对于“庞大”的定义完全不同。一名计算机科学专业的本科生可能认为一个包含几千行源代码的程序是一个大型系统。而对于波音(或其他大型公司)的项目经理来说,一个大型系统的代码行数通常超过一百万行。我上次统计的时候(那已经是很久以前的事了),微软的 Windows 操作系统(OS)超过了五千万行源代码;没有人会怀疑 Windows 是一个大型系统!

由于传统的软件工程定义通常适用于大型软件系统,我们需要提出一个合理的大型(和小型)软件系统定义。尽管代码行数(LOC)是软件工程师常用来描述软件系统大小的指标,但它是一个低质量的指标,具有接近两个数量级的误差范围。^(6) 本书将经常使用 LOC 或千行代码(KLOC)指标。但依赖这样一个糟糕的指标来构建正式定义并不是一个好主意。这样做会削弱定义的严谨性。

1.3.2 项目大小

小型项目是指一个普通程序员可以在合理的时间内(少于两年)完成的项目。中型项目则太大,个体在合理的时间内无法完成,但一个由两到五名程序员组成的小团队能够完成它。大型项目则需要一个庞大的程序员团队(超过五人)。就 LOC 而言,小型项目大约包含 50 到 100 KLOC;中型项目的 LOC 在 50 到 1000 KLOC(即一百万行源代码)之间;而大型项目的 LOC 则从约 500 到 1000 KLOC 起步。

小型项目很容易管理。因为小型项目不需要程序员之间的互动,也几乎不需要程序员与外界的互动,生产力几乎完全依赖于程序员的能力。

中型项目带来了新的挑战。因为有多名程序员在参与项目,沟通可能会成为一个问题,但团队足够小,这种开销是可以管理的。尽管如此,团队内部的动态仍需要额外支持,这增加了每行代码编写的成本。

大型项目需要一个庞大的程序员团队。沟通和其他开销往往消耗每个工程师 50%的生产力。有效的项目管理至关重要。

软件工程处理需要大型程序员团队成功管理项目的方法、实践和政策。不幸的是,适用于个人甚至小团队的实践,无法扩展到大型团队,而大型项目的方法、实践和政策也无法缩小到适应小型和中型项目。适用于大型项目的实践通常会给小型和中型项目带来不合理的开销,降低这些小团队的生产力。

让我们更深入地了解不同规模项目的一些优缺点。

1.3.2.1 小型项目

在小型项目中,单个软件工程师完全负责系统设计、实施、测试、调试、部署和文档编写。在这样的项目中,单一工程师需要负责的任务远多于中型或大型项目中的单个工程师。但这些任务相对较小,因此是可管理的。由于小型项目要求个人承担广泛的任务,程序员必须具备多样化的技能。个人软件工程学涵盖了开发者在小型项目中会做的所有活动。

小型项目能够最有效地利用工程资源。工程师可以采用最具生产力的方法来解决问题,因为他们不必与项目中的其他工程师达成共识。工程师还可以优化他们在每个开发阶段所花费的时间。在结构化的软件设计流程中,通常会花费大量时间进行操作文档记录,但当项目中只有一个程序员时,这显得不太合理(尽管在产品生命周期的后期,可能会有其他程序员需要与代码合作)。

小型项目的缺点和陷阱在于,工程师必须能够处理所需的各种任务。许多小型项目失败(或其开发成本过高),因为工程师缺乏足够的培训来独立处理整个项目。Write Great Code系列的目标之一,甚至比其他任何目标都更加重要,就是教会程序员如何正确地完成小型项目。

1.3.2.2 中型项目

在一个中型项目中,个人软件工程包括了项目中由单个工程师负责的那些方面。这通常包括他们系统组件的设计、实现(编码)以及该模块的文档。通常,他们还负责对其组件进行测试(单元测试),然后由整个团队进行系统测试(集成测试)。通常会有一名工程师负责完整的系统设计(项目负责人首席程序员),并负责部署。根据项目的不同,技术文档可能由技术写作人员来负责。由于工程师在中型项目中共享任务,因此可以实现专业化,项目也不要求每个工程师都能完成所有单独的任务。首席程序员可以指导经验较少的工程师,以确保项目的整体质量。

在一个小型项目中,单个工程师能够看到整体情况,并根据对整个项目的理解优化某些活动。而在一个大型项目中,单个工程师对除自己负责的小部分外,其他部分几乎不了解。中型项目则提供了这两种极端的混合:个人可以看到大部分整个项目,并根据实际情况调整他们的系统实施方法。同时,他们也可以在不被其他系统细节压倒的情况下,专注于系统的某些方面。

1.3.2.3 大型项目

在大型项目中,各团队成员有着更为专业化的角色,从系统设计到实现、测试、文档、部署以及系统的增强和维护。与中型项目一样,在大型项目中,个人软件工程只包括程序员自己负责的那些活动。大型项目中的软件工程师通常只做几项任务(例如编码和单元测试),因此他们不需要像小型项目中的单个工程师那样广泛的技能。

除了活动的范围外,项目的大小也会影响工程师的生产力。在大型项目中,工程师可以变得非常专业化,专注于自己擅长的领域。这使得他们能够比起使用更广泛的技能集时,更高效地完成工作。然而,大型项目必须使用统一的软件开发方法才能有效,而一些工程师可能因为不喜欢这种方法而生产力下降。

1.3.3 软件工程失败的原因

确实可以将工程技术应用于软件开发,以更具成本效益的方式生产应用程序。然而,正如皮特·麦克布林在《软件工艺:新的必然性》中所指出的,软件工程面临的最大问题是认为“系统化、严谨、可量化的方法”是唯一合理的途径。事实上,他提出了一个非常好的问题:软件开发是否真的可能变得系统化和量化?引用 www.controlchaos.com/,麦克布林说:

如果一个过程可以被完全定义,所有相关内容都已知,可以设计并反复执行,且结果可预测,那么它被称为已定义过程,并且可以进行自动化。如果一个过程的所有内容并不完全已知——只知道当你混合这些输入时通常会发生什么,以及如何测量和控制以获得期望的输出——这些则被称为经验过程。

软件开发不是一个已定义过程;它是一个经验过程。因此,软件开发不能完全自动化,且通常很难将工程原理应用于软件开发。问题的一部分在于,实际的工程工作高度依赖于现有设计的重用。尽管在计算机编程中也可以进行大量的重用,但它需要比其他工程领域更多的定制化。

软件工程的另一个重要问题,正如本书引言中简要讨论的那样,是软件工程将软件工程师视为可以随意调换进出项目的商品资源,这忽视了个体才能的重要性。问题并不在于工程技术从未有价值,而在于管理层试图将这些技术普遍应用于每个人,并鼓励使用当前的一些“最佳实践”进行软件开发。这种方法能够产生高质量的软件,但它不允许跳出框架思考,去创造可能更好的新实践。

1.4 软件工艺

软件工艺,指程序员在大师的指导下培养和实践技能,是为了终身学习,成为最优秀的软件开发者。遵循工艺模型,程序员接受教育,完成学徒期,成为中级程序员,并努力开发出杰作。

1.4.1 教育

大学为实习生成为软件工匠提供了必备的基础。如果一个实习生(学徒)能够接触到和正规教育相同的信息和挑战,那么这次实习可能与正式教育等效。不幸的是,很少有软件工匠有时间或能力从零开始培养一个学徒。他们太忙于处理实际项目,无法投入足够的时间来教导实习生所有他们需要知道的内容。因此,教育是通向软件工匠之路的第一步。

此外,大学的正规教育实现了两个主要目标:首先,你必须学习那些如果你自己学习可能会跳过的计算机科学主题;其次,你向全世界证明了你有能力完成一个你开始的重大承诺。特别是,在完成正规的计算机科学课程后,你已准备好真正开始学习软件开发。

然而,无论学位多么高级,都无法自动将你资格化为软件工匠。拥有研究生学位的人,因其需要深入且专门化的计算机科学研究,和拥有本科学位的人一样,也从实习生做起。拥有研究生学位的实习生可能作为学徒的年限较短,但仍然需要大量的培训。

1.4.2 学徒制

完成正规的计算机科学课程为你准备了以学徒身份开始学习如何成为一名工匠的基础。典型的计算机科学课程会教授你编程语言(其语法和语义)、数据结构、编译器、操作系统等理论,但不会教授你如何编程,除非是在第一或第二学期的编程入门课程中。学徒训练则展示了当你进入实际工作后,编程究竟是什么。学徒训练的目的是获得必要的经验,以便将你所学的知识用来从多种不同角度解决问题,并尽可能多地积累不同的经验。

学徒在一位掌握了高级编程技巧的人指导下学习。这个人可以是软件学徒(见下一节)或者软件工匠。这位“导师”给学徒分配任务,展示如何完成任务,并检查学徒的工作,做出适当的中途调整,以确保高质量的工作。最重要的是,学徒还需要检查导师的工作。这可以通过多种形式进行,包括测试、结构化演练和调试。关键是学徒要了解导师的代码是如何工作的。^(7) 通过这样做,学徒能够掌握一些自己无法独立掌握的编程技巧。

如果学徒足够幸运,他们将有机会在几位大师的指导下学习,并从他们那里学习到扎实的技术。每完成一个项目,学徒就会在高级程序员的指导下接近学徒生涯的尽头,并进入软件工匠之路的下一阶段:软件中级工匠。

从某种意义上说,学徒生涯永远不会结束。你应该始终关注新的技术和新技能。例如,考虑那些从结构化编程成长起来的软件工程师,他们曾经需要学习面向对象编程。然而,在某个时刻,你会达到这样一个阶段:你更多地使用已有的技能,而不是开发新的技能。到了那个时候,你开始把自己的智慧传授给他人,而不是从他人那里学习。此时,你和“大师们”合作的同事会觉得你已经准备好独立承担项目,无需帮助或监督。那时,你就成为了一名软件中级工匠。

1.4.3 软件中级工匠

软件中级工匠负责大部分软件开发工作。如其名所示,他们通常在不同项目之间流动,运用他们的技能解决应用问题。尽管软件开发者的教育从未结束,但软件中级工匠更专注于应用开发,而不是学习如何开发应用。

软件中级工匠还承担着另一个重要任务,那就是培训新的软件学徒。他们会审查学徒在项目中的工作,并与他们分享编程技术和知识。

一名软件中级工匠不断寻找能够改善软件开发过程的新工具和新技术。通过尽早采纳新的(但经过验证的)技术,他们保持领先于学习曲线,跟上当前趋势,避免落后。利用行业最佳实践为客户创造高效且具有成本效益的解决方案是这一阶段工艺的标志。软件中级工匠高效、知识丰富,正是大多数项目经理在组建软件团队时希望找到的类型的开发人员。

1.4.4 大师工匠

成为一名高级工匠的传统方式是创作一个杰作,这是使你脱颖而出的作品。一些(高端)软件杰作的例子包括 VisiCalc,^(8) Linux 操作系统,以及 vi 和 emacs 文本编辑器。这些产品最初是一个人的创意和创作,尽管后来涉及了数十甚至数百名不同的程序员。杰作不一定像 Linux 或某些 GNU 工具那样成名。然而,你的同行必须认可你的杰作作为解决问题的有用和创造性的解决方案。杰作也不一定是独立的原创代码。为操作系统编写复杂的设备驱动程序,或者在几个有用的方面扩展其他程序,完全可以算作是杰作。杰作的目的是创建一个告诉世界的作品:「我有能力制作严肃的软件——请认真对待我!」杰作让他人知道他们应该认真考虑你的意见,并信任你所说的话。

通常,高级工匠的领域是确定当前的最佳实践并发明新的实践。最佳实践描述了完成任务的最佳已知方法,不一定是绝对最佳方法。高级工匠研究是否有更好的方法来设计应用程序,认识到新技术或方法论对广泛应用的实用性,并验证该实践是否最佳,并将这些信息传达给他人。

1.4.5 软件工艺失败的地方

史蒂夫·麦康奈尔在他的经典软件工程著作代码大全中声称,经验是那些特征之一,其重要性并不像人们想象的那么大:“如果一个程序员在一两年后还没学会 C 语言,接下来的三年也不会有多大改变。”然后他问道:“如果你工作了 10 年,你获得了 10 年的经验还是获得了 1 年的经验 10 次?”麦康奈尔甚至暗示,书本学习可能比编程经验更重要。他声称,计算机科学领域变化如此之快,以至于在过去十年里,有 10 年编程经验的人错过了新程序员接触到的所有重要研究。

1.5 通向编写优秀代码的道路

写出优秀的代码并不是因为你遵循了一串规则。你必须做出个人决定,付出努力,确保你写的代码真的很出色。违反公认的软件工程原则是确保你的代码不优秀的好方法,但严格遵循这些规则也不能保证代码的伟大。一位经验丰富、严谨的开发者或软件工匠能够在两种方法之间游刃有余:在需要时遵循既定的实践,但当需要时,也敢于尝试不同的技术或策略。

不幸的是,一本书只能教给你规则和方法论。创造力和智慧是你需要自己培养的品质。本书教给你规则,并建议你何时可以考虑打破这些规则。然而,是否打破规则仍然取决于你自己。

1.6 更多信息

Hunt, Andrew, 和 David Thomas. 程序员修炼之道. Upper Saddle River, NJ: Addison-Wesley Professional, 1999.

Kernighan, Brian, 和 Rob Pike. 编程实践. Upper Saddle River, NJ: Addison-Wesley Professional, 1999.

McBreen, Pete. 软件工艺:新的命令. Upper Saddle River, NJ: Addison-Wesley Professional, 2001.

McConnell, Steve. 代码大全. 第二版. Redmond, WA: Microsoft Press, 2004.

———. 快速开发:驾驭混乱的软件进度. Redmond, WA: Microsoft Press, 1996.

Pressman, Robert S. 软件工程:实践者的方案. 纽约: McGraw-Hill, 2010.

第三章:生产力**

图片

在 1960 年代末期,显然仅仅培养更多的程序员并不能缓解软件危机。唯一的解决方案是提高程序员的生产力——也就是使现有的程序员能够编写更多的代码——这就是软件工程领域的起源。因此,研究软件工程的一个好的起点是理解生产力。

2.1 什么是生产力?

尽管“生产力”一词通常被描述为软件工程的基础,但令人惊讶的是,很多人对其有着扭曲的看法。问任何程序员关于生产力的定义,你一定会听到“代码行数”、“功能点”、“复杂性指标”等等。事实是,软件项目中的生产力概念并没有什么神秘的地方。我们可以将生产力定义为:

在单位时间内或以特定成本完成的单元任务数量。

这个定义的挑战在于如何定义一个单元任务。一个便捷的单元任务可能是一个项目;然而,项目在规模和复杂性上差异巨大。程序员 A 在给定时间内完成了三个项目,而程序员 B 仅在一个大项目的某个小部分上工作,这并不能告诉我们这两位程序员的相对生产力。因此,单元任务通常比整个项目要小得多。通常,它是像一个函数、一行代码,或者项目中的一个更小的组件。只要单元任务在不同项目之间保持一致,并且预期一个程序员在任何项目中完成单元任务所需的时间相同,那么具体的度量标准就无关紧要。一般来说,如果我们说程序员 A 的生产力是程序员 B 的n倍,那么程序员 A 在相同时间内可以完成n倍(等效的)项目,而程序员 B 完成一个项目所需的时间。

2.2 程序员生产力与团队生产力

1968 年,Sackman、Erikson 和 Grant 发表了一篇震撼人心的文章,声称程序员之间的生产力差异可达 10 到 20 倍。^(1) 后来的研究和文章将这一差距推得更高。这意味着某些程序员的代码产出是一些能力较弱的程序员的 20 倍(或更多)。一些公司甚至声称,在他们的组织中,不同软件团队之间的生产力差异可以达到两个数量级。这是一个惊人的差异!如果某些程序员的生产力是其他程序员的 20 倍(即所谓的“大师级程序员”[GMPs]),那么我们是否可以采用某些技术或方法,来提高普通(或低生产力)程序员的生产力呢?

因为无法训练每个程序员将其提升到 GMP(高效能程序员)水平,大多数软件工程方法论采用其他技术,如更好的管理流程,来提高大团队的生产力。本书系列采取另一种方法:不是尝试提高团队的生产力,而是教导个人程序员如何提高自己的生产力,并朝着成为 GMP 努力。

尽管单个程序员的生产力对项目交付进度的影响最大,但现实世界更关注项目成本——完成项目所需的时间和费用——而非程序员的生产力。除非是小型项目,团队的生产力优先于团队成员的生产力。

团队的生产力不仅仅是每个成员生产力的平均值;它基于团队成员之间的复杂互动。会议、沟通、个人互动和其他活动都会对团队成员的生产力产生负面影响,培训新成员或经验较少的成员、以及重构现有代码也会产生同样的影响。(这些活动带来的开销是程序员在小型项目中比在中型或大型项目中更具生产力的主要原因。)团队可以通过管理沟通和培训的开销、抵制重构现有代码的冲动(除非确实必要),以及管理项目使代码在第一次编写时就正确(减少重构的需要)来提高生产力。

2.3 人工工时与实际时间

前面给出的定义提供了两种生产力的衡量标准:一种基于时间(生产力是单位时间内完成的任务数量),另一种基于成本(生产力是给定成本下完成的任务数量)。有时成本比时间更重要,反之亦然。为了衡量成本和时间,我们可以分别使用人工工时和实际时间。

从公司的角度来看,项目成本中与程序员生产力相关的部分与其人工工时直接成正比,即每个团队成员在项目上花费的时间。一个工人日大约是 8 个人工工时,一个工人月大约是 176 个人工工时,一个工人年大约是 2,000 个人工工时。项目的总成本是项目上花费的人工工时总数乘以每个团队成员的平均时薪。

实际时间(也称为日历时间墙钟时间)就是项目进行过程中时间的推移。项目计划和最终产品的交付通常基于实际时间。

工时是实际时间与同时在项目上工作的团队成员数量的乘积,但优化其中一个数量并不总是能优化另一个。例如,假设你正在开发一个市政选举需要的应用程序。在这种情况下,最关键的数量是实际时间;软件必须在选举日期之前完全功能化并部署,无论成本如何。相比之下,一个“地下程序员”正在开发世界上下一个杀手级应用,他可以花更多时间在项目上,从而延长交付日期,但无法雇佣额外人员以更快完成应用。

项目经理在大型项目中常犯的一个最大错误是将工时与实际时间混淆。如果两个程序员可以在 2,000 工时(和 1,000 实际小时)内完成一个项目,你可能会得出结论,四个程序员可以在 500 实际小时内完成该项目。换句话说,通过将项目团队扩充一倍,你可以在一半的时间内完成项目,并按计划完成。然而,现实中,这并不总是奏效(就像增加第二个烤箱也不会让蛋糕更快烤好一样)。

增加人员以提高每个日历小时的工时数,通常在大型项目中比在小型和中型项目中更成功。小型项目的范围足够有限,单个程序员可以跟踪项目相关的所有细节;程序员无需咨询、协调或培训其他人来参与项目。通常情况下,向小型项目添加程序员会消除这些优势,并在不显著影响交付计划的情况下,显著增加成本。在中型项目中,平衡较为微妙:两个程序员的生产力可能比三个程序员更高,^(2) 但增加更多的编程资源可以帮助加快一个人手不足的项目的完成(尽管可能会以更高的成本)。在大型软件项目中,增加团队规模会相应缩短项目的计划时间,但一旦团队超过一定规模,可能需要添加两三个人来完成通常由一个人完成的工作。

2.4 概念和范围复杂性

随着项目变得更加复杂,^(3) 程序员的生产力会下降,因为更复杂的项目需要更深刻(且更长时间)的思考才能理解发生了什么。此外,随着项目复杂性的增加,软件工程师引入错误的可能性也更大,系统中早期引入的缺陷可能要到后来才会被发现,这时修复它们的成本会更高。

复杂性有几种形式。考虑以下两个“复杂”的定义:

  1. 拥有复杂、繁琐或错综复杂的部分排列,使得理解变得困难

  2. 由许多互相关联的部分组成

我们可以称第一个定义为概念复杂性。例如,考虑一个高层语言(HLL)中的单个算术表达式,如 C/C++,它可能包含复杂的函数调用、几个具有不同优先级的奇怪算术/逻辑运算符,以及许多使表达式难以理解的括号。概念复杂性可能出现在任何软件项目中。

我们可以称第二个定义为范围复杂性,当信息量过大,人类的思维无法轻松消化时就会发生。即使项目的各个组件很简单,项目的庞大规模也使得一个人无法理解整个项目。范围复杂性出现在中型和大型项目中(事实上,正是这种复杂性将小项目与其他项目区分开来)。

概念复杂性通过两种方式影响程序员的生产力。首先,复杂的结构需要更多的思考(因此也需要更多的时间)来实现,而比简单的结构复杂得多。其次,复杂的结构更容易包含缺陷,必须在后期进行修复,从而导致生产力的相应损失。

范围复杂性引入了不同的问题。当项目达到一定规模时,项目中的程序员可能完全不了解项目其他部分的进展,甚至可能重复编写系统中已有的代码。显然,这会降低程序员的生产力,因为程序员浪费了时间在编写那部分代码上。^(4) 由于范围复杂性,也可能会导致系统资源使用效率低下。在工作系统的某一部分时,一个小团队的工程师可能只测试自己的部分,但他们看不到它与系统其他部分的交互(可能这些部分甚至还没有准备好)。因此,系统资源使用的问题(如 CPU 周期或内存)可能直到后期才被发现。

通过良好的软件工程实践,部分复杂性是可以缓解的。但总体结果是一样的:随着系统的复杂度增加,人们必须花更多的时间去思考它们,并且缺陷的机会大幅增加。最终结果是生产力下降。

2.5 预测生产力

生产力是一个可以衡量并尝试预测的项目属性。当项目完成时,假设团队在项目开发过程中保持了准确的任务记录,判断团队(及其成员)的生产力是相对简单的。尽管过去项目的成功或失败不能保证未来项目的成败,但过去的表现是预测软件团队未来表现的最佳指标。如果你想改进软件开发过程,你需要跟踪哪些技术方法有效,哪些无效,这样你就能知道未来项目中该做什么(或不做什么)。为了追踪这些信息,程序员及其支持人员必须记录所有软件开发活动。这是软件工程引入的纯粹负担的一个很好的例子:文档几乎对当前项目的完成或提高质量没有帮助,但它是对未来项目的投资,帮助预测(和提高)生产力。

Watts S. Humphrey 的《软件工程的学科》(A Discipline for Software Engineering)(Addison-Wesley Professional, 1994)是一本很棒的书,适合那些有兴趣了解如何追踪程序员生产力的人。Humphrey 教授了一种形式、指导方针和程序的系统,来开发他称之为个人软件过程(PSP)的方法。尽管 PSP 主要面向个人,但它提供了宝贵的洞察,帮助发现程序员在软件开发过程中的问题所在。反过来,这能极大帮助他们确定如何攻克下一个重大项目。

2.6 指标及其必要性

通过观察团队或个人在类似项目中的过去表现来预测其生产力的问题在于,这种方法仅适用于类似的项目。如果新项目与团队过去的项目有显著不同,过去的表现可能不是一个好的指标。由于项目在规模上有很大差异,跨整个项目衡量生产力可能无法提供足够的信息来预测未来的表现。因此,需要某种低于整个项目粒度级别的衡量系统(指标),以更好地评估团队和团队成员。理想的指标应该独立于项目(团队成员、所选编程语言、所用工具及其他相关活动和组件);它必须能够跨多个项目使用,以便进行比较。虽然确实存在一些指标,但没有一个是完美的——甚至不是非常好的。尽管如此,一个不完美的指标总比没有指标好,因此软件工程师们会继续使用它们,直到出现更好的测量方法。在本节中,我将讨论几种较为常见的指标,以及每种指标的优缺点。

2.6.1 可执行文件大小指标

程序员用来指定软件系统复杂度的一个简单度量是最终系统中可执行文件的大小。^(5) 假设是复杂的项目产生大的可执行文件。

该度量的优点包括:

  • 它的计算非常简单(通常只需要查看目录列表并计算一个或多个可执行文件的总和)。

  • 它不需要访问原始源代码。

不幸的是,执行文件大小度量也存在一些缺陷,使其不适合大多数项目:

  • 可执行文件通常包含未初始化的数据,这些数据对文件大小的贡献与系统复杂度几乎没有关系。

  • 库函数增加了可执行文件的大小,但实际上它们减少了项目的复杂性。^(6)

  • 执行文件大小度量不是与语言无关的。例如,汇编语言程序通常比高级语言(HLL)可执行文件更为紧凑,但大多数人认为汇编程序比等效的 HLL 程序更复杂。

  • 执行文件大小度量不是与 CPU 无关的。例如,针对 80x86 CPU 的可执行文件通常比为 ARM(或其他 RISC)CPU 编译的同一程序要小。

2.6.2 机器指令度量

执行文件大小度量的一个主要缺陷是,某些可执行文件格式包括用于未初始化静态变量的空间,这意味着对输入源文件的微小更改可能会显著改变可执行文件的大小。解决这个问题的一种方法是仅计算源文件中的机器指令(要么是机器指令的大小,以字节为单位,要么是机器指令的总数)。尽管这种度量解决了未初始化静态数组的问题,但它仍然表现出执行文件大小度量的所有其他问题:它依赖于 CPU,它计算了程序员未编写的代码(如库代码),并且它与语言有关。

2.6.3 代码行数度量

代码行数(LOC,或者用于表示千行代码的 KLOC)度量是目前使用最广泛的软件度量方法。顾名思义,它是对项目中源代码行数的统计。该度量有一些优点,也有一些缺点。

单纯计算源代码行数似乎是使用 LOC 度量的最流行形式。编写一个行数统计程序相当简单,绝大多数操作系统(如 Linux)上可用的字数统计程序都可以为你计算行数。

以下是一些关于 LOC 度量的常见说法:

  • 无论使用什么编程语言,编写一行源代码所需的时间大致相同。

  • LOC 度量不受项目中使用库例程(或其他代码重用)的影响(当然,前提是不计算预编写的库源代码中的行数)。

  • LOC 度量与 CPU 无关。

LOC 度量方法确实有一些缺点:

  • 它无法很好地指示程序员完成了多少工作。100 行 VHLL 代码完成的工作比 100 行汇编代码要多。

  • 它假设每行源代码的成本相同。然而,事实并非如此。空白行成本微乎其微,简单的数据声明具有较低的概念复杂度,而包含复杂布尔表达式的语句则具有非常高的概念复杂度。

2.6.4 语句数度量

语句数度量计算源文件中的语言语句数量。它不计算空白行或注释,也不将分布在多行上的单个语句视为独立的实体。因此,它在计算程序员的工作量方面,比 LOC 更有效。

尽管语句数度量比代码行数提供了更好的程序复杂度视图,但它也存在许多相同的问题。它度量的是工作量而非完成的工作,它不像我们希望的那样独立于语言,并且它假设程序中的每条语句需要相同的工作量来编写。

2.6.5 功能点分析

功能点分析(FPA)最初是为了预测一个项目在编写源代码之前需要多少工作量而设计的。基本想法是考虑程序所需的输入数量、产生的输出数量以及必须执行的基本计算,并利用这些信息来确定项目的进度。^(7)

FPA(功能点分析)相比于行数或语句数等简化的度量方法,具有几个优势。它真正独立于语言和系统。它依赖于软件的功能性,而不是实现方式。

然而,FPA 确实存在一些严重的缺点。首先,与行数或语句数不同,计算程序中的“功能点”数量并非简单直接。分析是主观的:分析人员必须决定每个功能的相对复杂性。此外,FPA 从未成功实现自动化。这样的程序如何决定一个计算何时结束,另一个计算何时开始?它如何将不同的复杂度值(同样是主观分配)应用于每个功能点?由于这种手动分析相当耗时且成本高昂,FPA 并不像其他度量方法那样流行。在很大程度上,FPA 是一个事后分析(项目结束时的工具),通常在项目完成后而非开发过程中使用。

2.6.6 McCabe 的环形复杂度度量

如前所述,LOC 和语句计数度量的一个根本问题是它们假设每个语句具有相同的复杂性。FPA 稍微好一些,但需要分析师为每个语句分配复杂性评分。不幸的是,这些度量无法准确反映为完成工作所付出的努力,因此无法记录程序员的生产力。

Thomas McCabe 开发了一种被称为循环复杂度的软件度量方法,通过计算程序中的路径数量来衡量源代码的复杂性。它从程序的流程图开始。流程图中的节点对应程序中的语句,节点之间的边对应程序中的非顺序控制流。通过计算节点数量、边数量和流程图中连接组件的数量,可以为代码提供一个单一的循环复杂度评分。考虑一个包含 1,000 行printf语句的程序(没有其他内容);其循环复杂度为 1,因为程序中只有一条路径。现在考虑第二个例子,其中包含大量的控制结构和其他语句;它的循环复杂度评分将会更高。

循环复杂度度量是有用的,因为它是一个客观的衡量标准,而且可以编写程序来计算这个值。它的缺点是程序的体积大小无关紧要;也就是说,它把一个简单的printf语句和连续 1,000 个printf语句视为相同,尽管第二种情况显然需要更多的工作(即使这额外的工作只是大量的剪切和粘贴操作)。

2.6.7 其他度量

我们可以设计很多度量标准来衡量程序员生产力的某个方面。一个常见的度量是统计程序中的操作符数量。这个度量认识到并调整了某些语句(包括那些不涉及控制路径的语句)比其他语句更复杂,需要更多的时间来编写、测试和调试。另一个度量是统计程序中的标记数量(如标识符、保留字、操作符、常量和标点符号)。不过,无论是哪种度量,它们都会有缺陷。

许多人尝试使用多种度量标准的组合(例如,将行数与环形复杂度和操作符数相乘)来创建一个“多维度”的度量标准,以更好地衡量编写一段代码所涉及的工作量。不幸的是,随着度量标准复杂性的增加,它在给定项目上的使用变得更加困难。LOC(行数)之所以成功,是因为你可以使用 Unix 的wc(字数统计)工具,它也会统计行数,从而快速了解程序的大小。计算这些其他度量标准的值通常需要专门的、依赖语言的应用程序(假设该度量标准是可以自动化的)。因此,尽管人们提出了大量的度量标准,但很少有像 LOC 那样变得如此广泛应用。

2.6.8 度量标准的问题

大致衡量项目源代码量的度量标准,若假设每一行或每条语句的编写大致需要相同的时间,它们能很好地反映项目的工作量。然而,代码行数(或语句数)与实际完成的工作之间的关系是微弱的。不幸的是,度量标准衡量的是程序的一些物理属性,但很少衡量我们真正关心的内容:编写代码所需的智力劳动。

几乎所有度量标准的另一个失败之处在于,它们都假设更多的工作会产生更多(或更复杂)的代码。但这并不总是正确的。例如,一个优秀的程序员通常会花费时间重构代码,使其更简洁、更不复杂。在这种情况下,更多的工作反而会产生更少的代码(而且代码更简单)。

度量标准也未能考虑与代码相关的环境问题。例如,10 行代码写在裸机嵌入式设备上,是否等同于在 SQL 数据库应用程序中写的 10 行代码?

所有这些度量标准都未考虑某些项目的学习曲线。10 行 Windows 设备驱动程序代码是否等同于 10 行 Java 代码写在 Web 小程序中?这两个项目的 LOC 值是无法比较的。

最终,大多数度量标准都失败了,因为它们衡量的是错误的内容。它们衡量的是程序员所写的代码量,而不是程序员对完整项目的整体贡献(生产力)。例如,一个程序员可能用一条语句完成一个任务(比如标准库调用),而另一个程序员可能需要写几百行代码来完成同样的任务。大多数度量标准会认为第二个程序员的生产力更高。

正是由于这些原因,当前使用的最复杂的软件度量标准也存在根本性的缺陷,这些缺陷使得它们无法完全有效。因此,选择一个“更好的”度量标准往往得不到比使用“有缺陷”的度量标准更好的结果。这也是 LOC 度量标准继续流行的另一个原因(以及本书使用它的原因)。它是一个极其糟糕的度量标准,但它并不比许多其他现有的度量标准差多少,而且它非常容易计算,无需编写特别的软件。

2.7 我们如何突破每天 10 行代码?

早期的软件工程文献声称,一个参与重大产品的程序员每天平均写 10 行代码。在 1977 年的一篇文章中,Walston 和 Felix 报告了每位开发者每月约 274 行代码^(8)。这两个数字描述的是在该产品生命周期内(即从第一次发布到退役期间,所有程序员花费的时间总量)所产生的经过调试和文档化的代码,而不仅仅是每天编写代码所花费的时间。尽管如此,这些数字看起来还是偏低。为什么?

在项目开始时,程序员可能每天能迅速写出 1,000 行代码,然后为了研究项目某个部分的解决方案,测试代码,修复错误,重写一半代码,再进行文档编写,速度就会减慢。到产品的第一次发布时,生产力已经从最初的 1,000 行代码每天下降了十倍,变成了不到 100 行。产品第一次发布后,通常会开始进行第二次发布,然后是第三次,依此类推。在产品的生命周期中,可能会有几个不同的开发人员参与其中。到项目结束时,代码已经被重写了几次(这是生产力的巨大损失),并且几个程序员花费了宝贵的时间去学习代码的运行方式(这也在削弱他们的生产力)。因此,在产品的生命周期内,程序员的生产力降到了每天 10 行代码。

软件工程生产力研究的一个重要结果是,提高生产力的最佳方式并不是发明某种方案,让程序员在单位时间内写出两倍的代码行数,而是减少浪费在调试、测试、文档编写、重写代码以及在第一版代码存在后对新程序员进行培训的时间。为了减少这些损失,改进程序员在项目中使用的流程比训练他们在单位时间内写两倍代码要容易得多。软件工程一直认识到这个问题,并试图通过减少所有程序员花费的时间来解决它。个人软件工程的目标是减少个别程序员在项目中其部分工作的时间。

2.8 估算开发时间

如前所述,尽管生产力对管理层评定奖金、加薪或口头表扬等方面有意义,但跟踪生产力的真正目的是预测未来项目的开发时间。过去的结果并不能保证未来的表现,因此你还需要知道如何估算项目的时间表(或者至少是你在项目中的部分时间表)。作为一名独立的软件工程师,你通常没有足够的背景、教育和经验来确定时间表的内容,因此你应该与项目经理会面,让他们解释在时间表中需要考虑的因素(这不仅仅是编写代码所需的时间),然后以此为基础构建估算。尽管本书的范围超出了正确估算项目所需的所有细节(有关更多信息,请参见第 37 页中的“更多信息”部分),但简要描述一下开发时间估算如何因项目规模不同(无论是小型、中型、大型项目,还是仅仅是项目的一部分)而有所不同,仍然是有价值的。

2.8.1 估算小型项目开发时间

按照定义,小型项目是由一名工程师完成的。项目进度的主要影响因素将是该软件工程师的能力和生产力。

估算小型项目的开发时间比大型项目要容易得多且更为准确。小型项目不会涉及并行开发,时间表只需要考虑单个开发者的生产力。

毫无疑问,估算小型项目开发时间的第一步是识别并理解所有需要完成的工作。如果项目的某些部分在此时尚未定义,当这些未定义的部分不可避免地需要比你预想的更多时间时,时间表将会引入相当大的误差。

在估算项目完成时间时,设计文档是项目中最重要的部分。没有详细的设计,就无法知道项目由哪些子任务组成,也无法估算每个子任务需要多长时间。一旦你将项目分解为适当大小的子任务(适当的大小是指能清楚估计完成所需时间的子任务),你只需将所有子任务的时间加总,就能得到一个合理的初步估算。

然而,人们在估算小型项目时犯的最大错误之一是,他们将子任务的时间加在一起,并称其为自己的进度计划,却忘记考虑会议、电话、电子邮件和其他行政任务的时间。他们还忘记添加测试时间,以及在发现缺陷时纠正(并重新测试)软件的时间。由于很难估算软件中会有多少缺陷,以及解决这些缺陷需要多少时间,大多数经理会将进度计划的初步估算乘以 2 到 4 倍。假设程序员(团队)在项目中保持合理的生产力,这个公式可以为小型项目提供一个良好的估算。

2.8.2 估算中型和大型项目的开发时间

从概念上讲,中型和大型项目由许多小项目组成(分配给各个团队成员),这些小项目结合起来形成最终结果。因此,大型项目进度计划的初步估算是将其拆分为一堆较小的项目,为每个子项目开发估算,然后将这些估算加在一起。这实际上是小型项目估算的一个更大版本。不幸的是,在现实生活中,这种估算形式充满了错误。

第一个问题是,中型和大型项目引入了小型项目中不存在的问题。一个小型项目通常只有一名工程师,正如之前所提到的,进度完全依赖于该人的生产力和可用性。在较大的项目中,多个人员(包括许多非工程师)会影响估算的进度计划。一位掌握关键知识的软件工程师可能因度假或生病而缺席几天,导致另一名需要这些信息来推进工作的工程师被耽搁。较大项目中的工程师通常每周都有几次会议(在大多数进度计划中没有考虑到),这些会议使得他们不能进行编程工作—也就是说,他们需要脱离工作几个小时。大型项目的团队组成可能会发生变化;一些有经验的程序员离开,其他人必须接手并学习子任务,而新程序员加入项目时需要时间适应。有时,甚至为新员工提供一台计算机工作站也需要几周时间(例如,在一个有官僚主义的 IT 部门的大公司)。等待购买软件工具、开发硬件和获得组织其他部门的支持也会导致进度问题。这个问题清单不断延伸。很少有进度估算能够准确预测这些多种多样的方式会如何消耗时间。

最终,创建中型和大型项目的进度估算涉及四个任务:将项目拆分为较小的项目,对这些小项目进行估算,加入集成测试和调试的时间(即将小任务合并并使它们能正常协同工作),然后对这个总和应用一个乘数因子。它们并不精确,但目前的情况大致就是这样。

2.8.3 估算开发时间的问题

因为项目进度估算涉及预测开发团队未来的表现,几乎没有人相信预计的进度会完全准确。然而,典型的软件开发进度预测尤其不准确。以下是一些原因:

它们是研发项目。 研发项目涉及做一些你以前从未做过的事情。它们需要一个研究阶段,在此阶段,开发团队分析问题并尝试确定解决方案。通常,没有办法预测研究阶段将花费多少时间。

管理层有预设的进度安排。 通常,市场部门决定希望在某个日期推出一个可以销售的产品,管理层则通过倒推该日期来创建项目进度。在要求编程团队提供子任务的时间估算之前,管理层已经对每个任务的完成时间有了一些预设的看法。

团队以前做过这件事。 管理层通常假设,如果你以前做过某事,那么第二次会更容易(因此会节省时间)。在某些情况下,这个假设是有一定道理的:如果一个团队从事的是研发项目,第二次做会更容易,因为他们只需要进行开发,且可以跳过(至少大部分)研究。然而,假设一个项目第二次总是更容易的观点很少是正确的。

时间或资金不足。 在很多情况下,管理层会设定某种金钱或时间限制,要求项目必须在这个限制内完成,否则就会被取消。这对那些薪水依赖于项目推进的人来说是错误的说法。如果面临选择是说“是的,我们可以按时完成进度”,还是找新工作,大多数人——即使知道成功的几率不高——都会选择前者。

程序员往往夸大自己的效率。 有时当软件工程师被问及是否能在某个时间框架内完成一个项目时,他们并不是在谎报所需的时间,而是对自己的表现做出乐观估计——而这种估计在实际工作中很少成立。当被问及在真正高压的情况下能产出多少时,大多数软件工程师给出的数字代表了他们在短时间内最大产出的记录(例如,在“危机模式”下,每周工作 60 到 70 小时),而没有考虑到意外的障碍(比如突然出现的非常棘手的 bug)。

进度依赖于额外工作时间。 管理层(和工程师)常常认为程序员在进度开始延迟时,总能投入“额外的一些小时”。因此,进度计划往往比实际情况更为激进(忽视了让工程师加班的负面影响)。

工程师就像积木一样。 项目进度的一个常见问题是,管理层假设可以通过增加程序员来提前项目的发布日期。然而,正如之前提到的,这并不一定正确。你不能随意增减项目中的工程师,并期望项目进度会相应成比例地变化。

子项目估算不准确。 现实的项目进度是通过自上而下的方式制定的。整个项目被拆分为更小的子项目。然后,这些子项目再拆分为子子项目,依此类推,直到子项目的规模足够小,以便某人能够准确预测每个小部分所需的时间。然而,这种方法有三个挑战:

  • 愿意为这种方式制定计划付出努力(即提供一个正确且准确的自上而下的项目分析)

  • 获取准确的子项目估算(特别是来自那些可能没有适当管理培训的工程师,他们不理解在时间估算中需要考虑哪些内容)

  • 接受进度预测的结果

2.9 危机模式项目管理

尽管所有相关人员都抱有最好的意图,但许多项目仍会大幅落后于计划,管理层不得不加速开发以达到某个重要的里程碑。为了赶上截止日期,工程师通常被要求每周投入更多的时间,以减少(实际时间)交付日期。当这种情况发生时,项目就进入了“危机模式”。

危机模式的工程工作可以有效应对(快速)临近的截止日期,但总体来说,危机模式从来都不是特别高效,反而会导致生产力下降,因为大多数人工作之外还有其他事务需要处理,并且需要时间休息、放松大脑,让自己整理一下长时间工作中积累下来的问题。在疲劳状态下工作容易犯错误,而这些错误往往需要花费更多的时间来修正。从长远来看,放弃危机模式并坚持 40 小时工作周会更高效。

处理危机模式安排的最佳方法是在整个项目中添加里程碑,生成一系列“小危机”,而不是在项目结束时发生一次大危机。每个月增加一天或几天的加班,远比在项目结束时拼命工作几个七天的周要好得多。为了赶上最后期限,工作一两天 16 小时并不会对你的生活质量造成负面影响,也不会让你感到疲惫不堪。

除了健康和生产力问题,处于危机模式下还可能引发调度、伦理和法律问题:

  • 一个糟糕的工作安排也会影响到未来的项目。如果你每周工作 60 小时,管理层会认为未来的项目也可以在相同的(实际)时间内完成,并期望你未来以此节奏工作,而不支付额外的报酬。

  • 长期处于危机模式下运营的项目,其技术人员流动率较高,进一步降低了团队的生产力。

  • 还有一个法律问题,就是加班时间没有得到加班费的补偿。视频游戏行业中几起高调的诉讼案件表明,工程师有权获得加班费(他们并非免薪的员工)。即使你的公司能够挺过这些诉讼,时间报告、行政管理和工作时间的规定将变得更加严格,导致生产力下降。

再次强调,处于危机模式下如果管理得当,可以帮助你在某些时限内完成任务。但最好的解决方案是制定更合理的计划,避免完全进入危机模式。

2.10 如何提高生产力

本章花了相当多的时间定义生产力及衡量生产力的指标。但它并没有花费太多时间描述程序员如何提高自己的生产力,成为一名优秀的程序员。关于这个话题,整本书都可以写(并且确实有人写过)。本节提供了可以在个人和团队项目中提高生产力的技术概述。

2.10.1 明智选择软件开发工具

作为一名软件开发人员,你的大部分时间将花在使用软件开发工具上,工具的质量对你的生产力有着巨大的影响。遗憾的是,选择开发工具的主要标准似乎是对工具的熟悉度,而不是工具对当前项目的适用性。

在选择项目初期的工具时,请记住你可能需要在整个项目生命周期内(甚至可能是更长时间)使用它们。例如,一旦你开始使用缺陷跟踪系统,由于数据库文件格式不兼容,可能很难切换到另一个系统;源代码控制系统也是如此。幸运的是,如今软件开发工具(尤其是 IDE)相对成熟,而且很多工具之间能够互操作,所以很难做出错误的选择。尽管如此,在项目开始时仔细思考,可以避免将来出现许多问题。

对于软件开发项目来说,最重要的工具选择是选择哪种编程语言以及使用哪些编译器/解释器/翻译器。选择最佳语言是一个难题。选择一种编程语言往往很容易,因为你对它比较熟悉,不会因为学习它而失去生产力;然而,未来的新工程师可能会因为一边学习编程语言一边维护代码而大大降低生产力。此外,有些语言选择可以简化开发过程,显著提高生产力,足以弥补学习语言的时间。如前所述,选择错误的编程语言可能会导致浪费大量开发时间,直到你发现它不适合项目,不得不重新开始。

编译器性能(处理一个常见源文件所需的时间)对生产力有巨大影响。如果编译器编译一个普通源文件需要两秒钟,而不是两分钟,你可能会发现使用更快的编译器更能提高生产力(尽管更快的编译器可能缺少一些功能,这会在其他方面影响你的生产力)。工具处理代码的时间越少,你就能有更多的时间进行设计、测试、调试和优化代码。

同时,使用一组能很好协作的工具也非常重要。如今,我们理所当然地使用集成开发环境(IDE),它将编辑器、编译器、调试器、源代码浏览器以及其他工具整合成一个程序。在同一窗口中,能够快速在编辑器中进行小的修改、重新编译源代码模块并在调试器中运行结果,这对提升生产力有着显著的帮助。

然而,你常常需要在 IDE 外部处理项目的某些部分。例如,一些 IDE 不直接支持源代码控制或缺陷跟踪(尽管许多 IDE 支持)。大多数 IDE 没有提供用于编写文档的文字处理器,也没有提供用于维护需求列表、设计文档或用户文档的简单数据库或电子表格功能。你很可能需要使用一些 IDE 外部的程序——如文字处理、电子表格、绘图/图形、网页设计和数据库程序等等——来完成项目所需的所有工作。

在 IDE 外部运行程序并不是问题。只需确保你选择的应用程序与你的开发过程以及 IDE 生成的文件(反之亦然)兼容。如果你在 IDE 和外部应用程序之间传输文件时必须不断运行转换程序,你的生产力将会下降。

我能为你推荐使用的工具吗?绝对不行。项目需求各不相同,这里不可能考虑所有的建议。我的建议是,从项目开始时就要意识到这些问题。

但我给出的一个建议是,在选择开发工具时,避免采用“哇,为什么不试试这项新技术”这种方式。花费六个月时间使用一个开发工具后发现它根本无法完成任务(并且以它为基础编写了源代码)可能会造成灾难性的后果。在不涉及产品开发的情况下评估你的工具,只有在你确信它们能为你所用时才开始使用新工具。一个经典的例子就是苹果的 Swift 编程语言。直到 Swift v5.0 发布(大约是 Swift 首次推出四年后),使用 Swift 一直是一种令人沮丧的经历。每年苹果都会发布一个新版本,与早期版本的源代码不兼容,这迫使你回过头来修改旧程序。此外,早期版本的语言中缺少了许多功能,有些功能也还没准备好进入“黄金时间”。直到 5.0 版本(本书写作时发布),该语言才显得相对稳定。然而,那些早早加入 Swift 阵营的人,为语言的不成熟发展付出了代价。^(9)

可悲的是,许多项目中你并不能选择开发工具。这个决定通常是高层指令,或者你继承了早期产品的工具。抱怨无济于事,只会浪费时间和精力,降低你的生产力。相反,要善于利用现有工具集,并成为它的使用专家。

2.10.2 管理开销

在任何项目中,我们可以将工作分为两类:与项目直接相关的工作(例如编写代码或项目文档)和与项目间接相关的工作。间接活动包括会议、阅读和回复电子邮件、填写时间卡和更新进度。这些是开销活动:它们增加了项目的时间和成本,但并没有直接帮助完成工作。

通过遵循 Watts S. Humphrey 的个人软件工程指南,你可以追踪项目中的时间花费,并轻松看到自己在项目上的时间花费与在开销活动上的时间花费。如果你的开销时间超过了总时间的 10%,就应该重新考虑你的日常活动。尝试减少或合并这些活动,减少它们对生产力的影响。如果你不追踪项目外的时间,你就错过了通过管理开销来提高生产力的机会。

2.10.3 设定明确的目标和里程碑

人类有一种自然的倾向,在没有迫近的截止日期时会放松自己,然后在截止日期临近时进入“超高速模式”。如果没有目标,很少有有效的工作能够完成。如果没有截止日期,很少有人会有动力在及时的方式下达成目标。因此,为了提高生产力,确保自己设定清晰的目标和子目标,并为它们设定严格的里程碑

从项目管理的角度来看,里程碑是项目中的一个标记,用来确定工作进展的程度。一个优秀的经理总是会在项目进度中设定目标和里程碑。然而,少有进度表能够为单个程序员提供有用的目标。这就是个人软件工程发挥作用的地方。要成为一个高效的程序员,就需要在你负责的(项目部分)中精细管理自己的目标和里程碑。简单的目标,比如“在午饭前完成这个函数”或者“在今天下班前找到这个错误的源头”能帮助你保持专注。更大的目标,比如“在下周二前完成这个模块的测试”或者“今天至少执行 20 个测试程序”能帮助你衡量生产力,并确定自己是否达成了预期的目标。

2.10.4 自我激励的实践

提高生产力全靠态度。尽管他人可以帮助你更好地管理时间并在你遇到困难时提供帮助,但关键在于你必须主动去提升自己。始终关注自己的节奏,并不断努力提高自己的表现。通过追踪目标、努力和进展,你将能够在需要时“激励自己”,更加努力地提高生产力。

缺乏动力可能是生产力的最大障碍之一。如果你的态度是“唉,我今天必须做那个”,那么你完成任务的时间可能会比你抱着“哦!这是最有趣的部分!这一定很有意思!”的态度要长。

当然,并不是每个任务都能让你感到有趣和好玩。这正是个人软件工程发挥作用的地方。如果你想保持高于平均水平的生产力,当一个项目让你感到“缺乏动力”时,你需要有相当强的自我激励。试着创造一些理由让工作变得更有吸引力。例如,给自己设定小挑战,并在达成目标后奖励自己。一位高效的软件工程师不断地进行自我激励:你对一个项目的动力保持得越久,你的生产力就越高。

2.10.5 集中注意力并消除干扰

保持专注并消除干扰是提升生产力的另一种方法。进入“心流状态”。以这种方式工作的软件工程师比那些在脑海中同时处理多任务的人更具生产力。为了提高生产力,尽可能长时间地集中精力处理一项任务。

集中注意力最容易在一个安静的环境中,且没有任何视觉刺激(除了你的显示屏)。有时候,工作环境并不利于极度专注。在这种情况下,戴上耳机并播放背景音乐可能有助于消除干扰。如果音乐太分散注意力,可以尝试听白噪音;网上有好几个白噪音应用可供下载使用。

每当你在任务进行中被打断时,重新进入专注状态需要时间。事实上,可能需要长达半小时才能完全重新聚焦于工作。当你需要集中精力完成一项任务时,可以挂上告示,表示只有紧急事项才可打扰你,或者在你的工作区附近标明“办公时间”——你可以被打扰的时间;例如,你可以允许每小时开始时打扰五分钟。为了节省同事 10 分钟的时间,回答一个他们自己能解决的问题,可能会让你损失半小时的生产力。你确实需要与团队合作并做一个好的队友;然而,确保过度的团队互动不会影响你(以及他人)的生产力同样重要。

在一个典型的工作日中,会有许多预定的干扰:餐休、休息时间、会议、行政事务(例如处理电子邮件和时间记录)等。如果可能的话,尽量将其他干扰安排在这些事件周围。例如,关闭所有电子邮件提醒;在几秒钟内回复邮件是很少是紧急的,如果有紧急情况,别人可以亲自来找你或者给你打电话。如果人们确实需要你快速回复,设定闹钟提醒你在固定时间检查电子邮件(短信和其他干扰也一样)。如果你能做到,可以考虑将手机静音,如果你接到很多非紧急的电话,可以在休息时每小时检查一次消息。适合你的方法取决于你个人和职业生活的情况。但你所经历的干扰越少,你的工作效率就会越高。

2.10.6 如果感到无聊,做其他事情

有时候,不管你多么自我激励,你都会感到对手头的工作厌倦,难以集中注意力;这时,你的生产力会大幅下降。如果你无法进入工作状态并专注于任务,休息一下,换做其他工作。不要用无聊作为借口,在任务间徘徊而没有取得实质性进展。但当你真的卡住了,无法继续前进时,换一个你能有效完成的任务。

2.10.7 尽量保持自给自足

尽可能自己处理分配给你的所有任务。这不会提升你的工作效率;然而,如果你不断寻求其他工程师的帮助,可能会影响他们的生产力(记住,他们也需要保持专注,避免干扰)。

如果你正在处理一个需要比你当前知识更多的任务,并且不想不断打扰其他工程师,你有几个选择:

  • 花时间自学,尽量让自己能够完成任务。虽然这可能会影响你短期的生产力,但你获得的知识会帮助你完成未来的类似任务。

  • 和你的经理见面,解释你遇到的问题。讨论是否可以将任务重新分配给更有经验的人,并给你分配一个你能更好处理的任务。

  • 和你的经理安排一次会议,在不影响其他工程师工作效率的时间(例如工作日的开始)寻求他们的帮助。

2.10.8 识别何时需要帮助

你可能会把自我支持的态度推得有些过头。你可能会花费过多的时间解决一个队友只需要几分钟就能解决的问题。作为一个优秀的程序员的一个方面是认识到自己卡住了,需要帮助才能前进。当你遇到困难时,最好的方法是设置一个定时提醒。在被卡住一段时间(无论是几分钟、几小时,甚至几天)后,寻求帮助。如果你知道该向谁寻求帮助,就直接去找他们。如果不确定,找经理帮忙。大多数情况下,经理可以把你引导到正确的人那里,这样你就不会打扰那些无法帮助你的人。

团队会议(每日或每周)是向团队成员寻求帮助的好地方。如果你有几个任务,而其中一个任务让你陷入困境,先放一放,做其他任务(如果可能的话),并将你的问题留到团队会议上。如果在会议之前你已经没有任务了,可以请求经理让你忙碌,以免打扰到别人。此外,在做其他任务的过程中,解决方案可能会突然想到。

2.10.9 克服低落士气

没有什么比团队成员之间士气低落更能迅速摧毁一个项目了。以下是一些帮助你克服低落士气的建议:

  • 了解项目的商业价值。通过学习或提醒自己项目在现实世界中的实际应用,你会更加投入和感兴趣。

  • 对(你负责的部分)项目负责。当你拥有项目时,你的自豪感和声誉都在其中。无论发生什么,确保你始终能够谈论自己为项目做出的贡献。

  • 避免对自己无法控制的项目组件产生过多情感投入。例如,如果管理层做出了一些影响项目进度或设计的错误决策,在这些限制条件下尽力工作。不要只是坐在那里对管理层心生不满,而是把精力投入到解决问题上。

  • 如果你面临性格差异导致的士气问题,应该与经理和其他受影响的人员讨论这些问题。沟通是关键。允许问题继续下去只会导致未来更大的士气问题。

  • 始终关注可能会破坏士气的情况和态度。一旦项目的士气开始下降,通常很难恢复失去的部分。越早处理士气问题,解决起来就越容易。

有时,财务、资源或人员问题会降低项目参与者的士气。作为一个优秀的程序员,你的工作是挺身而出,超越这些问题,继续编写优秀的代码,并鼓励项目中的其他人也这么做。这并不总是容易的,但没有人说成为一名优秀的程序员是轻松的。

2.11 更多信息

Bellinger, Gene. “项目系统。” 系统思维,2004 年。systems-thinking.org/prjsys/prjsys.htm

Heller, Robert, 和 Tim Hindle. 基本经理:管理会议. 纽约:DK Publishing,1998 年。

Humphrey, Watts S. 软件工程的一门学科. 上塞德尔河,新泽西州:Addison-Wesley Professional,1994 年。

Kerzner, Harold. 项目管理:规划、调度与控制的系统方法. 霍博肯,新泽西州:Wiley,2003 年。

Lencioni, Patrick. 会议致命:关于解决商业中最痛苦问题的领导寓言. 旧金山:Jossey-Bass,2004 年。

Levasseur, Robert E. 突破性商务会议:共享领导力的实践. 林肯,内布拉斯加州:iUniverse.com,Inc.,2000 年。

Lewis, James P. 项目规划、调度与控制. 纽约:McGraw-Hill,2000 年。

McConnell, Steve. 软件项目生存指南. 红蒙德,华盛顿州:Microsoft Press,1997 年。

Mochal, Tom. “在团队士气低迷时激励项目团队的创意方法。” TechRepublic,2001 年 9 月 21 日。www.techrepublic.com/article/get-creative-to-motivate-project-teams-when-morale-is-low/

Wysocki, Robert K., 和 Rudd McGary. 有效项目管理. 印第安纳波利斯:Wiley,2003 年。

第四章:软件开发模型**

Image

你不会通过对每个项目都遵循一套固定的规则来编写出伟大的代码。对于某些项目,可能只需要编写几百行代码就能生产出一个优秀的程序。然而,其他项目可能涉及到数百万行代码,成百上千的项目工程师,以及几层管理或其他支持人员;在这些情况下,你所使用的软件开发过程将极大地影响项目的成功。

在本章中,我们将讨论各种开发模型以及何时使用它们。

3.1 软件开发生命周期

在软件的生命周期中,一般会经历八个阶段,统称为软件开发生命周期(SDLC)

  1. 产品构思

  2. 需求开发与分析

  3. 设计

  4. 编码(实现)

  5. 测试

  6. 部署

  7. 维护

  8. 退役

让我们逐一看看每个阶段。

产品构思

客户或经理为某个软件构思一个想法,并创建一个业务案例,证明其开发的合理性。

通常,一个非工程师会设想出对软件的需求,并寻求能够实现它的公司或个人。

需求开发与分析

一旦你有了产品构思,就必须制定产品需求。项目经理、利益相关者和客户(用户)会面,讨论并明确软件系统必须完成的任务,以满足各方需求。当然,用户希望软件能够做到任何事情。项目经理会根据可用资源(例如程序员)、估算的开发时间和成本来调整这一预期。其他利益相关者可能包括风险投资家(为项目提供资金的人)、监管机构(例如,如果你正在开发核反应堆的软件,可能需要核监管委员会的审批),以及可能为设计提供意见以使其具备销售潜力的市场人员。

通过会议、讨论、谈判等,相关方根据以下问题来制定需求:

  • 系统是为谁设计的?

  • 系统应该提供什么输入?

  • 系统应该产生什么样的输出(以及以何种格式)?

  • 会涉及到什么类型的计算?

  • 如果有视频显示,系统应该使用什么屏幕布局?

  • 预期的输入与输出响应时间应该是多少?

通过这一讨论,开发人员将编写系统需求规格说明书(SyRS),该文档规定了所有硬件、软件等的主要需求。接着,项目管理和系统分析师使用 SyRS 来生成软件需求规格说明书(SRS),^(1)这是本阶段的最终成果。一般而言,SRS 是仅供内部使用的,由软件开发团队使用,而 SyRS 则是供客户参考的外部文档。SRS 从 SyRS 中提取所有软件需求并进行扩展。第十章详细讨论了这两份文档(请参见《系统需求规格说明书》,第 193 页和《软件需求规格说明书》,第 194 页)。

设计

软件设计架构师(软件工程师)根据 SRS 中的软件需求准备软件设计描述(SDD)。SDD 提供了以下项目的某些组合,但不一定是全部:

  • 系统概述

  • 设计目标

  • 使用的数据(通过数据字典)和数据库

  • 数据流(可能使用数据流图表示)

  • 接口设计(软件如何与其他软件以及软件用户进行交互)

  • 必须遵循的任何标准

  • 资源需求(例如,内存、CPU 周期和磁盘容量)

  • 性能要求

  • 安全性要求

详见第十一章,了解 SDD 内容的更多细节。设计文档将成为下一阶段——编码的输入。

编码

编码——编写实际的软件——是软件工程师最熟悉且最有趣的步骤。软件工程师使用 SDD 来编写软件。WGC5: 伟大的编码将专门讨论这一阶段。

测试

在这个阶段,代码将针对 SRS 进行测试,以确保产品解决了需求中列出的所有问题。此阶段包含几个组成部分,包括:

单元测试 检查程序中的各个语句和模块,以验证它们是否按预期工作。虽然这通常发生在编码阶段,但从逻辑上来说,它属于测试阶段。

集成测试 验证软件中的各个子系统能够很好地协同工作。这个过程通常发生在编码阶段,通常是在接近尾声时。

系统测试 验证实现情况;即,证明软件正确地实现了 SRS(软件需求规格说明书)。

验收测试 向客户展示软件是否适合其预期用途。

WGC6: 测试、调试与质量保证将详细讨论测试阶段。第十二章描述了你将在测试过程中创建的软件测试用例和软件测试程序文档。

部署

软件产品交付给客户用于使用。

维护

一旦客户开始使用软件,他们很有可能会发现缺陷并要求新增功能。在这个过程中,软件工程师可能会修复缺陷或增加新的功能,然后将软件的新版本部署给客户。

退休

最终,在一些软件的生命周期中,开发将停止,可能是因为开发组织决定不再支持或开发它,或者它被不同版本的程序取代,或者开发它的公司倒闭,或者它运行的硬件变得过时。

3.2 软件开发模型

软件开发模型 描述了 SDLC(软件开发生命周期)中的所有阶段如何在软件项目中结合。不同的模型适用于不同的情况:有些强调某些阶段,忽略其他阶段;有些在开发过程中反复进行各个阶段;还有一些完全跳过某些阶段。

目前有八种受人尊敬的软件开发模型,且这些模型有数十种,甚至数百种变体在使用。那么,为什么开发者不选择一个流行的模型并用它来处理所有的事情呢?如第一章中所述,适合个人或小团队的做法,在大型团队中不一定能有效扩展;同样,适合大项目的技术,往往不能很好地适用于小项目。本书将重点讨论适合个人的技术,但伟大的程序员必须能够在所有设计过程中工作,才能在各类项目中成为优秀的程序员。

在本章中,我将描述八种主要的软件模型——它们的优缺点,以及如何正确地应用它们。然而,在实践中,这些模型中的任何一个都不能盲目跟随,也不能指望它们保证项目的成功。本章还将讨论伟大的程序员如何在被迫使用某个模型的局限性时,依然能够绕开这些限制,编写出优秀的代码。

3.2.1 非正式模型

非正式模型描述了软件开发过程中的最小化流程或纪律:没有正式的设计,没有正式的测试,缺乏项目管理。这个模型最初被称为 黑客^(2),那些参与其中的人被称为 黑客。然而,随着这些最初的黑客成长并获得经验、教育和技能,他们自豪地保留了“黑客”这个称号,所以这个术语不再指代一个缺乏经验或技能的程序员^(3)。我仍然会使用 黑客 来表示一种非正式的编码过程,但我会用 非正式编码者 来描述那些参与黑客行为的人。这样可以避免与“黑客”一词不同定义的混淆。

在非正式模型中,程序员直接从产品概念化进入编码阶段,不断“敲代码”,直到程序有些许功能(通常效果不好),而不是设计一个健壮、灵活、易读的程序。

黑客技术有几个优势:它很有趣,通常是独立完成的(虽然当然许多人参与像黑客马拉松这样的团体活动),程序员负责大多数设计决策并推动项目进展,因此他们通常能够比遵循正式开发过程的软件工程师更快地让某些东西工作。

非正式模型的问题在于它有意缺乏设计,这可能导致一个无效的系统,无法满足最终用户的需求,因为他们的要求没有被考虑到需求和软件规格中——如果这些需求和规格存在的话——而且通常软件没有经过测试或文档化,这使得除原始程序员外的任何人都很难使用它。

因此,非正式模型适用于那些仅供编写程序的程序员使用的小型一次性程序。对于这样的项目,编写几百行代码用于有限且谨慎的使用,比起完整的软件开发过程要便宜得多且更高效。(不幸的是,一些“一次性”程序可能会因用户的发现而获得生命,并变得流行。发生这种情况时,程序应当重新设计并重新实现,以便能够正确地维护。)

黑客技术也有助于开发小型原型,特别是那些旨在向潜在客户展示正在开发中的程序的屏幕显示。然而,这里有一个棘手的问题,那就是客户和经理可能会查看这个原型,并假设已经有大量代码就绪,这意味着他们可能会推动进一步开发被“破解”的代码,而不是从头开始开发,这会导致未来的问题。

3.2.2 瀑布模型

瀑布模型是软件开发模型的祖父,大多数模型都是它的变体。在瀑布模型中,SDLC 的每个步骤都是按顺序从头到尾执行的(参见图 3-1),每个步骤的输出形成下一个步骤的输入。

image

图 3-1:瀑布模型

你从生成系统需求说明书(SyRS)开始使用瀑布模型。一旦系统需求被确定,你就可以从 SyRS 生成软件需求说明书(SRS)。当软件需求被确定后,你可以从 SRS 生成软件设计文档(SDD)。接着你从 SDD 生成源代码并进行软件测试。然后你将部署并维护软件。SLDC 中的一切都按照这个顺序进行,没有任何偏差。

作为原始的 SDLC 模型,瀑布模型通常非常简单,容易理解并应用于软件开发项目,因为每个步骤都是独立的,且有明确的输入和输出交付物。使用该模型进行的工作也相对容易审查,并且可以验证项目是否按计划进行。

然而,瀑布模型存在一些重大问题。最重要的一点是,它假设你在进行到下一个步骤之前会完美地执行每个步骤,并且会在某个步骤的早期发现错误并进行修复。然而,现实情况是,这种情况很少发生:需求或设计阶段的缺陷通常要到测试或部署时才会被发现。到那时,倒退到系统中并修正所有问题可能非常昂贵。

另一个缺点是,瀑布模型在开发过程的很晚阶段才能提供一个可供客户评审的工作系统。我数不清有多少次,我向客户展示静态截图或代码如何工作图示,获得了他们的认可,但最终他们却拒绝了运行结果。如果我在需求阶段就制作了一个可用的代码原型,让客户能够在某些系统功能上进行实验,这种重大期望上的差距本可以避免。

最终,这个模型是非常有风险的。除非你能准确地在开始之前定义系统将要执行的任务,否则瀑布模型很可能不适合你的项目。

瀑布模型适用于小型项目,例如代码行数不到几万行,且仅涉及少数几名程序员的情况;或者适用于非常大型的项目(因为在那个层级,别的模型都无法适用);又或者当当前项目与之前使用瀑布模型开发的产品相似时(因此你可以使用现有的文档作为模板)。

3.2.3 V 模型

V 模型,如图 3-2 所示,遵循与瀑布模型相同的基本步骤,但强调在开发生命周期的早期制定测试标准。V 模型的结构使得早期的步骤——需求和设计——会产生两组输出:一组用于后续步骤,另一组用于测试阶段的平行步骤。

image

图 3-2:V 模型

在图 3-2 中,V 左侧的项目直接链接到右侧的项目:在每个设计阶段,程序员都在考虑如何测试并使用正在建模的概念。例如,在需求和架构阶段,系统架构师设计系统验收测试,以验证软件是否正确实现了所有需求。在设计阶段,系统设计师实施软件的单元测试和集成测试。

与瀑布模型的最大区别在于,工程师在早期就实现了测试用例和程序,因此当编码开始时,软件工程师可以利用现有的测试程序在开发过程中验证代码行为。这种方法被称为测试驱动开发(TDD),在这种方法中,程序员在整个开发过程中不断进行测试。持续的测试可以让你更早发现漏洞,并使修复这些漏洞变得更加便宜和迅速。

话虽如此,V 模型并不完美。像其母模型瀑布模型一样,V 模型过于简单,并且在早期阶段要求过多的完善,以避免后期的灾难。例如,需求和架构阶段的缺陷可能直到系统测试和验证时才显现出来,导致在开发过程中需要付出昂贵的返工成本。因此,V 模型并不适用于需求在产品生命周期中可能发生变化的项目。

该模型通常鼓励验证,而牺牲了验证的效果。验证确保产品满足特定的需求(例如软件需求)。开发测试用例来证明软件符合 SRS 和 SyRS 中列出的要求是很容易的。相比之下,确认证明产品满足最终用户的需求。由于更加开放,确认更难以实现。

例如,测试软件是否会因试图处理NULL指针而崩溃是困难的。因此,验证测试通常在测试程序中完全缺失。大多数测试用例是需求驱动的,像“这一段代码中没有除以零的情况”或“这个模块没有内存泄漏”等需求则很少见(这些被称为需求缺口;在没有任何需求依据的情况下设计测试用例可能会非常具有挑战性,尤其对于初学者来说)。

3.2.4 迭代模型

像瀑布模型和 V 模型这样的顺序模型假设在编码之前,规格、需求和设计都已经完美无缺,这意味着用户在软件首次部署之前不会发现设计问题。而此时,通常修复设计、修正软件并进行测试的成本已经过高(或者为时已晚)。迭代模型通过多次遍历开发模型克服了这个问题。

迭代模型的标志是用户反馈。系统设计师从用户和利益相关者那里获取产品的整体构想,并创建一个最小的需求和设计文档。编码人员实施并测试这个最小实现。然后,用户体验这个实现并提供反馈。系统设计师根据用户反馈产生新的需求和设计,程序员实施并测试这些更改。最后,用户会得到第二个版本进行评估。这个过程会重复,直到用户满意或软件达成最初的目标。

迭代模型的一个大优点是,当在开发周期开始时很难完全指定软件的行为时,它依然能很好地工作。系统架构师可以从一个大致的路线图出发,设计足够的系统部分,让最终用户可以进行操作并确定哪些新功能是必要的。这避免了花费大量精力去开发那些最终用户希望以不同方式实现或根本不需要的功能。

另一个优势是,迭代模型降低了市场时间风险。为了快速将产品推向市场,你会决定最终产品的一部分功能并优先开发这些功能,先让产品以最简化的方式工作,并发布这个最小可行产品 (MVP)。然后,在每次新迭代中添加功能,生产出新版本的增强产品。

迭代模型的优势包括:

  • 你可以非常迅速地实现最小功能。

  • 管理风险比顺序模型更容易,因为你不需要完成整个程序就能确定它不能正常工作。

  • 随着项目的进展(接近完成),管理变得比顺序模型更容易和更明显。

  • 支持需求变化。

  • 改变需求的成本更低。

  • 并行开发是可能的,通过两组(或更多)团队交替工作在不同版本上。

以下是迭代模型的一些缺点:

  • 管理项目的工作量更多。

  • 它不太适合规模较小的项目。

  • 它可能需要更多资源(特别是当进行并行开发时)。

  • 定义迭代可能需要一个“更宏大的”系统路线图(即,在开发开始前重新指定所有需求)。

  • 迭代次数可能没有限制;因此,可能无法预测项目何时完成。

3.2.5 螺旋模型

螺旋模型也是一个迭代模型,它重复四个阶段:规划、设计、评估/风险分析和构建(见图 3-3)。

image

图 3-3:螺旋模型

螺旋模型极度依赖于风险:每个迭代都会评估继续推进项目的风险。管理层通过分析风险(即失败的可能性)来选择添加和省略哪些特性,并决定采取哪些方法。

螺旋模型常被称为模型生成器元模型,因为你可以在每个螺旋中使用进一步发展的模型——相同类型的或不同类型的。缺点是,最终的模型会变得特定于该项目,难以应用于其他项目。

螺旋模型的一个关键优势是,它在开发过程中通过定期生成工作原型,早期且持续地让最终用户参与进来。最终用户可以与这些原型互动,判断开发是否走在正确的轨道上,并在需要时调整开发过程。这解决了瀑布模型和 V 模型的一大缺陷。

这种方法的一个缺点是它奖励“刚刚足够好”的设计。如果代码能写得“足够快”或“足够小”,那么进一步的优化会被推迟到后期阶段,直到确有必要时才进行。类似地,测试只会进行到足以对代码有最低程度信心的程度。额外的测试被认为是浪费时间、金钱和资源。螺旋模型往往会导致在早期工作的妥协,尤其是当管理不善时,这种情况会导致开发后期出现问题。

另一个缺点是螺旋模型增加了管理的复杂性。这个模型本身很复杂,因此项目管理需要风险分析专家。寻找具备此类专业知识的经理和工程师很困难,且通常无法找到合适的替代人选会导致灾难性后果。

螺旋模型仅适用于大型、高风险项目。为低风险项目投入的工作量(尤其是在文档方面)是难以证明合理的。即便是在更大的项目中,螺旋模型可能会无限循环,最终产品迟迟无法产生,或者预算在开发仍处于中间阶段时已被完全消耗。

另一个问题是,工程师需要花费大量时间开发原型和其他中间版本的代码,而这些代码最终并不会出现在最终的软件版本中,这意味着螺旋模型通常比使用其他方法开发软件要花费更多。

尽管如此,螺旋模型还是提供了一些巨大的优势:

  • 在项目开始之前,不需要完全明确需求;螺旋模型非常适合需求不断变化的项目。

  • 它在开发周期的早期就能产生可用的代码。

  • 它与快速原型开发(见下节“快速应用开发模型”)配合得非常好,能让客户和其他利益相关者在应用程序开发的早期就能感到安心。

  • 开发可以被分成若干部分,风险较大的部分可以提前完成,从而降低整体开发风险。

  • 由于需求可以在发现时就创建,因此更加准确。

  • 和迭代模型一样,功能可以随着时间的推移逐步展开,允许在时间或预算允许的情况下添加新功能,而不会影响初始版本的发布。

3.2.6 快速应用开发模型

与螺旋模型类似,快速应用开发(RAD)模型强调在开发过程中与用户的持续互动。RAD 模型由 IBM 的研究员 James Martin 在 1990 年代提出,最初的 RAD 模型将软件开发分为四个阶段(见图 3-4)。

image

图 3-4:RAD 模型

需求规划 项目的相关方聚集在一起讨论业务需求、范围、限制和系统需求。

用户 设计 终端用户与开发团队合作,制作系统模型和原型(详细说明输入、输出和计算),通常使用 计算机辅助软件工程(CASE) 工具。

构建 开发团队利用工具从需求和用户设计中自动生成代码来构建软件。在此阶段,用户仍然参与其中,随着用户界面的逐步完善,他们会提出修改建议。

切换 软件被部署。

RAD 比螺旋模型更轻量,风险缓解技术较少,文档需求也相对较轻,这使得它更适用于中小型项目。与其他模型不同,传统的 RAD 强烈依赖于高级语言(VHLL)、用户界面建模工具、复杂的现有代码库和框架,以及 CASE 工具来根据需求和用户界面模型自动生成代码。一般来说,RAD 只有在针对特定项目问题可用 CASE 工具时才实际可行。今天,许多通用语言系统都支持高度自动化的代码生成,包括微软的 Visual Basic 和 Visual Studio 包、苹果的 Xcode/Interface Builder 包、Free Pascal/Lazarus 以及 Embarcadero 的 Delphi(Object Pascal)包。

RAD 模型的优点与螺旋模型类似:

  • 客户在整个开发过程中都参与其中,从而降低了风险。

  • RAD 缩短了开发时间,因为写文档的时间减少了,避免了文档在规格变更时需要重新编写的情况。

  • RAD 模型鼓励快速交付可运行的代码,测试(和缺陷缓解)也更加高效。开发人员将更多时间花在运行代码上,测试问题。

和任何开发模型一样,RAD 也有一些缺点:

  • RAD 需要具备大宗师级别的开发经验的软件工程师,他们能够缩短其他开发模型中冗长的开发过程。然而,这类资源在许多组织中比较稀缺。

  • RAD 需要与终端用户持续互动,这在许多项目中可能会受到限制。

  • RAD 可能很难进行调度和控制。那些依赖 Microsoft Project 的经理会发现很难应对 RAD 模型中的不确定性。

  • 如果管理不当,RAD 很容易变成拼凑式编程。软件工程师可能会放弃正式的设计方法,而直接在代码上“乱改”以实现更改。当最终用户开始提出“只是为了看看结果会是什么样”这样的建议时,这种情况尤为棘手^(4)。

  • RAD 不适用于大型系统开发。

3.2.7 增量模型

增量模型与迭代模型非常相似,主要的区别在于规划和设计。在迭代模型中,首先创建系统设计,软件工程师在每次迭代中实现不同的部分;初始设计仅定义第一段工作代码。一旦程序运行,新功能会按阶段逐步设计并添加。

增量模型强调“保持代码可运行”的理念。当基础产品可操作时,开发团队在每次迭代中添加最少量的新功能,并对软件进行测试并保持其可用性。通过限制新功能,团队可以更容易地定位和解决开发中的问题。

增量模型的优点是始终保持一个可工作的产品。该模型对程序员来说也很自然,特别是在小型项目中。缺点是,它一开始并没有考虑到产品的完整设计。通常,新的功能只是简单地加在现有设计上。当最终用户请求在原始设计中未考虑到的功能时,这可能会导致未来的问题。增量模型适用于小型项目,但对于大型项目而言,扩展性较差,此时迭代模型可能是更好的选择。

3.3 软件开发方法论

软件开发模型描述了做什么工作,但在如何做的方面留有相当大的余地。本节将介绍一些可以应用于许多前面提到的模型的开发方法论和过程。

Belitsoft 公司博客^(5)描述了软件方法论如下:

一套原则体系,以及一组定义软件开发风格的理念、概念、方法、技术和工具。

因此,我们可以将软件方法论的概念归结为一个词:风格。在开发软件时,可以使用各种风格。

3.3.1 传统(预测型)方法论

传统的方法论是预测性的,这意味着管理层预测哪些活动将会发生,何时发生,以及由谁来执行。这些方法论与线性/顺序的开发模型(如瀑布模型或 V 模型)密切配合。你可以在其他模型中使用预测,但那些模型的设计本意就是为了避免预测方法论常见的问题。

预测性方法论在无法预测未来需求、关键人员或经济条件变化时会失败(例如,公司是否在项目的某个里程碑时获得了预期的额外融资?)。

3.3.2 自适应方法论

螺旋模型、RAD 模型、增量模型和迭代模型的出现,正是因为通常很难正确预测大型软件系统的需求。自适应方法论处理这些工作流程中的不可预测变化,并强调短期规划。毕竟,如果你只提前 30 天规划一个大型项目,最糟糕的情况就是你需要重新规划接下来的 30 天;这远没有在大型瀑布/预测性项目中出现变化时所面临的灾难性后果,那时的变化会迫使你重新同步整个项目。

3.3.3 敏捷

敏捷是一种增量方法论,专注于客户协作、快速响应变化的短期开发迭代、可工作的软件以及支持个人的贡献和互动。敏捷方法论作为一个总的框架,涵盖了几种不同的“轻量级”(即非预测性)方法论,包括极限编程、Scrum、动态系统开发模型(DSDM)、自适应软件开发(ASD)、Crystal、特性驱动开发(FDD)、务实编程等。这些方法论中的大多数被视为“敏捷”,尽管它们通常涵盖软件开发过程的不同方面。敏捷方法论已在现实项目中得到了充分验证,成为目前最流行的方法论之一,因此我们将在这里详细介绍它。

注意事项

有关敏捷背后原则的详细列表,请参阅敏捷宣言:agilemanifesto.org/

3.3.3.1 敏捷具有增量性质

敏捷开发本质上是增量的、迭代的和进化的,因此与增量模型或迭代模型最为契合(也可以使用螺旋模型或快速应用开发(RAD)模型)。一个项目被分解为团队可以在一到四周内完成的任务,这通常被称为sprint。在每个 sprint 期间,开发团队进行规划、创建需求、设计、编码、单元测试以及接受测试新功能的软件。

在 sprint 结束时,交付成果是一块能展示新功能、且尽可能少缺陷的可工作软件。

3.3.3.2 敏捷要求面对面沟通

在整个冲刺过程中,必须有一位客户代表随时待命,回答出现的任何问题。如果没有这位代表,开发过程很容易偏离正确方向或在团队等待回复时陷入困境。

敏捷开发中的高效沟通需要面对面的交流。^(6) 当开发人员直接向客户展示产品时,客户常常会提出一些在邮件中或自己试用功能时无法想到的问题。有时,演示中的随意评论会激发出一阵不同的思维,这种思维如果没有面对面交流是难以产生的。

3.3.3.3 敏捷注重质量

敏捷强调各种提升质量的技术,如自动化单元测试、TDD、设计模式、结对编程、代码重构以及其他广为人知的最佳软件实践。其核心思想是在初始设计和编码过程中尽可能减少缺陷。

自动化单元测试创建了一个测试框架,开发人员可以自动运行该框架来验证软件是否正常运行。它对于回归测试也非常重要,回归测试用来确保在添加新功能后,代码仍然能够正常工作。手动运行回归测试太过繁琐,因此通常不会发生。

在 TDD 中,开发人员会先编写自动化测试用例,再编写代码,这意味着测试最初会失败。开发人员运行测试,选择一个失败的测试,编写软件来修复这个失败,然后重新运行测试。只要某个测试通过,开发人员就会继续处理下一个失败的测试。成功地消除所有失败的测试就验证了软件满足需求。

结对编程是敏捷中较具争议的一项实践,它要求两名程序员共同编写每一段代码。一名程序员输入代码,另一名程序员观察,纠正屏幕上的错误,提供设计建议,进行质量控制,并帮助第一个程序员保持专注于项目。

3.3.3.4 敏捷冲刺(迭代)周期短

敏捷方法最有效的方式是保持迭代周期短—从一周到最多几个月。这也呼应了那句老话:“如果没有最后一分钟,什么事都做不成。”通过保持迭代短小,软件工程师总是在“最后一分钟”工作,减少了疲劳和拖延,提升了项目的专注度。

短周期的冲刺与短周期的反馈紧密相连。一个常见的敏捷特性是简短的每日站会,通常不超过 15 分钟,^(7),在会上,程序员简洁地描述他们正在做什么,遇到了什么问题,已经完成了什么。这使得项目管理可以重新安排资源并在进度滞后的情况下提供帮助。这些会议能及早发现问题,而不是等到问题积累数周才引起项目管理的注意。

3.3.3.5 敏捷不强调笨重文档

瀑布模型的最大问题之一是,它会产生大量从未再被阅读的文档。过于全面的、笨重的文档有几个问题:

  • 文档必须得到维护。每当软件进行更改时,文档也必须更新。一份文档中的更改必须反映在许多其他文档中,从而增加了工作量。

  • 许多文档在编写代码之前很难写。更多时候,这些文档是在代码写完后更新的,并且再也没有被阅读(浪费了时间和金钱)。

  • 迭代开发过程会迅速破坏代码与文档之间的一致性。因此,在每次迭代中正确地维护文档与敏捷方法论不太匹配。

敏捷强调恰到好处(JBGE)的文档——也就是说,文档应该足够多,以便下一个程序员能接着你的工作继续,但不能过多(事实上,敏捷强调 JBGE 适用于大多数概念,包括设计/建模)。

许多书籍都写了关于敏捷开发的内容(参见“更多信息”在第 69 页)。这本书并不是其中之一,但我们将探讨敏捷方法下的几种不同方法论。这些方法论并不是互相排斥的;可以将两种或更多方法结合起来使用在同一个项目中。

3.3.4 极限编程

极限编程(XP)可能是最广泛使用的敏捷方法论。它旨在简化开发实践和过程,交付能够提供所需功能集的工作软件,而不附加不必要的内容。

XP 的指导原则有五个:

沟通 客户与团队之间、团队成员之间、团队与管理层之间的良好沟通对于成功至关重要。

简单性 XP 力求今天产生最简单的系统,即使明天扩展它的成本更高,而不是产生一个复杂的产品,实现那些可能永远不会用到的功能。

反馈 XP 依赖于持续的反馈:单元和功能测试在程序员更改代码时为其提供反馈;客户在添加新功能时提供即时反馈;项目管理跟踪开发进度,提供关于估算的反馈。

尊重 XP 要求团队成员彼此尊重。程序员永远不会提交破坏代码库的更改,或者做出任何会拖延其他团队成员工作进度的行为。

勇气 XP 的规则和实践与传统的软件开发实践不一致。XP 要求投入资源(例如“随时可用”的客户代表或配对程序员),这些资源在传统方法中可能成本高昂或难以证明其价值。一些 XP 的政策,如“早重构,常重构”,与常见做法(如“如果没有坏,就不要修”)相悖。如果没有勇气完全实施其极端政策,XP 会变得不那么严谨,甚至可能退化为临时拼凑的编程方式。

3.3.4.1 XP 团队

XP 过程中的关键概念是 XP 全体团队理念:团队的所有成员共同合作生产最终产品。团队成员并不是专门从事某一领域的专家,而是经常承担不同的责任或角色,不同的团队成员可能在不同时间执行相同的角色。XP 团队由不同的成员填补以下角色。

客户代表

客户代表负责确保项目按正确的轨道进行,提供验证,编写用户故事(需求、功能和用例)和功能测试,并决定新功能的优先级(发布规划)。客户代表必须在团队需要时随时可用。

没有可用的客户代表是成功的 XP 项目中最大的障碍之一。如果没有来自客户的持续反馈和指导,XP 就会退化为临时拼凑的编程方式。XP 不依赖于需求文档,而是将客户代表视为该文档的“活版本”。

程序员

程序员在 XP 团队中有多项责任:与客户代表合作编写用户故事,估算资源分配给这些故事的方式,估算实现这些故事的时间表和成本,编写单元测试,并编写代码来实现这些故事。

测试人员

测试人员(实施或修改给定单元并运行单元测试的程序员)执行功能测试。通常,至少有一名测试人员是客户代表。

教练

教练是团队领导,通常是首席程序员,负责确保项目的成功。教练确保团队拥有合适的工作环境;促进良好的沟通;通过充当上级管理层的联络人,保护团队免受组织其他部分的干扰;帮助团队成员保持自律;并确保团队维持 XP 过程的执行。当程序员遇到困难时,教练提供资源帮助他们克服问题。

经理/追踪者

XP 项目经理负责安排会议并记录会议结果。跟踪员通常(但不总是)与经理是同一个人,负责跟踪项目的进度,并确定当前迭代的计划是否能够按时完成。为此,跟踪员每周会与每位程序员沟通几次。

不同的 XP 配置通常包括额外的团队角色,如分析师、设计师、悲观者等。由于 XP 团队的规模较小(通常约 15 名成员),并且(配对)程序员占据团队的主要成员,因此大多数角色是共享的。有关更多信息,请参见第 69 页的“更多信息”部分。

3.3.4.2 XP 软件开发活动

XP 使用四个基本的软件开发活动:编码、测试、倾听和设计。

编码

XP 认为代码是开发过程中的唯一重要输出。与瀑布等串行模型中的“先思考,后编码”哲学相反,XP 程序员从软件开发周期开始时就开始编写代码。毕竟,“到头来,必须有一个可工作的程序。”^(8)

XP 程序员不会立即开始编码,而是会得到一个小且简单的功能列表来实现。他们为特定功能制定基本设计,然后编写该功能并确保其正常工作,再逐步扩展每个增量,确保每个增量都能正确工作,从而确保主代码始终运行。程序员在将更改集成到更大的系统之前,通常只会对项目进行小幅度的修改。XP 最大限度地减少了所有非代码输出,如文档,因为它几乎没有什么好处。

测试

XP 强调通过自动化单元和功能测试来实现 TDD。这使得 XP 工程师能够开发出正确的产品(通过自动化单元测试进行验证)和开发出合适的产品(通过功能测试进行验证)。WGC6: 测试、调试和质量保证将更专注于测试,因此我们在这里不会深入讨论;只需要知道 TDD 对 XP 过程非常重要,因为它确保系统始终正常运行。

XP 中的测试始终是自动化的。如果添加一个功能因某些原因破坏了无关的功能,那么及时发现这一点至关重要。通过在添加新功能时运行完整的单元(和功能)测试,可以确保新代码不会引起回归。

倾听

XP 开发人员几乎与客户进行持续沟通,确保他们开发的是正确的产品(验证)。

XP 是一个以变化为驱动的过程,这意味着它期望在整个过程中根据客户的反馈,要求、资源、技术和性能发生变化。

设计

设计在整个 XP 过程中不断发生——在发布规划、迭代规划、重构等阶段。这个关注点防止 XP 陷入乱搞代码的局面。

3.3.4.3 XP 过程

XP 的每个周期都会产生一个软件发布。频繁的发布确保了来自客户的持续反馈。每个周期由几个固定时间段组成,这些时间段被称为迭代(每个迭代的时间不超过几周)。如图 3-5 所示,周期对于规划是必需的;该图中的中间框代表一个或多个迭代。

image

图 3-5:XP 周期

在规划游戏中,XP 团队决定要实现哪些功能,估算其成本,并规划发布。在探索阶段,客户定义功能集,开发人员估算这些功能的成本和时间需求。接下来的部分(在“用户故事”下)描述了客户用来指定功能的机制。

在发布规划阶段,客户与开发人员就本次迭代中要实现的功能进行协商。开发人员承诺执行发布计划,并分配各种任务。在发布规划结束时,过程进入引导阶段,在该阶段,客户确保项目按计划进行。

在总体计划确定后,当前发布的过程进入一个包含三个步骤的内循环:迭代规划、实施和功能测试。迭代规划是对单个功能的缩小版规划游戏。

实施步骤是功能的编码和单元测试。开发人员编写一组单元测试,实施足够的代码以使单元测试通过,必要时对代码进行重构,并将更改集成到共享代码库中。

在迭代的最后一步,客户进行功能测试。然后,如果当前版本的所有迭代都完成,则进入下一个迭代,或者发布新版本。

3.3.4.4 XP 软件开发规则

XP 通过 12 条简单规则来实现四项软件开发活动——编码、测试、倾听和设计:^(9)

  • 用户故事(规划游戏)

  • 小规模发布(构建块)

  • 隐喻(标准化命名方案)

  • 集体所有权

  • 编码标准

  • 简单设计

  • 重构

  • 测试

  • 配对编程

  • 现场客户

  • 持续集成

  • 可持续节奏

每个规则接下来都会描述,并列出其优缺点。

用户故事

用户故事描述了一组简化的用例,由客户编写,定义了系统的需求。项目团队使用这一套用户故事,它应该只提供足够的细节来估算实现该功能所需的时间,进而估算成本并规划系统的开发。

在项目开始时,客户会生成 50 到 100 个用户故事,用于发布计划会议中。然后,客户和团队会协商决定在下一次发布中由团队实现哪些功能。客户可能会在开发人员的帮助下,也会根据用户故事创建功能测试。

小范围发布

一旦一段软件实现了基本功能,团队会一次添加一个功能。在新功能编写、测试、调试并合并到主版本之前,不会添加其他功能。每添加一个新功能,团队都会创建一个新的系统构建版本。

隐喻

XP 项目围绕着一个所有利益相关者都能理解的系统操作故事展开。隐喻是软件中使用的命名约定,确保每个人都能清晰地理解操作;它们将复杂的业务流程名称替换为简单的名称。例如,“火车乘务员”可能用来描述数据采集系统的操作方式。

集体所有权

在 XP 中,整个团队共同拥有并维护所有源代码。在任何时候,任何团队成员都可以检出代码并进行修改。在评审过程中,不会因为编码错误而特别指责任何人。集体代码所有权避免了延误,并且意味着某个人的缺席不会妨碍进展。

编码标准

所有 XP 成员必须遵守关于样式和格式的共同编码标准。团队可以制定这些标准,或者它们可以来自外部来源,但每个人都必须遵守。编码标准使得系统更易于阅读和理解,尤其是对于新加入的成员,他们需要迅速适应项目,并帮助团队避免以后浪费时间重构代码以符合标准。

简单设计

始终选择满足所有要求的最简单设计。设计中绝不会预见到尚未添加的功能——例如,添加“钩子”或应用程序编程接口(API),以便将来代码能够与当前代码进行接口。简单设计意味着足够完成当前任务。最简单的代码会通过当前迭代的所有测试。这与传统的软件工程相悖,后者通常将软件设计得尽可能通用,以便处理任何未来的增强功能。

重构

重构代码是指在不改变外部行为的情况下,重构或重写代码,以使代码更简单、更易读,或通过其他改善指标来提高代码质量。

WGC5: 卓越编码将更详细地讲解重构。有关重构的更多参考资料,请参见第 69 页中的“更多信息”。

测试

XP 采用 TDD 方法论,如第 57 页的“XP 软件开发活动”中所讨论的。

结对编程

在配对编程中,一名程序员(司机)输入代码,第二名程序员(导航员)在每行代码编写时进行审查。两名工程师在整个过程中互换角色,并且配对通常会不断形成和解散。

说服管理层相信两名程序员在同一份代码上合作工作比各自独立工作更具生产力,往往是很困难的。XP 传教士认为,由于导航员在不断审查司机的代码,因此不需要单独的审查会话,除此之外还有其他好处:^(10)

经济效益 配对编程所花费的时间比个人多大约 15%,但代码的缺陷比个人少约 15%。^(11)

设计质量 两名程序员能产生更好的设计,因为他们为项目带来了更多经验。他们从不同的角度思考问题,并根据他们的司机/导航员角色以不同的方式设计解决方案。更好的设计意味着项目在整个生命周期中需要更少的回溯和重设计。

满意度 大多数程序员喜欢配对工作,而不是单独工作。他们对自己的工作更有信心,因此能够产生更好的代码。

学习 配对编程使配对成员可以相互学习,提高各自的技能。这是单独编程无法实现的。

团队建设与沟通 团队成员共享问题和解决方案,这有助于将知识产权(IP)传播开来,并使其他人在给定的代码部分上更容易进行工作。

总的来说,关于配对编程有效性的研究结果是喜忧参半的。大多数来自行业的已发布论文讨论了配对编程的成功案例,但描述其在行业(相对于学术界)环境中失败的论文通常不会被发布。Kim Man Lui 和 Andreas Hofer 的研究考虑了配对编程中的三种配对类型:专家–专家、初学者–初学者和专家–初学者。

专家–专家配对 可以产生有效的结果,但两个专家程序员可能会使用“经验证的方法”,而不引入任何新的见解,这意味着这种配对相较于两个单独的专家程序员的有效性是值得质疑的。

初学者–初学者配对 通常比让合作伙伴独立做项目更有效。初学者的背景和经验差异很大,他们的知识更有可能是互补的,而不是重叠的(这与专家配对的情况不同)。两位初学者一起工作时,更可能在两个项目上按顺序工作,而不是独立地平行处理各自的项目。

专家–新手配对通常被称为指导。许多 XP 的拥护者认为这不是配对编程,但指导是一种高效的方式,能让初级程序员迅速熟悉代码库。在指导中,最好让新手担任驾驶员角色,这样他们就可以与代码互动并从中学习。

简单设计指南

与简单设计相关的常见短语包括:

不要重复自己(DRY) 重复的代码是复杂的代码。

一次且仅一次(OAOO) 所有独特的功能应该以某种方法/过程的形式存在于代码中,并且只出现一次(这一点是 DRY 的体现)。

你不需要它(YAGNI) 避免推测性编码。当你向代码库中添加功能时,确保它是由用户故事(需求)指定的。不要提前为未来的需求添加代码。

限制 API 和(发布的)接口 如果你的代码通过发布 API 与其他系统进行交互,将接口数量限制到最小,将使得未来修改代码时更容易(而不会破坏外部代码)。

简单设计是极其困难的。往往你只能通过编写复杂的代码,并反复重构它,直到你对结果满意,才能实现它。一些著名计算机科学家的几句话将帮助你深刻理解这一点:

构建软件设计有两种方式:一种是使其简单到明显没有缺陷,另一种是使其复杂到没有明显缺陷。

—C. A. R. 霍尔

最便宜、最快和最可靠的组件是那些不存在的组件。

—戈登·贝尔

删除的代码就是已调试的代码。

—杰夫·西克尔

调试的难度是编写代码的两倍。因此,如果你编写的代码足够巧妙,那么按定义来说,你就不够聪明去调试它。

—布赖恩·肯尼汉和 P. J. 普劳杰

任何试图做到足够通用和可配置的程序,试图处理任何任务,要么无法达到这一目标,要么会被严重破坏。

—克里斯·温哈姆

添加一个功能的成本不仅仅是编码所需的时间。成本还包括为未来扩展增加的障碍。诀窍在于挑选那些不会相互冲突的功能。

—约翰·卡马克

简单的设计难以构建,容易使用,但很难收费。复杂的设计容易构建,难以使用,却容易收费。

—克里斯·萨卡

尽管配对编程的支持证据主要是轶事,并且本质上未经过验证,但 XP 依赖配对编程来替代正式的代码审查、结构化的走查和—在一定程度上—设计文档,因此它不能被放弃。正如 XP 方法论中常见的,某些繁重的过程,如代码审查,往往会被融入到配对编程等其他活动中。试图消除某一条规则或子流程,很可能会在整体方法中留下空隙。

并非所有 XP 活动都是以配对形式进行的。许多非编程活动是单独进行的——例如,阅读(和编写)文档、处理电子邮件和在网上进行研究——有些活动总是单独进行的,比如编写代码突发(用于测试某个理论或想法的临时代码)。归根结底,结对编程对 XP 成功至关重要。如果一个团队无法有效地进行结对编程,它应该采用不同的开发方法。

现场客户

如前所述,在 XP 中,客户是开发团队的一部分,必须始终可用。

现场客户规则可能是最难遵循的规则。大多数客户不愿意或无法提供这个资源。然而,如果没有客户代表的持续参与,软件可能会偏离轨道,遇到延迟,或从之前的工作版本中退步。这些问题都是可以解决的,但解决方案会破坏使用 XP 的好处。

持续集成

在传统的软件开发系统(如瀑布模型)中,系统的各个组件是由不同的开发人员编写的,直到项目中的某个重要里程碑才会进行整合测试,而集成后的软件可能会出现灾难性的失败。问题在于单元测试与必须与单元集成的代码行为不同,通常是由于通信问题或需求理解错误。

一定会发生误解和沟通不畅,但 XP 通过持续集成使集成问题更容易解决。每当一个新功能实现时,它就会与主构建合并并进行测试。有些测试可能会失败,因为某个功能尚未实现,但整个程序都会运行,测试与应用中其他单元的链接。软件构建会频繁创建(每天多次)。因此,你可以在问题成本较低时,尽早发现集成问题。

可持续的步伐

许多研究表明,创造性的人在没有过度工作的情况下会产出最好的成果。XP 规定软件工程师的工作时间为每周 40 小时。有时可能会出现需要少量加班的紧急情况。但如果管理层让编程团队始终处于危机模式,工作的质量就会下降,加班也会变得适得其反。

3.3.4.5 其他常见实践

除了之前的 12 条规则,XP 还推广了其他一些常见的实践:

开放的工作空间和联合办公

XP 方法论建议整个团队使用开放的工作区域,团队成员在相邻的工作站进行结对工作。让大家聚在一起促进了持续的沟通,并保持了团队的专注。问题可以迅速提出并得到解答,其他程序员可以根据需要在讨论中加入评论。^(12)

但开放式工作空间也有其挑战。一些人比其他人更容易分心。嘈杂的噪音和对话会非常烦人,并打破集中注意力的状态。

开放式工作空间在 XP 中是“最佳实践”,而不是绝对规则。如果这种设置对某个团队不适用,他们可以使用办公室或小隔间,在没有干扰的情况下工作。

回顾/总结会议

当一个项目完成时,团队会聚在一起讨论成功和失败的经验,将这些信息传播以帮助改进下一个项目。

自主团队

自主团队在没有通常管理层(项目负责人、高级和初级工程师等)的情况下工作。团队通过共识来决定优先事项。XP 团队并非完全没有管理,但这里的观点是,给定一组任务和适当的截止日期,团队可以自行管理任务分配和项目进度。

3.3.4.6 XP 方法的问题

XP 不是万能的。它有几个问题,包括:

  • 没有创建或保存详细的规格说明。这使得在项目后期很难加入新程序员,或者让另一个编程团队来维护项目。

  • 即使不起作用,也要求进行结对编程。在某些情况下,这可能是过度的。两个程序员共同开发一个相对简单的代码片段可能会使开发成本翻倍。

  • 实际上,XP 通常要求所有团队成员都具备 GMP(通用多功能专业人员)的能力,以应对每个成员必须支持的广泛角色。除非在最小规模的项目中,这在现实中很少能实现。

  • 持续重构可能带来与它解决的问题(新 bug)一样多的麻烦。当程序员重构不需要重构的代码时,还会浪费时间。

  • 不进行前期大设计(即非瀑布式开发)通常会导致过度的重新设计。

  • 需要有客户代表。通常,客户会因考虑成本将一个初级人员安排到这个职位上,导致失败点。如果客户代表在项目完成前离开,所有未记录下来的需求都会丢失。

  • XP 不适用于大团队。一个高效的 XP 团队的上限大约是十几个工程师。

  • XP 尤其容易受到“功能蔓延”的影响。由于缺乏文档化的需求/功能,客户可能会将新功能注入系统。

  • 单元测试,即使是 XP 程序员创建的,也常常无法指出缺失的功能。单元测试测试的是“已存在的代码”,而不是“应该存在的代码”。

  • XP 通常被认为是一种“全有或全无”的方法论:如果你不遵循“XP 宗教”的每一条原则,过程就会失败。大多数 XP 规则都有缺陷,而这些缺陷会被其他规则的优点所弥补。如果你未能应用一条规则,另一条规则很可能会被打破(因为它的缺点不再得到弥补,这条破坏的规则又会破坏另一条规则,如此反复)。

这个关于 XP 的简短介绍无法完全阐述该主题。有关 XP 的更多信息,请参阅 “更多信息” 以及 第 69 页。

3.3.5 Scrum

Scrum 方法论本身并不是一种软件开发方法论,而是一种管理软件开发过程的敏捷机制。通常,Scrum 用于管理其他模型,例如 XP。

除了工程师外,Scrum 团队还有两位特殊成员:产品负责人和 Scrum Master。产品负责人 负责引导团队构建正确的产品,例如,维护需求和功能。Scrum Master 是一位教练,指导团队成员通过基于 Scrum 的开发流程,管理团队进展,维护项目清单,并确保团队成员不受阻碍。

Scrum 是一个迭代开发过程,像所有其他敏捷方法论一样,每个迭代都是一个为期一到四周的 sprint。sprint 从计划会议开始,团队在会议中确定要完成的工作。将组成一个被称为 待办事项清单 的事项列表,团队估算每个待办事项需要的时间。待办事项清单一旦创建,sprint 就可以开始了。

每天,团队都会召开简短的站会,在会上,成员简要提到昨天的进展和今天的计划。Scrum Master 记录下任何进度问题,并在会议后处理这些问题。站会期间不会进行项目的详细讨论。

团队成员从待办事项清单中选择任务并开始工作。当任务从待办事项清单中移除时,Scrum Master 维护一个 Scrum 燃尽图,显示当前 sprint 的进展。当所有事项都已按产品负责人要求完成,或者团队认为某些事项无法按时完成或根本无法完成时,团队会召开 结束会议

在结尾会议上,团队展示已实现的功能,并解释未完成事项的失败。如果可能,Scrum Master 会收集未完成的事项以供下一个迭代使用。

结尾会议的一部分是 sprint 回顾会议,在会议上,团队成员讨论他们的进展,提出流程改进建议,并确定哪些做得好,哪些做得不好。

请注意,Scrum 并不规定工程师如何执行他们的工作或如何记录任务,也没有提供开发过程中要遵循的一套规则或最佳实践。Scrum 将这些决策留给开发团队。例如,许多团队在 Scrum 框架下采用 XP 方法论。任何与迭代开发兼容的方法论都可以很好地工作。

和极限编程(XP)类似,Scrum 适用于小团队(成员少于十人),并且难以扩展到大型团队。为支持大型团队,Scrum 进行了一些扩展。特别是,"scrum-of-scrums"过程允许多个团队将 Scrum 方法应用于大型项目。大型项目被拆分成多个团队,然后每个团队派出一名代表参加每日的 scrum-of-scrums 会议,讨论他们的进展。这虽然无法解决大型团队所有的沟通问题,但可以使该方法适用于稍大一些的项目。

3.3.6 特性驱动开发

特性驱动开发,作为敏捷方法中的一种有趣方法,专门设计用来扩展到大型项目。

大多数敏捷方法论的共同点是,它们要求专家级程序员才能成功。另一方面,FDD 允许有大型团队,在这种情况下,物流上不可能确保每个项目活动都有最合适的人来完成,因此在涉及十多个软件工程师的项目中,FDD 值得认真考虑。

特性驱动开发(FDD)采用迭代模型。项目开始时进行三个过程(通常称为零迭代),然后剩余的两个过程在项目持续过程中迭代进行。这些过程如下:

  1. 开发一个整体模型。

  2. 创建特性列表。

  3. 按特性规划。

  4. 按特性设计。

  5. 按特性构建。

3.3.6.1 开发整体模型

开发整体模型是所有利益相关者——客户、架构师和开发人员——之间的协作工作,所有团队成员共同努力理解系统。与串行方法中的规格和设计文档不同,整体模型侧重于广度而非深度,旨在尽可能多地填充通用特性,以定义整个项目,然后在模型设计的未来迭代中填充深度,目的是引导当前项目,而不是为未来进行文档化。

与其他敏捷方法相比,这种方法的优点在于,大多数特性从项目开始时就已经规划好。因此,设计无法朝着某个方向偏离,以至于某些特性在后期很难或不可能添加,同时也无法随意增加新的特性。

3.3.6.2 创建特性列表

在 FDD 的第二个步骤中,团队记录了在模型开发步骤中制定的特性列表,然后由首席程序员正式化,以便在设计和开发过程中使用。此过程的输出是正式的特性文档。虽然不像其他模型中的 SRS 文档那么繁重,但特性描述是正式且明确的。

3.3.6.3 按特性规划

按特性计划过程包括为软件开发制定初步的时间表,决定哪些特性将首先实现,哪些特性将在后续迭代中实现。

按特性计划还将一组特性分配给不同的首席程序员,他们与各自的团队一起负责实施这些特性。首席程序员及其团队成员对这些特性及相关代码负有所有权。这与标准的敏捷实践有所不同,因为在标准敏捷中,整个团队都拥有代码。这也是 FDD 在大型项目中比标准敏捷流程更有效的原因之一:集体代码所有权对于大型项目的扩展性较差。

通常,每个特性是一个小任务,由三到五人组成的团队可以在两到三周内开发完成(更常见的是几天内完成)。每个特性类是独立的,因此没有任何特性依赖于其他团队所拥有的特性类的开发。

3.3.6.4 按特性设计

一旦选定了给定迭代的特性,由每个特性集的首席程序员组成的团队会设计该特性。特性团队不是静态的;它们会在每次设计按特性和按特性构建的迭代过程中组建和解散。

特性团队分析需求并设计当前迭代的特性。团队决定该特性的实现方式及其与系统其他部分的互动。如果特性影响范围较广,首席程序员可能会邀请其他特性类所有者以避免与其他特性集发生冲突。

在设计阶段,特性团队决定使用哪些算法和流程,并为特性开发和记录测试。如果有必要,首席程序员(以及原始利益相关者)会更新整体模型以反映设计。

3.3.6.5 按特性构建

按特性构建步骤包括编写代码和测试特性。开发人员会对自己的代码进行单元测试,特性团队则提供对特性的正式系统测试。FDD 并不要求 TDD,但确实要求对所有添加到系统中的特性进行测试和审查。

FDD 要求代码审查(这是最佳实践,但大多数敏捷流程并不要求)。正如 Steve McConnell 在《Code Complete》(Microsoft Press,2004)中指出的,执行良好的代码检查能发现很多仅靠测试无法发现的缺陷。

3.4 优秀程序员的模型和方法论

一个优秀的程序员应该能够适应团队使用的任何软件开发模型或方法论。也就是说,一些模型比其他模型更合适。如果你可以选择模型,本章将指导你选择一个合适的模型。

没有任何一种方法论可以在项目规模上进行上下扩展,因此你需要根据项目大小选择合适的模型和方法论。对于小型项目,黑客式开发或无文档版本的瀑布模型可能是一个不错的选择。对于中型项目,迭代(敏捷)模型和方法论是最佳选择。对于大型项目,顺序模型或 FDD 通常是最成功的(尽管通常非常昂贵)。

通常,你无法选择你参与项目的开发模型,除非它们是你的个人项目。关键是要熟悉各种模型,这样你才能适应任何被要求使用的模型。以下部分提供了一些资源,帮助你进一步了解本章描述的不同软件开发模型和方法论。像往常一样,互联网搜索将提供大量关于软件开发模型和方法论的信息。

3.5 更多信息

Astels, David R. 测试驱动开发:实践指南. 上萨德尔河,新泽西州:Pearson Education, 2003.

Beck, Kent. 通过示例进行测试驱动开发. 波士顿:Addison-Wesley Professional, 2002.

Beck, Kent,和 Cynthia Andres. 极限编程解释:拥抱变化. 第二版. 波士顿:Addison-Wesley, 2004.

Boehm, Barry. 螺旋开发:经验、原则与改进. (特别报告 CMU/SEI-2000-SR-008.)由 Wilfred J. Hansen 编辑. 匹兹堡:卡内基梅隆软件工程研究所, 2000.

Fowler, Martin. 重构:改善现有代码的设计. 雷丁,马萨诸塞州:Addison-Wesley, 1999.

Kerievsky, Joshua. 重构到模式. 波士顿:Addison-Wesley, 2004.

Martin, James. 快速应用开发. 印第安纳波利斯:Macmillan, 1991.

Martin, Robert C. 敏捷软件开发,原则,模式与实践. 上萨德尔河,新泽西州:Pearson Education, 2003.

McConnell, Steve. 代码大全. 第二版. 雷德蒙德,华盛顿:Microsoft Press, 2004.

———. 快速开发:驯服狂野的软件进度表. 雷德蒙德,华盛顿:Microsoft Press, 1996.

Mohammed, Nabil, Ali Munassar, 和 A. Govardhan. “五种软件工程模型的比较。” IJCSI 国际计算机科学问题期刊 7,第 5 期(2010 年)。

Pressman, Robert S. 软件工程:实践者的方法. 纽约:McGraw-Hill, 2010.

Schwaber, Ken. Scrum 敏捷项目管理(开发者最佳实践). 雷德蒙德,华盛顿:Microsoft Press, 2004.

Shore, James,和 Shane Warden. 敏捷开发的艺术. 塞巴斯托波尔,加利福尼亚州:O'Reilly, 2007.

Stephens, Matt, 和 Doug Rosenberg. 极限编程重构:反对 XP 的案例. 纽约:Apress, 2003.

Wake, William C. 重构工作簿. 波士顿:Addison-Wesley Professional, 2004.

Williams, Laurie,和 Robert Kessler. 配对编程启示. 雷丁,马萨诸塞州:Addison-Wesley, 2003.

第五章:**第二部分

UML**

第六章:UML 和用例简介

Image

统一建模语言(UML) 是一种基于图形的开发语言,用于描述软件设计的需求和标准。最新版本的电气和电子工程师学会(IEEE)软件设计文档(SDD)标准围绕 UML 概念构建,因此我们将首先介绍 UML 的背景和特点,然后再讨论如何使用该语言实现用例,帮助我们清晰、一致地表示软件系统设计。

4.1 UML 标准

UML 最早起源于 1990 年代中期,作为三种独立建模语言的集合:Booch 方法(Grady Booch)、面向对象建模技术(Jim Rumbaugh)和面向对象软件工程系统(Ivar Jacobson)。经过这次初步合并后,面向对象管理组织(OMG)于 1997 年制定了第一个 UML 标准,得到了众多研究人员的输入。今天,UML 仍然由 OMG 管理。由于 UML 本质上是通过统一设计的,它包含了许多不同的方式来指定相同的内容,这导致了许多系统范围的冗余和不一致性。

那么,为什么要使用 UML 呢?尽管它存在一些缺点,但它仍然是一个相当完整的面向对象设计建模语言。它也已成为事实上的 IEEE 文档标准。所以即使你不打算在自己的项目中使用 UML,处理其他项目的文档时,你也需要能够阅读它。因为 UML 已经变得非常流行,所以很有可能你的项目相关方已经熟悉它。它有点像 C 编程语言(或者,如果你不懂 C,可以类比 BASIC):语言设计上它并不美观,但每个人都知道它。

UML 是一种非常复杂的语言,需要相当多的学习才能掌握,这一教育过程超出了本书的范围。幸运的是,关于这个主题有许多优秀的书籍可供参考,有些书籍几乎有 1000 页长(例如,The UML Bible,作者 Tom Pender;请参见 “更多信息” 和 第 88 页)。本章及随后的章节并不是为了让你成为 UML 专家,而是快速介绍本书中使用的 UML 特性和概念。这样,当你在本书后续章节中遇到 UML 图表时,可以回头参考这些章节来帮助理解。

简短的介绍结束后,接下来我们将讨论 UML 如何帮助我们以标准化的方式可视化系统设计。

4.2 UML 用例模型

UML 通过用例来描述系统的功能。用例 大致对应于一个需求。设计师创建 用例图 来指定从外部观察者的角度来看系统做什么,这意味着他们仅指定系统做什么,而不是它是如何做到的。然后,他们会创建一个用例叙述,填充图表的细节。

4.2.1 用例图元素

用例图通常包含三个元素:参与者、通信链接(或关联)以及实际的用例:

  • 参与者通常以火柴人图形表示,代表使用设计中的系统的用户或外部设备和系统。

  • 通信链接绘制为参与者和用例之间的一条线,表示两者之间的某种形式的通信。

  • 用例绘制为椭圆形,具有适当的描述,表示参与者在系统上执行的活动。

图 4-1 展示了一个用例图的示例。

image

图 4-1:一个示例用例图

每个用例应具有一个高层次的名称,简洁且唯一地描述操作。例如,核反应堆操作员可能希望从核电(NP)通道中选择一个功率输入:“选择 %Pwr”是一个通用描述,而“按下 NP 设备上的百分比功率按钮”可能过于具体。用户如何选择百分比功率更多的是设计问题,而不是系统分析问题(分析是我们在此阶段进行的工作)。

用例名称应唯一,因为您可能会用它将图表与 UML 文档中的其他用例叙述关联起来。实现唯一性的一种方法是附加一个标签(请参阅标签格式)在第 172 页)。然而,用例图的主要目的是使操作对读者和利益相关者(即外部观察者)显而易见,而标签可能会使意义不清晰。一种可能的解决方案是在用例椭圆中同时包含描述性名称(或短语)标签,如图 4-2 所示。

image

图 4-2:一个结合了用户友好名称的用例标签

标签唯一地标识用例叙述,且用户友好的名称使得图表易于阅读和理解。

用例图可以包含多个参与者以及多个用例,如图 4-3 所示,其中提供了生成个别兆瓦时(MWH)和其他报告的用例。

image

图 4-3:用例图中的多个参与者和用例

火柴人形象对于快速表明你正在指定一个演员非常有用,但它也有一些缺点。首先,火柴人形象比较大,可能占用相当大的屏幕(或页面)空间。另外,在一个大且杂乱的 UML 图中,将名称和其他信息与火柴人演员关联起来可能会变得困难。因此,UML 设计师通常使用构造型来表示演员。构造型是一个特殊的 UML 名称(如“演员”),它被引号(«和»)包围,并与元素名称一起封闭在矩形内,如图 4-4 所示。(如果你的编辑系统中没有引号,可以使用一对尖括号——小于号和大于号。)

image

图 4-4:一个演员的构造型

构造型可以应用于任何 UML 元素,而不仅仅是演员。构造型占用较少空间,减少了杂乱,但其缺点是,元素的类型不像使用实际图标时那么直观清晰。^(1)

4.2.2 用例包

你可以通过使用一对冒号将用例名称与不同的名称分开,来为不同的包分配用例名称。例如,如果上述的反应堆操作员需要从两个不同的核电系统(NP 和 NPP)中选择百分比功率,我们可以使用NPNPP包来分隔这些操作(参见图 4-5)。

image

图 4-5:用例中的包名称

4.2.3 用例包含

有时,用例会重复信息。例如,图 4-5 中的用例可能对应于反应堆操作员为某个操作选择使用的核电通道(NP 或 NPP 仪表)。如果操作员必须在做出选择之前验证通道是否在线,那么NP::Select%PwrNPP::Select%Pwr中的任一用例可能都包含确认这一点所需的步骤。当编写这两个用例的叙述时,你可能会发现你在重复大量信息。

为了避免这种重复,UML 定义了用例包含,它允许一个用例完全包含另一个用例的功能。

你通过绘制两个椭圆形图标的用例,并在包括用例和被包含用例之间放置一个虚线箭头来指定用例包含。同时,将标签«include»附加到虚线箭头上,如图 4-6 所示。

image

图 4-6:用例包含

我们可以使用包含的方式重新绘制图 4-5,如图 4-7 所示。

image

图 4-7:用例包含示例

包含是用例图中函数调用的等效物。包含允许你从其他用例中重用一个用例,从而减少冗余。

4.2.4 用例泛化

有时候,两个或更多用例共享一个基础设计,并在其基础上构建出不同的用例。回顾图 4-3 中的例子,资深反应堆操作员演员可能会生成额外的反应堆报告(即“所有报告”),而这些报告是反应堆操作员演员生成的报告(“单个 MWH 报告”)之外的。然而,这两个用例仍然是更一般的“生成报告”用例的例子,因此它们共享一些共同的(继承的)操作。这种关系被称为用例泛化

我们可以通过在用例图中画一条从具体用例指向更一般的用例的空心箭头来表示用例泛化,如图 4-8 所示。

image

图 4-8:用例的泛化

该图告诉我们,“单个 MWH 报告”和“所有报告”用例共享一些从“生成报告”用例继承的共同活动。

我们也可以通过画一条从多个(具体的)演员指向一个概括的演员的开放箭头来对演员进行泛化,如图 4-9 所示。

image

图 4-9:演员的泛化

泛化(特别是用例泛化)等同于面向对象系统中的继承。空心箭头指向基础用例,箭头的尾部(即没有箭头的那一端)连接到继承的或派生的用例。在图 4-9 中,“生成报告”是基础用例,而“单个 MWH 报告”和“所有报告”是派生用例。

派生用例继承了基础用例的所有特性和活动。也就是说,基础用例中的所有项和功能都存在于派生用例中,同时还包括一些仅属于派生用例的项。

在图 4-9 中,反应堆操作员演员只能选择“单个 MWH 报告”。因此,反应堆操作员演员生成的任何报告总是遵循与该单个报告相关的步骤。另一方面,资深反应堆操作员演员可以生成任何从“所有报告”或“单个 MWH 报告”用例派生的报告。

尽管泛化看起来与包含非常相似,但它们之间有细微的区别。在包含的情况下,一个用例是完全被包含的,而在继承中,基础用例是通过派生用例中的特性进行扩展的。

4.2.5 用例扩展

UML 的用例扩展允许你指定某些用例的可选(条件性)包含。你绘制一个类似包含的扩展,只不过使用“extend”代替“include”,并且箭头是虚线带实心箭头的。另一个不同点是,箭头指向扩展的用例,箭头尾指向扩展用例,如图 4-10 所示。

image

图 4-10:用例扩展

用例扩展在你希望根据某些内部系统/软件状态选择多个不同用例中的一个时非常有用。一个经典的例子是错误或异常处理条件。假设你有一个小型命令行处理器,它识别以动词开头的某些命令(例如read_digital)。命令语法可能如下所示:

read_digital port#

其中,port#是一个表示要读取端口的数字字符串。当软件处理此命令时,可能会发生两种错误:port#可能存在语法错误(即它不是有效的数字值),或者port#的值超出了范围。因此,处理此命令时可能有三种结果:命令正确并读取指定的端口;发生语法错误,系统向用户展示适当的消息;或者发生范围错误,系统显示适当的错误消息。用例扩展可以轻松处理这些情况,如图 4-11 所示。

image

图 4-11:用例扩展示例

请注意,正常情况(无错误)不是扩展用例。read_port命令用例直接处理无错误的情况。

4.2.6 用例叙述

目前为止,你看到的用例图并没有解释任何细节。一个实际的用例(与用例不同)是文本,而不是图形。用例图提供了用例的“高层概览”,并使外部观察者容易区分活动,但用例叙述才是你真正描述用例的地方。尽管用例叙述中没有固定的一组项,但通常包含表 4-1 中列出的信息。

表 4-1:用例叙述项

用例叙述项 描述
相关需求 与用例相关的需求标签或其他需求指示。这提供了与 SyRS 和 SRS 文档的可追溯性。
角色 与用例交互的角色列表。
目标/目的/简要描述 目标的描述(及其在系统中的背景),以明确用例的目的。
假设和前提条件 执行用例之前必须为真条件的描述。
触发器 启动用例执行的外部事件。
交互/事件流程 描述外部演员在执行用例期间如何与系统逐步交互。
可选交互/替代事件流程 与交互步骤描述的交互不同的替代交互。
终止 导致用例终止的条件。
结束条件 描述用例成功终止或失败时发生的情况。
后置条件 用例执行完成后适用的条件(无论成功或失败)。

额外项(在线搜索描述)可能包括:^(2)

  • 最小保证

  • 成功的保证

  • 对话(实际上是交互的另一种名称)

  • 次要演员

  • 扩展(可选/条件性交互的另一个名称)

  • 异常(即,错误处理条件)

  • 相关用例(即,其他相关的用例)

  • 利益相关者(对用例感兴趣的人)

  • 优先级(在用例实现中的优先顺序)

4.2.6.1 用例叙述的正式性

用例叙述可以从随意到正式不等。

随意的用例叙述是用自然语言(例如,英语)描述的用例,结构上较为简单。随意叙述适用于小型项目,并且通常因用例而异。

完全正式的用例叙述是对用例的正式描述,通常通过填写包含所有项目定义的表单来创建。完全正式的用例叙述通常包括三种形式:

  • 用例项的列表,排除对话/事件流程/交互和替代事件流程/可选交互项

  • 主要事件流程

  • 替代事件流程(扩展)

表 4-2,4-3,和 4-4 显示了完全正式的用例叙述示例。

表 4-2: 选择核电源,RCTR_USE_022

需求 RCTR_SyRS_022, RCTR_SRS_022_000
演员 反应堆操作员,高级反应堆操作员
目标 选择在自动操作期间使用的电力测量通道
假设和前提条件 操作员已登录反应堆控制台
触发器 操作员按下相应按钮,选择自动模式电源
终止 操作员指定的电源被选择
结束条件 如果成功,系统在自动操作时使用选定的电源;如果失败,系统将恢复到原来的自动模式电源
后置条件 系统有一个可用的操作自动模式电源

表 4-3: 事件流程,RCTR_USE_022

步骤 操作
1 操作员按下 NP 选择按钮
2 系统验证 NP 是否在线
3 系统切换自动模式电源选择到核电源通道

表 4-4: 事件的替代流程(扩展),RCTR_USE_022

步骤 动作
2.1 NP 通道不在线
2.2 系统未切换到使用 NP 电源通道,并继续使用先前选择的电源通道进行自动模式
4.2.6.2 事件的替代流程

每当事件流程表中的步骤包含条件或可选项(在 UML 术语中称为扩展)时,你将在替代事件流程表中看到相应的条目,描述当条件项为 false 时的行为。请注意,你不会为每个条件使用单独的替代事件流程表;你只需使用与事件流程表中的步骤编号(例如表 4-3 的步骤 2)关联的子步骤(在本示例中为表 4-4 中的 2.1 和 2.2)。

这只是一个完整的用例叙述的可能示例。还有许多其他形式。例如,你可以创建第四个表来列出所有可能的结束条件,如表 4-5 所示。

表 4-5: 结束条件,RCTR_USE_022

条件 结果
成功 NP 通道被选为自动模式电源通道
失败 先前选择的通道继续控制自动模式

如果有两个以上的结束条件,添加结束条件表尤其有说服力。

另一个示例是考虑图 4-11 中的 read_port 用例。它的叙述可能类似于表 4-6、4-7 和 4-8。

表 4-6: read_port 命令

需求 DAQ_SyRS_102, DAQ_SRS_102_000
角色 PC 主机计算机系统
目标 读取数据采集系统上的数字数据端口
假设和前提条件 数字数据采集端口已初始化为输入端口
触发条件 接收到 read_port 命令
终止条件 数据端口被读取,返回值给请求系统
结束条件 如果命令格式不正确,系统返回端口值或适当的错误信息
后置条件 系统准备好接受另一个命令

表 4-7: 事件流程,read_port 命令

步骤 动作
1 主机 PC 发送以 read_port 开头的命令行
2 系统验证是否存在第二个参数
3 系统验证第二个参数是否为有效的数字字符串
4 系统验证第二个参数是否为 0–15 范围内的数字值
5 系统从指定的端口读取数字数据
6 系统将端口值返回给主机 PC

表 4-8: 事件的替代流程(扩展),read_port 命令

步骤 动作
2.1 第二个参数不存在
2.2 系统向主机 PC 返回“语法错误”消息
3.1 第二个参数不是有效的数字字符串
3.2 系统向主机 PC 返回“语法错误”消息
4.1 第二个参数超出了 0–15 的范围
4.2 系统向主机 PC 返回“范围错误”消息

表 4-8 实际上包含了几个独立的事件流程。小数点左边的主要数字指定了与事件流程表中的步骤相关联的步骤;小数点右边的次要数字是替代事件流程中的特定步骤。流程仅在与单个事件流程编号关联的步骤内发生。也就是说,2.1 到 2.2 的流程在 2.2 结束;它不会继续到 3.1(在这个例子中)。

通常,一旦系统选择了一个替代流程(例如,本例中的“范围错误”流程,步骤 4.1 和 4.2),用例就在完成该替代流程时结束(即在步骤 4.2)。控制不会返回到主事件流程。只有在没有替代流程发生时,才会执行到主事件流程列表的末尾。

使用事件流程和替代事件流程的“正确”方式是编写一个直线序列,表示通过用例的路径,该路径产生预期的结果。如果存在多个可行路径,通常会为每个正确路径创建多个用例。替代流程处理任何偏离正确路径的情况(通常是错误路径)。当然,这种方法的一个风险是,您可能最终会得到过多的用例图。

对于事件流程,图表的创建和维护比文本描述更为昂贵;即使使用适当的 UML 绘图工具,创建图形通常也比仅仅编写文本描述更耗时和精力。

4.2.6.3 条件事件流程

对于具有多个正确路径的用例,您可以使用分支和条件将这些路径编码到主事件流程中,并将替代路径留给异常情况。考虑一个数据采集系统的命令,该命令支持两种不同的语法:^(3)

ppdio boards

ppdio boards boardCount

第一个变体返回系统中的 PPDIO 板数量,第二个变体设置 PPDIO 板的数量。技术上正确的做法是为这两个命令创建两个独立的用例,每个用例都有自己的事件流程。然而,如果数据采集系统有数十个不同的命令,创建独立的用例可能会使文档显得杂乱无章。一个解决方案是通过将条件操作(即if..else..endif)合并到单个事件流程中,将这些用例合并为一个用例,具体如下面的示例所示。

事件流程

  1. 验证命令以ppdio开头。

  2. 验证命令行上的第二个单词是boards

  3. 如果命令行上没有出现额外的参数:

    1. 返回系统中 PPDIO 板的数量作为响应。
  4. 验证该行上是否只有一个数值参数。

  5. 验证数值参数是否在0..6的范围内。

  6. 将 PPDIO 板的数量设置为数值参数中指定的值。

替代流

1.1 如果命令没有以ppdio开头,返回not PPDIO响应。

2.1 如果命令没有以ppdio boards开头,返回not PPDIO BOARDS响应。

5.1 返回syntax error作为响应。

6.1 返回range error作为响应。

在事件流中使用条件语句和多个退出点并不是“干净”的 UML;然而,它可以减少文档的整体大小(节省时间和费用),因此这是用例中常见的解决办法。

你甚至可以在事件流中加入whileforswitch等高级语言风格的操作。但请记住,用例(及其描述)应当非常通用。一旦你开始将编程语言的概念嵌入到用例中,你不可避免地会开始引入实现细节,而这些不属于用例的范畴;这些细节应留到后续的 UML 图(如活动图)中。

这些示例可能让人觉得替代流仅仅用于错误处理,但你也可以用它们来处理其他情况;任何时候条件分支脱离主流,你都可以使用扩展来处理。然而,使用替代流来处理通用条件的一个问题是,原本相关的概念最终会在你的用例描述中被分开,这会使得理解这些描述中的逻辑变得更加困难。

4.2.6.4 泛化与扩展

泛化通常比扩展更有效。例如,假设你有一个通用的port_command用例,并且你想将read_portwrite_port附加到它上面。理论上,你可以创建一个扩展来处理这个问题,如图 4-12 所示。

image

图 4-12:用例扩展的糟糕示例

实际上,这种情况可能更适合使用泛化处理,因为read_portwrite_portport_command的特例(而不是从port_command派生的替代分支)。图 4-13 展示了泛化方法。

image

图 4-13:使用泛化而不是扩展

使用泛化时,派生的用例遵循基本用例中的所有步骤。当使用扩展时,控制从主事件流转移到替代事件流,主事件流中剩余的步骤将不再执行。

4.2.7 用例场景

场景是用例中的单一路径。例如,read_port 用例有四个场景:当命令读取端口并返回端口数据时的成功场景;两个语法错误场景(Alternative Flow of Events 中的 2.1/2.2 和 3.1/3.2);以及一个范围错误场景(Alternative Flow of Events 中的 4.1/4.2)。通过选择完成特定路径的事件流程和备用事件流程中的步骤,你可以生成一个完整的场景。read_port 命令有以下场景:

成功场景

  1. 主机发送以 read_port 开头的命令。

  2. 系统验证是否存在第二个参数。

  3. 系统验证第二个参数是否为数字字符串。

  4. 系统验证第二个参数是否在 0..15 范围内。

  5. 系统从指定的端口读取数据。

  6. 系统将端口值返回给主机 PC。

语法错误 #1 场景

  1. 主机发送以 read_port 开头的命令。

  2. 系统确定没有第二个参数。

  3. 系统向主机 PC 发送语法错误。

语法错误 #2 场景

  1. 主机发送以 read_port 开头的命令。

  2. 系统验证是否存在第二个参数。

  3. 系统确定第二个参数不是合法的数字字符串。

  4. 系统向主机 PC 发送语法错误。

范围错误场景

  1. 主机发送以 read_port 开头的命令。

  2. 系统验证是否存在第二个参数。

  3. 系统验证第二个参数是否为数字字符串。

  4. 系统确定数字字符串的值超出了 0..15 范围。

  5. 系统向主机 PC 发送范围错误。

你可以使用场景来为系统创建测试用例和测试流程。每个场景都会有一个或多个测试用例。

你可以通过在事件流程中加入 if 语句来结合用例场景。然而,由于这会将低级细节引入你的用例叙述中,除非用例叙述数量失控,否则应避免结合场景。

4.3 UML 系统边界图

当你绘制简单的用例图时,应该能明显区分哪些组件是系统内部的,哪些是外部的。具体来说,参与者是外部实体,用例是内部的。不过,如果你使用带有刻板矩形而不是火柴人图形来表示参与者,可能不容易立刻明确哪些组件是系统外部的。此外,如果你在用例图中引用了多个系统,确定哪些用例属于哪个系统可能会很有挑战性。UML 系统边界图解决了这些问题。

UML 系统边界图 只是一个阴影矩形,围绕着特定系统内部的用例,如图 4-14 所示。系统标题通常出现在矩形的顶部附近。

image

图 4-14:系统边界图

4.4 超越用例

本章介绍了 UML 用例,这是统一建模语言中一个非常重要的特性。然而,UML 除了用例之外,还有许多其他组件。下一章将介绍 UML 活动图,它提供了一种在软件设计中建模动作的方法。

4.5 获取更多信息

Bremer, Michael. 用户手册手册:如何研究、编写、测试、编辑和制作软件手册。加利福尼亚州格拉斯谷:UnTechnical Press,1999 年。可以通过www.untechnicalpress.com/Downloads/UMM%20sample%20doc.pdf下载样本章节。

Larman, Craig. 应用 UML 与模式:面向对象分析与设计及迭代开发导论。第 3 版。新泽西州上萨德尔河:普伦蒂斯霍尔,2004 年。

Miles, Russ, 和 Kim Hamilton. 学习 UML 2.0:UML 的实用入门。加利福尼亚州塞巴斯托波尔:O’Reilly Media,2003 年。

Pender, Tom. UML 圣经。印第安纳波利斯:Wiley,2003 年。

Pilone, Dan, 和 Neil Pitman. UML 2.0 概要:桌面快速参考。第 2 版。加利福尼亚州塞巴斯托波尔:O’Reilly Media,2005 年。

Roff, Jason T. UML:初学者指南。加利福尼亚州伯克利:麦格劳-希尔教育,2003 年。

Tutorials Point. “UML 教程。”https://www.tutorialspoint.com/uml/

第七章:UML 活动图**

Image

UML 活动图,传统上称为流程图,用于说明系统不同组件之间的工作流。流程图在软件开发的早期阶段非常流行,并且在面向对象编程(OOP)兴起之前的时期仍然用于软件设计。虽然 UML 面向对象的符号在很大程度上取代了传统的流程图,但 OOP 仍然依赖于小方法、函数和过程来实现低级的、细节丰富的部分,而流程图在这些情况下仍然用于描述控制流。因此,UML 的设计者创建了活动图,作为流程图的更新版本。

5.1 UML 活动状态符号

UML 活动图使用基于传统流程图符号的状态符号。本节描述了你将常用的一些符号。

注意

如果你想了解关于一般流程图的信息,任何网络搜索应该都会有不错的结果。

5.1.1 起始和停止状态

UML 图总是包含一个单一的起始状态,表示开始终端对象。它由一个实心圆和一个从中指向的单箭头(UML 术语中的过渡)组成。你可以将起始状态与一个标签关联,该标签可以是整个活动图的名称。

UML 通常还包含结束状态结束流程符号。结束状态符号表示整个过程的终止,而结束流程符号表示单个线程的终止,适用于涉及多个执行线程的过程。你可以将结束状态符号与一个标签关联,表示过程结束时系统的状态。

图 5-1 显示了起始状态、结束状态和结束流程符号。

image

图 5-1: UML 的起始和结束状态

尽管活动图只有一个起始状态符号,但它可能有多个结束状态符号(想象一个方法从代码中的多个点返回)。附加在各种结束状态上的标签可能不同,如“异常退出”和“正常退出”。

5.1.2 活动

UML 中的活动符号是带有半圆形端点的矩形(类似于流程图中的终止符号),表示某个动作,如图 5-2 所示。^(1)

image

图 5-2: UML 活动

活动通常对应于编程语言中的一条或多条语句(动作),并按顺序执行。符号内的文本描述了要执行的动作,如“读取数据”或“计算 CRC”。一般来说,UML 活动图不包含太多低级细节;提供这些细节的任务由程序员负责。

5.1.3 状态

UML 活动图除了起始状态和结束状态外,还提供了中间状态,这些状态实际上充当了里程碑,指示在状态符号的某一点上存在的某些条件。状态符号是圆角矩形(roundangles),如图 5-3 所示,尽管圆角的大小比活动符号的圆角小得多。

image

图 5-3: UML 状态

状态符号中的文本应描述系统在该特定点的状态。例如,如果活动是“计算 CRC”,你可能会将紧接其后的状态标记为“CRC 已计算”或“CRC 可用”。状态不包含任何动作,只表示在特定时刻系统的当前状态。

5.1.4 过渡

过渡表示活动图中从一个点(例如状态或活动)到另一个点的控制流。如果一个过渡从某个活动流出,则表示系统在完成该活动的大部分动作后会进行该过渡。如果一对过渡流入并流出一个状态,则控制流会立即转移到流出箭头所指向的地方。UML 状态实际上是过渡中的一个标记,因此在 UML 状态中不会发生任何动作,如图 5-4 所示。

image

图 5-4: 通过一个状态的控制流

5.1.5 条件语句

在 UML 活动图中,你可以通过几种不同的方式处理条件语句:过渡守卫和决策点。

5.1.5.1 过渡守卫

在条件语句中,布尔表达式附加在过渡符号上。UML 称这些布尔表达式为守卫。一个条件 UML 符号必须至少有两个受保护的过渡,这些过渡用方括号括起来的表达式标注,但可能有超过两个,如图 5-5 所示(其中六边形形状表示一个任意的 UML 符号)。

image

图 5-5: 过渡守卫

布尔表达式的集合必须是互斥的;也就是说,在任何时候,只有一个表达式可以为true。此外,表达式覆盖必须是完整的,在此语境中,意味着对于所有可能的输入值组合,至少一个布尔表达式在一组受保护的过渡中必须评估为true(这与第一个条件结合起来,意味着只有一个布尔条件必须评估为true)。

如果你希望有一个“通用”的过渡来处理现有守卫未处理的任何输入值,只需在过渡上附加如elseotherwisedefault 等词语(见图 5-6)。

image

图 5-6: 通用过渡守卫

5.1.5.2 决策点

带有守卫的转换几乎可以从任何 UML 符号中退出;状态和动作符号通常包含它们。然而,如果多个动作或状态合并到一个决策点,这时可能会产生问题,决策可能会创建分支路径。为此,UML 提供了一个特殊符号,决策点,来清晰地收集和连接发生决策分支的路径。决策点使用菱形符号,如图 5-7 所示。

image

图 5-7:一个 UML 决策点

尽管 UML 允许受控转换从任何 UML 符号中发出,但最佳实践是始终使用决策点来开始一组相关的受控转换。

5.1.6 合并点

在 UML 中,我们还可以使用菱形符号将多个进入的转换收集到一个外出的转换中,如图 5-8 所示;我们称之为合并点

image

图 5-8:一个 UML 合并点

从技术上讲,合并点和决策点是相同的对象类型。实质上,合并点是一个没有名字的状态对象;它除了将控制从所有进入的转换传递到出去的转换外,不进行其他任何操作。决策点则是合并点的一个特殊情况,它有多个外出的受控转换。

从理论上讲,合并点可以有多个进入和外出的受控转换。然而,结果可能会非常复杂,因此常规做法是将单一的合并点拆分成独立的合并点和决策点,如图 5-9 所示。大多数情况下,这种分离比替代方案更清晰且易于阅读。

image

图 5-9:UML 合并点和决策点

5.1.7 事件和触发器

事件和触发器是控制流外的动作,通常来自其他执行线程或硬件输入,它们会导致控制流的某些变化。^(2) 在 UML 中,事件和触发器转换在语法上与带有守卫的转换相似,因为它们都由标记的转换组成。不同之处在于,带有守卫的转换会立即评估某个布尔表达式,并将控制转移到转换另一端的 UML 符号,而事件或触发器转换则在事件或触发器发生之前等待,然后才转移控制。

事件和触发器转换标有事件或触发器的名称,以及发生时提供给控制流的任何必要参数(参见图 5-10)。

image

图 5-10:UML 事件或触发器

在这个例子中,系统正在等待用户的输入(可能是点击显示屏上的一个 UI 按钮)。当用户激活保存、退出或加载操作时,控制将转移到事件或触发器转换末尾指定的动作(分别是保存文件、退出程序或加载文件)。

你还可以将守卫条件附加到事件或触发过渡中,守卫条件是一个布尔表达式,位于触发器或事件后面的方括号内,如图 5-11 所示。这样,过渡只有在事件或触发发生且守卫表达式求值为true时才会发生。

image

图 5-11:事件或触发器上的守卫条件

UML 事件和触发器还支持动作表达式和多个动作,这些内容超出了本章的范围。如需了解更多内容,请查阅 Tom Pender 的UML 圣经中的示例(参见更多信息在第 100 页)。

5.1.8 分叉与合并(同步)

UML 通过提供符号支持并发处理,允许将单个执行线程拆分成多个线程,以及将多个执行线程合并为单个线程(见图 5-12)。^(3)

image

图 5-12:分叉与合并

UML 的分叉操作(一个细的实心矩形)将单一的执行线程分割成两个或多个并发操作。合并操作(同样由细的实心矩形表示)将多个线程集合合并为一个执行线程。合并操作还会同步线程:该图假设,进入合并操作的所有线程,除了最后一个线程,将会暂停,直到最后一个线程到达,这时单个执行线程将继续进行。

5.1.9 调用符号

UML 中的调用符号,看起来像一个小耙子,附加到活动上,明确声明它是另一个 UML 序列的调用。你将在 UML 活动中包含调用符号,并附上要调用的序列名称,如图 5-13 所示。

在 UML 文档的其他部分,你会使用调用名称作为活动图的名称来定义该序列(或子程序),如图 5-14 所示。

image

图 5-13:一个 UML 序列调用

image

图 5-14:一个 UML 子程序

5.1.10 分区

分区用于组织一个过程的步骤,由多个并排的矩形框组成,每个框的顶部标有一个参与者、对象或域名。^(4) 活动图在框之间过渡,当过程的每个部分由某个框的拥有者控制时,正如图 5-15 所示。

image

图 5-15:一个 UML 分区

图 5-15 中的过程展示了正在测试的代码。一个操作员选择要运行的测试,控制权交给测试软件。一个事件或触发器随后将控制权转交给“运行测试#1”的动作。测试软件调用正在测试的代码(位于第三个分区)。在代码执行完毕后,控制返回测试软件,测试软件根据测试是否通过来决定是显示“通过”给操作员,还是运行诊断程序。

5.1.11 评论和注释

UML 中的评论和注释使用一个看起来像小纸张并有折角的图标,如图 5-16 所示。你需要从框的一侧画一条虚线,指向你想要注释的 UML 元素。

image

图 5-16:UML 评论或注释

5.1.12 连接器

连接器是带有内部标签(通常是数字)的圆圈,表示控制流转移到图中另一个具有相同标签的点(见图 5-17)。你可以使用相同的符号表示页面内连接器和页面外连接器。

image

图 5-17:UML 连接器

正确使用 UML 连接器可以通过减少冗长或重叠的过渡线,使活动图更易于阅读。但要记住,连接器是编程语言中goto语句的 UML 等价物,过度使用可能会使图表更难阅读。

5.1.13 额外的活动图符号

完整的 UML 2.0 规范提供了许多可以在活动图中使用的附加符号,例如结构化活动、扩展区域/节点、条件节点、循环节点等。在本书的 UML 基础介绍中没有空间讨论所有这些内容,但如果你对更多细节感兴趣,请参见第 100 页中“更多信息”部分列出的资料,或在线搜索“UML”。

5.2 扩展 UML 活动图

有时 UML 活动图的符号表示无法满足需求。在这种情况下,你可能会有动机创造自己定制的符号。这几乎总是个坏主意,原因如下:

  • UML 是一个标准。如果你扩展了 UML,就不再使用一个明确的标准。这意味着所有学习过 UML 的人将无法理解你的活动图,除非他们首先阅读你的文档(而且这些文档会在你非标准的活动图中对他们可用吗?)。

  • 有许多可用的 UML 图形工具用于创建和编辑 UML 活动图,而它们大多数无法处理非标准符号和对象。

  • 许多计算机辅助软件工程(CASE)工具可以直接从 UML 图生成代码。同样,这些 CASE 工具只适用于标准 UML,可能无法处理你的非标准扩展。

  • 如果你无法弄清楚如何在 UML 活动图中实现某个功能,你或许可以使用其他方案。用非标准的方法做一些你能通过标准工具轻松完成的任务,可能会被其他 UML 用户认为是一种业余的做法。

话虽如此,UML 仍然远远不完美。在少数情况下,开发一些非标准的活动图对象可以极大地简化你的活动图。

作为一个例子,考虑一个并发编程中的临界区,这是一个只有一个执行线程可以在同一时间内运行的代码区域。UML 序列图(详见第七章)使用序列片段符号来描述具有临界区的并发性。虽然你可以将序列片段符号应用于活动图,但结果会显得杂乱无章,且难以阅读和理解。在我为个人项目创建的一些活动图中,我使用了图 5-18 中的自定义符号来表示临界区。

image

图 5-18:非标准的临界区图

从左侧五边形进入的箭头表示转换(通常来自不同的线程),它们在争夺一个临界区。五边形外的单一箭头表示发生在临界区内的单一执行线程。右侧的五边形接受这个单一执行线程,并将其路由回原始线程(例如,如果 T1 是进入临界区的线程,则临界区结束时,控制将返回到 T1 的转换/流程)。

这个图表并不意味着只有五个线程可以使用这个临界区。它表达的是五个活动图流程(T1–T5)可能会竞争这个临界资源。实际上,可能有多个线程在执行这些流程中的任何一个,并且也在争夺临界区资源。例如,可能有三个线程在执行 T1 流程并等待临界区可用。

由于多个线程可能在同一个流程中执行,因此在临界区图中,只进入临界区的流可能是单一的(见图 5-19)。

image

图 5-19:单流临界区图

这个示例要求多个线程执行相同的流程(T1),才能使这个图表有意义。

正如你所看到的,即使是这样一个简单的图示,也需要相当多的文档来描述和验证它。如果这些文档不可用(即,如果它们没有直接嵌入你的 UML 活动图中),读者在试图理解你的图示时可能找不到它。当你在图示中直接注释一个非标准对象时,这是唯一合理的做法。将有意义的文档放在包含活动图的文档的单独部分(例如 SDD 文档)或完全放在另一个文档中,当别人将你的图示剪切并粘贴到其他文档时,这些信息将无法使用。

注意

图 5-19 中的临界区域图只是你可能扩展 UML 活动图的一种示例。一般来说,我不建议在你自己的图示中采用它,也不建议扩展 UML 符号。然而,如果你真的需要,你应该知道这个选项是可以使用的。

5.3 更多信息

Bremer, Michael. 《用户手册手册:如何研究、编写、测试、编辑和制作软件手册》。加利福尼亚州格拉斯谷:UnTechnical Press,1999 年。可以在 www.untechnicalpress.com/Downloads/UMM%20sample%20doc.pdf 获取示例章节。

Larman, Craig. 《应用 UML 和模式:面向对象分析与设计及迭代开发导论》。第三版。新泽西州上萨德尔河:Prentice Hall,2004 年。

Miles, Russ 和 Kim Hamilton. 《学习 UML 2.0:UML 的务实入门》。加利福尼亚州塞巴斯托波尔:O'Reilly Media,2003 年。

Pender, Tom. 《UML 圣经》。印第安纳波利斯:Wiley,2003 年。

Pilone, Dan 和 Neil Pitman. 《UML 2.0 概览:桌面快速参考》。第二版。加利福尼亚州塞巴斯托波尔:O'Reilly Media,2005 年。

Roff, Jason T. 《UML:初学者指南》。加利福尼亚州伯克利:McGraw-Hill Education,2003 年。

Tutorials Point. “UML 教程。” https://www.tutorialspoint.com/uml/

第八章:UML 类图**

Image

本章介绍了类图,这是 UML 中一个非常重要的图示工具。类图是定义程序中数据类型、数据结构和对数据进行操作的基础。反过来,它们也是面向对象分析 (OOA)面向对象设计 (OOD)的基础。

6.1 UML 中的面向对象分析与设计

UML 的创建者希望有一个正式的系统来设计面向对象的软件,以取代当时(1990 年代)可用的结构化编程形式。在这里,我们将讨论如何在 UML 中表示类(数据类型)和对象(数据类型的实例变量)。

UML 中最完整的类图形式见图 6-1。

image

图 6-1:完整的类图

属性 对应于类的数据字段成员(即变量和常量);它们代表类内部的信息。

操作 对应于表示类行为的活动。操作包括方法、函数、过程以及我们通常认作代码的其他内容。

有时,引用类图时不需要列出所有的属性和操作(或者根本没有属性和操作)。在这种情况下,你可以绘制部分类图,如图 6-2 所示。

image

图 6-2:部分类图

部分类图中缺少属性或操作并不意味着它们不存在;这只是表示在当前上下文中没有必要将它们添加到图中。设计者可能会让编码者在编码时自行补充这些内容;或者可能完整的类图在其他地方出现,而当前图中只包含感兴趣的信息。

在最简单的形式中,UML 使用一个简单的矩形表示类,矩形内包含类的名称,如图 6-3 所示。

image

图 6-3:一个简单的类图

再次强调,这并不意味着该类没有属性或操作(那样的话就没有意义);这只是表示这些项目在当前图中不重要。

6.2 类图中的可见性

UML 定义了四种类成员的可见性(这些都来自 C++ 和 Java,尽管其他语言,如 Swift,也支持它们):public(公共)、private(私有)、protected(受保护)和 package(包)。我们将逐一讨论每种可见性。

6.2.1 公共类可见性

公有类成员对所有类和代码都是可见的,既可以在包含该公有项的类内部访问,也可以在外部访问。在设计良好的面向对象系统中,公有项几乎总是操作(方法、函数、过程等),并构成类对外部世界的接口。尽管你也可以将属性设置为公有,但这样做往往会破坏面向对象编程的主要好处之一:封装性,即能够将类内部的值和活动隐藏起来,防止外界访问。

在 UML 中,我们使用加号(+)作为公有属性和操作的前缀,如图 6-4 所示。公有属性和操作的集合提供了类的公有接口

image

图 6-4:公有属性和操作

该图包含一个公有属性,maxSalinity_c_c后缀是我用来表示该字段是常量而非变量的约定。^(1) 在良好的设计中,常量通常是类中唯一的公有属性,因为外部代码不能更改常量的值:它仍然可见(即没有被隐藏或封装),但不可更改。封装的主要原因之一是防止外部代码更改内部类属性时产生副作用。由于外部代码不能更改常量的值,这种不可变性实现了与封装相同的效果;因此,面向对象的设计者愿意使某些类常量可见。^(2)

6.2.2 私有类可见性

在另一端是私有可见性。私有属性和操作仅在该类内部可访问:它们对其他类和代码隐藏。私有属性和操作是封装性的体现。

我们使用减号(-)表示类图中的私有实体,如图 6-5 所示。

image

图 6-5:私有属性和操作

你应该对任何不绝对需要其他可见性形式的属性或操作使用私有可见性,并力求确保所有属性(类中的数据字段)都是类的私有成员。如果外部代码需要访问数据字段,你可以使用公有的访问器函数(获取器和设置器)来提供对私有类成员的访问。获取器函数返回私有字段的值。设置器函数将一个值存储到私有字段中。

如果你在想,为什么还要使用访问器函数(毕竟,直接访问数据字段不是更简单吗?^(3)),请考虑以下情况:setter 函数可以检查你存储在属性中的值,确保它在范围内。另外,并非所有字段都独立于类中的其他属性。例如,在盐水游泳池中,盐度、氯含量和 pH 值并不是完全独立的:泳池中有一个电解池,将水和氯化钠(盐)转化为氢氧化钠和氯气。这一转化过程会降低盐度,同时提高氯含量和 pH 值。因此,与其让外部代码任意修改盐度值,你可能希望通过 setter 函数传递该修改,以便它可以决定是否同时调整其他水平。

6.2.3 受保护类可见性

尽管公有和私有可见性涵盖了大部分可见性需求,但在某些特殊情况下,如继承,你需要使用介于两者之间的可见性:受保护可见性。

继承与封装和多态一起,是面向对象编程的“三大特性”之一。继承允许一个类接收另一个类的所有特性。

私有可见性的问题在于你不能在继承它们的类中访问私有字段。然而,受保护可见性放宽了这些限制,允许继承类访问,但不允许类外部或其继承类访问私有字段。

UML 注记使用井号(#)来表示受保护的可见性,如图 6-6 所示。

image

图 6-6:受保护的属性和操作

6.2.4 包类可见性

包可见性位于私有和受保护之间,主要是 Java 的概念。其他语言也有类似的东西,包括 Swift、C++ 和 C#,你可以使用命名空间来模拟包可见性,尽管语义上并不完全相同。

包保护字段在同一包中的所有类之间是可见的。包外的类(即使它们继承了包含包保护字段的类)不能访问具有包可见性的项。

我们使用波浪号(~)来表示包可见性,如图 6-7 所示。第八章讨论了 UML 包注记(即如何将多个类放置在同一个包中)。

image

图 6-7:包属性和操作

6.2.5 不支持的可见性类型

如果你选择的编程语言不支持 UML 指定的相同可见性类型,会发生什么呢?好消息是,UML 可见性在很大程度上是一个范围,如图 6-8 所示。^(4)

image

图 6-8:可见性范围

如果你的编程语言不支持特定的可见性,你总可以将更私有的可见性替换为更公开的可见性。例如,高级汇编(HLA)语言仅支持公共字段;C++仅部分支持包可见性(通过friend声明或命名空间);Swift 支持包可见性的一个分支——对象内的所有私有字段会自动对同一源文件中声明的所有类可见。一种避免滥用额外可见性的方法是为属性或操作的名称添加某种可见性标注——例如,通过在受保护的名称前加上prot_,然后将其声明为公共对象,如图 6-9 所示。

image

图 6-9:伪造可见性限制

6.3 类属性

UML 类中的属性(也称为数据字段或简单地称为字段)包含与对象关联的数据。属性有可见性和名称;它还可以有数据类型和初始值,如图 6-10 所示。

image

图 6-10:属性

6.3.1 属性可见性

如前所述,你通过在属性名称前加上+-#~符号来指定属性的可见性,分别表示公共、私有、受保护和包可见性。有关更多细节,请参见“类图中的可见性”在第 105 页。

6.3.2 属性派生值

大多数情况下,类将属性值存储为变量或常量数据字段(即基础值)。然而,一些字段包含派生值,而不是基础值。每当某个表达式引用该属性时,类会计算派生值。一些语言(如 Swift)提供了直接定义声明值的语法;而在其他语言(如 C++)中,你通常需要编写 getter 和 setter 访问器函数来实现派生值。

在 UML 中创建派生属性时,立即在属性名称前(可见性符号之后)加上斜杠(/),如图 6-11 所示。

image

图 6-11:派生属性

每当你使用派生属性时,必须在某处定义如何计算它。图 6-11 为此目的使用了注释,尽管你也可以使用属性字符串(参见“属性字符串”在第 112 页)。

6.3.3 属性名称

属性名称应该适用于你用来实现设计的任何编程语言。尽量避免使用特定编程语言的语法或约定,除非要求使用该语言来实现。通常,以下约定适用于 UML 属性名称:

  • 所有名称应以一个(ASCII)字母字符(a–z 或 A–Z)开头。

  • 在第一个字符之后,名称应仅包含 ASCII 字母字符(a–z, A–Z)、数字(0–9)或下划线(_)。

  • 所有名称的前六到八个字符应当是唯一的(某些编译器允许任意长度的名称,但在编译过程中仅在内部符号表中保留其前缀)。

  • 名称应该短于某个任意长度(这里我们使用 32 个字符)。

  • 所有名称应当是不区分大小写的;也就是说,两个不同的名称必须包含至少一个不同的字符,而不仅仅是大小写的差异。此外,给定名称的所有出现应该在字母大小写方面保持一致。^(5)

6.3.4 属性数据类型

UML 对象可以选择性地关联一个数据类型(参见图 6-10 中的示例)。UML 并不要求你明确声明数据类型;如果缺少数据类型,假设读者可以从属性的名称或用法中推断出数据类型,或者程序员在实现设计时决定数据类型。

你可以为原始数据类型使用任何类型名称,并让程序员在编写代码时选择适当的或最匹配的数据类型。也就是说,在使用通用数据类型时,大多数人选择 C++或 Java 的类型名称(这很有道理,因为 UML 的设计在很大程度上是基于这两种语言的)。你会在 UML 属性中看到的常见数据类型包括:

  • int, long, unsigned, unsigned long, short, unsigned short

  • float, double

  • char, wchar

  • string, wstring

当然,任何用户定义的类型名称也是完全有效的。例如,如果你在设计中将uint16_t定义为与unsigned short相同的含义,那么将uint16_t作为属性类型是完全可以接受的。此外,你在 UML 中定义的任何类对象也都可以作为有效的数据类型名称。

6.3.5 操作数据类型(返回值)

你还可以将数据类型与操作关联。例如,函数可以返回某种数据类型的值。为了指定返回数据类型,可以在操作名称(和参数列表)后跟一个冒号和数据类型,如图 6-12 所示。

image

图 6-12:返回类型

我们将在“类操作”一节中进一步讨论操作,第 112 页。

6.3.6 属性多重性

一些属性可能包含数据对象的集合(数组或列表)。在 UML 中,我们使用方括号[]表示基数,类似于许多高级语言中的数组声明;见图 6-13。

image

图 6-13:基数

在方括号内,您可以指定一个表达式,可以是以下任意内容:

  • 一个数字值(例如,5)表示集合中元素的数量

  • 一个数字范围(例如,1..50..7),表示集合元素的数量和有效的后缀范围

  • 一个星号(*)表示任意数量的元素

  • 一个星号终止的范围(例如,0..*1..*),表示一个开放的数组元素范围

如果没有这个符号,基数默认为[1](即单个数据对象)。

6.3.7 默认属性值

要为属性指定初始值,使用等号(=)后跟一个表达式(类型应适合该属性)。这通常跟随属性的基数(如果存在)和/或类型。但如果类型可以从初始值推断出来,则可以省略类型和基数。如果基数不是1,则在大括号内包含一个逗号分隔的初始值列表,每个元素一个。见图 6-14。

image

图 6-14:初始值

在这个示例中,numTempSensors属性是integer类型(可以从初始值2推断出来),而tempSensorSpan是一个包含两个元素的double数组(通过大括号中的数量和类型的值来推断)。

6.3.8 属性字符串

UML 的属性语法可能无法覆盖所有可能的属性情况。UML 提供了属性字符串来处理特殊情况。要创建一个属性字符串,您需要在属性末尾添加描述它的文本,并将其放在大括号内,如图 6-15 所示。

image

图 6-15:属性字符串

您还可以使用属性字符串来定义其他属性类型。常见的例子包括{readOnly}{unique}{static}。^(6) 请记住,属性字符串是属性中的一个通用字段。您可以在大括号内定义任何语法。

6.3.9 属性语法

属性的正式语法如下所示(注意,选项项出现在大括号中,除了引用的大括号,它们表示字面上的大括号字符):

{visibility}{"/"} name { ":" type }{multiplicity}{"=" initial}{"{"property string"}"}

6.4 类操作

类操作是类中的项目,执行某些操作。通常,操作代表类中的代码(但也可能有与派生属性相关联的代码,因此代码不仅限于 UML 类中的操作)。

UML 类图将属性和操作放入不同的矩形中,尽管这并不是区分它们的标准。(考虑图 6-2:部分类图在区分哪些类图只包含属性,哪些类图只包含操作方面存在歧义。)在 UML 中,我们通过在操作名称后加上(可能为空的)参数列表并用括号括起来来明确指定操作(参考图 6-4 查看示例)。

如“操作数据类型(返回值)”一节中所述,在第 110 页你还可以通过在参数列表后加上冒号和数据类型名称来为操作指定返回类型。如果存在类型,则表示你有一个函数;如果缺少类型,则你可能有一个过程(空函数)。

到目前为止,所有操作示例中缺少的都是参数。要指定参数,可以在操作名称后面的括号内插入以逗号分隔的属性列表,如图 6-16 所示。

image

图 6-16:操作参数

默认情况下,UML 操作中的参数是 参数,这意味着它们作为参数传递给操作,且操作对值参数所做的更改不会影响调用者传递给函数的实际参数。值参数是 输入参数

UML 还支持 输出 参数和 输入/输出 参数。顾名思义,输出参数将信息从操作返回到调用代码;输入/输出参数则将信息传递给操作并返回数据。UML 使用以下语法表示输入、输出和输入/输出参数:

  • 输入参数:in paramName:paramType

  • 输出参数:out paramName:paramType

  • 输入/输出参数:inout paramName:paramType

默认的参数传递机制是输入。如果参数名前没有任何说明,UML 假设它是一个 in 参数。图 6-17 展示了一个简单的 inout 参数示例。

image

图 6-17:参数输入输出示例

在这张图中,待排序项的列表是一个输入 输出参数。在输入时,items 数组包含待排序的数据;在输出时,它包含排序后的项(就地排序)。

UML 尽量做到尽可能通用。inoutinout 参数传递说明符并不一定意味着按值传递或按引用传递。这个实现细节留给实际的实现者来决定。从设计角度来看,UML 只指定数据传输的方向,而不是数据如何被传输。

6.5 UML 类关系

在本节中,我们将探讨类之间的五种不同关系:依赖关系、关联关系、聚合关系、组合关系和继承关系。

类似于可见性,类之间的关系沿着一个光谱分布(见图 6-18)。这个范围是基于它们的强度,即两个类之间的相互通信的级别和类型。

image

图 6-18:类关系光谱

强度范围从松散耦合紧密耦合。当两个类是紧密耦合时,对一个类的任何修改很可能会影响到另一个类的状态。松散耦合的类通常相互独立;对一个类的修改不太可能影响到另一个类。

我们将依次讨论每种类型的类关系,从最弱到最强。

6.5.1 类依赖关系

当一个类的对象需要与另一个类的对象短暂合作时,这两个类就形成了依赖关系。在 UML 中,我们使用一个虚线开口箭头来表示依赖关系,如图 6-19 所示。

image

图 6-19:依赖关系

在这个例子中,每当userInterface对象想要检索数据进行显示时(例如,当你将poolMonitor对象作为参数传递给userInterface方法时),userInterfacepoolMonitor类就会协同工作。除此之外,两个类(以及这些类的对象)相互独立操作。

6.5.2 类关联关系

关联关系发生在一个类包含一个属性,而该属性的类型是另一个类时。UML 中有两种表示关联关系的方式:内联属性和关联链接。你已经见过内联属性——它们是你在第 112 页的“属性语法”中看到的常规属性定义。唯一的要求是类型名称必须是其他类。

指定类关联关系的第二种方式是通过关联线链接,如图 6-20 所示。

image

图 6-20:关联关系

关联名称通常是一个动词短语,描述关联关系,如拥有控制由…拥有由…控制(见图 6-21)。

image

图 6-21:关联名称

我们如何从关联图中判断哪个类是另一个类的属性呢?注意关联名称左侧或右侧的箭头头部。这指示了关联的方向;在这里,它表明poolMonitor有一个phClass,而不是反过来。

尽管一个有意义的关联名称和箭头动词短语可以给你一个线索,但无法保证你的直觉是正确的。虽然这看起来可能违反直觉,但在图 6-21 中,pumpClass可能包含poolMonitor对象作为一个属性,即使poolMonitor类控制着pumpClass对象。UML 的解决方案是通过应用可导航性(参见“可导航性”在第 123 页)来处理,通过在指向作为其他类属性的类上放置一个开口箭头,如图 6-22 所示。

image

图 6-22:关联的可导航性

6.5.3 类聚合关系

聚合是关联的一个稍微紧密耦合的版本,存在于一个可能是独立存在的类,但又是一个更大类的一部分。大多数时候,聚合关系是一种控制关系;也就是说,一个控制类(聚合类整体类)控制一组从属对象或属性(部分类)。聚合类不能在没有部分类的情况下存在;然而,部分类可以在聚合类的上下文之外存在(例如,一个部分类可以与聚合类和另一个类建立关联)。

聚合作为门控器,负责其部分属性,确保部分方法以适当的参数(例如,范围检查)被调用,并且这些部分的操作环境是一致的。聚合类还可以检查返回值的一致性,处理部分类引发的异常和其他问题。

例如,你可以有一个pHSensor类,它与独立的 pH 计配合得很好,一个salinitySensor类,它与独立的盐度(或电导率)传感器配合得很好。poolMonitor类不是一个独立的类:它需要这两个类来完成它的工作,尽管它们不需要poolMonitor来完成它们的工作。我们通过在聚合类(poolMonitor)上使用空心菱形符号,并通过一条关联线连接到部分类(pHSensorsalinitySensor)来建模这种关系,如图 6-23 所示。

image

图 6-23:聚合关系

关联线的开口菱形端(即聚合类)总是包含关联线另一端的属性类(部分类)。

聚合对象及其关联部分对象的生命周期不一定相同。你可以创建多个部分对象,然后将它们附加到聚合对象上。当聚合对象完成任务后,它可以被销毁,而部分对象则可以继续解决其他问题。换句话说,从低级编程的角度来看,系统在聚合对象中存储指向部分对象的指针。当系统销毁聚合对象的存储时,指针可能会消失,但它们所引用的对象可能会继续存在(并且可能会被系统中的其他聚合对象指向)。

为什么使用聚合图?对于关联和聚合生成的代码是相同的。区别在于意图。在聚合图中,设计师表示部分对象或类由聚合类或对象控制。以我们的poolMonitor示例为例,在聚合关系中,poolMonitor完全掌控——salinitySensorpHSensor对象由它控制,绝不是反过来。而在关联关系中,相关类是平等的,而不是主从关系;也就是说,pHSensorsalinitySensor可以独立于poolMonitor操作——反之亦然——仅在必要时共享信息。

6.5.4 类的组合关系

在组合关系中,较小的类是由较大的类包含的,这些小类不是独立的类:它们仅存在于支持包含它们的组合类。与聚合不同,组合部分只能属于单一的组合。

组合对象和部分对象的生命周期是相同的。当你销毁组合对象时,包含的部分对象也会被销毁。组合对象负责分配和回收与部分对象相关的存储空间。

我们使用实心菱形来表示组合关系,如图 6-24 所示。

image

图 6-24:组合关系

6.5.5 关系特性

对于依赖、关联、聚合和组合关系,UML 支持这 10 个特性,其中一些你已经见过:

  • 属性名称

  • 角色

  • 接口说明符

  • 可见性

  • 多重性

  • 顺序

  • 约束

  • 限定符

  • 可导航性

  • 可变性

这些特性不适用于继承关系,这就是为什么我还没有描述它。我们将在后面的“类继承关系”部分中讨论继承内容,具体见第 125 页,但首先我们会讲解这些关系特性。

注意

为了简化,我使用关联来讨论每个特性,但依赖、聚合组合都同样适用。

6.5.5.1 关联和属性名称

附加到链接上的关联名称可以告诉你交互的类型或所有权,但它并没有说明这两个类是如何相互引用的。关联链接仅提供两个类对象之间的连接。类通过类定义中的属性和操作字段来相互引用。

正如你在“类关联关系”部分中阅读到的,关联图实际上是将属性或操作名称嵌入类中的内联语法的替代方案。图 6-25 中的两个图示是等效的。

image

图 6-25:简写(上)和长写(下)关联关系图

在图 6-25 中,简写版本缺少了属性或操作名称(在此示例中为pHSensor)以及可见性(-,或私有),但你可以通过将属性名称附加到离对象最近的关联链接上,来补充这些缺失的部分,如图 6-26 所示。

与内联语法一样,属性名称由属性或操作名称和可见性符号前缀(-~#,或+)组成。可见性符号必须存在,因为它区分了属性名称和角色(接下来将描述)。

image

图 6-26:属性名称

另一种选择是将关联和属性名称结合在一起,如图 6-27 所示。

image

图 6-27:组合关联和属性名称

6.5.5.2 角色

在图 6-27 中,两个类的作用并不完全清晰。poolMonitor类有一个连接到pHClasspHSensor字段,但图示并未解释发生了什么。通常出现在关联链接两端的角色,提供了缺失的描述。

在此示例中,poolMonitor类或对象通常从 pH 传感器设备(封装在pHClass中)读取 pH 值。反之,pHClass类或对象可以提供 pH 读数。你可以使用 UML 中的角色来描述这两个活动(读取 pH 和提供 pH 值)。图 6-28 展示了这些角色的示例。

image

图 6-28:角色

6.5.5.3 接口说明符

接口是一组预期由某些类提供的操作。它类似于类,但不会实例化对象。遵循接口的类确保提供接口中所有存在的操作(并为这些操作提供方法)。如果你是 C++程序员,你可以把接口看作是一个只包含抽象成员函数的抽象基类。Java、C#和 Swift 都有各自的定义接口(也称为协议)的特殊语法。

注意

接口说明符在 UML 1.x 中受支持,但已从 UML 2.0 中删除。我在本章中描述它们,因为你可能会遇到它们,但你不应该在新的 UML 文档中使用它们,因为它们已被弃用。

如果一个类实现了一个接口,它实际上是继承了该接口的所有操作。也就是说,如果一个接口提供了操作 A、B 和 C,而某个类实现了该接口,那么这个类也必须提供操作 A、B 和 C(并提供这些操作的具体实现)。有两种不同的方式来指定一个接口——使用 立体符号球体符号,如 图 6-29 所示。

image

图 6-29:接口语法:立体符号(顶部)和球体符号(底部)

为了表示一个类实现了某个接口,你需要从类到接口图绘制一条带有空心箭头的虚线,如 图 6-30 所示。

image

图 6-30:接口实现图

6.5.5.4 可见性

可见性适用于关联链接中的属性名。如前所述,所有属性名必须以符号(-~#+)作为前缀,用来指定它们的可见性(分别为私有、包内、受保护或公共)。

6.5.5.5 多重性

“属性多重性”部分在 第 111 页 介绍了内联属性的多重性。你还可以通过在关联链接的两端或其中一端指定多重性值,将多重性包含在关联图中(参见 图 6-31)。将多重性值放在链接的上方或下方,且尽可能靠近它所适用的类或对象。如果没有提供多重性值,则默认值为 1

image

图 6-31:关联链接上的多重性

图 6-31 表明存在一个 poolMonitor 对象,它可以有一个或多个关联的 pHSensor(例如,水疗池和游泳池中可能分别有不同的 pH 传感器)。

这个示例展示了一个 一对多 的关系。图中也可以有 多对一 甚至 多对多 的关系。例如,图 6-32 显示了 poolMonitorpHClass 类或对象之间的多对多关系(如果你很难想象这种关系如何运作,可以考虑一个有多个泳池和多个 pH 测量仪的水上乐园)。

image

图 6-32:多对多关系

6.5.5.6 排序

UML 提供了 {ordered} 约束,你可以将其附加到任何多重性不是 1 的关联上(参见 图 6-33)。

image

图 6-33:有序关联

当单独出现时,{ordered} 约束并未指定如何排序项目列表,只说明它们有序的。排序的方式必须由实现来处理。

6.5.5.7 约束

约束是应用程序特定的文本,它放在大括号内并附加到关联链接上。尽管 UML 有一些预定义的约束(如刚刚提到的 {ordered} 约束),通常你会创建自己的约束来对关联链接提供应用程序定义的控制。你甚至可以通过在大括号内用逗号分隔来指定多个约束。例如,图 6-33 中的单一 {ordered} 约束并未描述如何对温度历史信息进行排序。你可以通过向图示中添加另一个约束(如 按日期/时间排序)来指定排序方式,如 图 6-34 所示。

image

图 6-34:一个自定义约束

6.5.5.8 限定符

限定符告知实现者,指定的关联需要快速访问,通常是通过键或索引值。例如,假设 图 6-34 中的温度记录机制每分钟记录一次游泳池温度。在一周的时间里,历史对象将积累 10,080 次读取;一年内,它将积累超过 360 万次读取。为了提取过去一年中每天(比如中午)的读取,你必须扫描近 400 万次读取,才能得出 365 或 366 次读取。这可能需要大量的计算资源,并且可能会带来性能问题,特别是对于实时系统(如游泳池监测系统)。我们可以为每次读取分配一个唯一的索引值,这样就只提取我们需要的读取。

要创建 UML 限定符,你需要将一些限定信息(通常是限定类或对象中的属性名称)放置在关联链接的一端的矩形框内,如 图 6-35 所示。

image

图 6-35:一个限定符示例

唯一的限定符要求所有 tempHistoryClass 对象具有唯一的日期和时间;也就是说,不能有两个读取记录具有相同的日期和时间值。

图 6-35 表明系统将维护一个特殊机制,让我们可以基于 date_time 值直接选择一个 tempHistoryClass 对象。这类似于数据库表中的键。^(7)

在此示例中,乘法值均为 1,因为日期和时间都是唯一的,date_time 限定符将选择一个特定的日期,因此只能有一个关联记录。(从技术上讲,可能没有匹配项;然而,图示中并未考虑这种情况,因此必须有匹配对象。)

如果date_time键在历史对象中不是唯一的,那么多重性可能是除1以外的其他值。例如,如果你想生成一份记录了正午所有温度的报告,你可以按如下方式指定,如图 6-36 所示。

image

图 6-36:一个限定符集示例

假设你有一年份的tempHistoryClass对象读数,你将得到 365/366 条不同日期但相同时间(在此示例中为正午)的读数。

需要记住的一点是,你可以有多个关联图来描述同一关联的不同变体。例如,在同一套 UML 文档中看到图 6-34、6-35 和 6-36 是合理的。图 6-34 描述了poolMonitor类或对象与tempHistoryClass对象之间的通用关联。图 6-35 可能描述了一个搜索操作,在这个操作中,你正在查找特定的温度;这个操作可能非常常见,以至于你想生成某种关联数组(即哈希表)来提高其性能。同样,图 6-36 建议你希望拥有另一个快速查找表,以加速收集正午时录得的一组读数。每个图表都有其自己的上下文,它们并不相互冲突。

6.5.5.9 可导航性

在第 109 页的“属性名称”中,我介绍了将属性名称添加到关联链接中的概念。建议将名称放置在包含该属性的类或对象附近(即,在关联链接的另一端引用其他类或对象)。尽管这种隐式指定通信方向和属性所有权的方式对于大多数简单图表非常有效,但随着 UML 图表变得更加复杂,这种方式可能会变得令人困惑。UML 可导航性功能解决了这个问题。

可导航性指定图表中信息流的方向(即数据如何在系统中流动)。默认情况下,关联链接是双向可导航的。这意味着链接一端的类/对象可以访问另一端的数据字段或方法。然而,也可以指定信息只在关联链接的一个方向上流动。

为了指示可导航性,可以在关联链接的末端放置箭头,以指定通信流的方向(你不需要在关联链接的两端都放置箭头来指定双向通信)。例如,在图 6-37 中,通信流从poolMonitor类或对象流向pHClass类或对象。这个方向告诉你两件事:pHSensor属性是poolMonitor类或对象的成员,且pHClass没有属性可以引用poolMonitor内部的任何内容。

image

图 6-37:可导航性

UML 2.x新增了一个符号,用来明确表示某个方向上不发生通信:你可以在禁止通信的一侧的关联链接上放置一个小×(参见图 6-38)。

image

图 6-38:显式非可导航性

我认为这会使图表显得杂乱,阅读起来更困难,所以我坚持使用默认规范。你可以自行决定使用哪种选项。

6.5.5.10 可变性

UML 的可变性特性允许你指定某个数据集在创建后是否可以修改。在图 6-34 中的历史记录示例中,一旦温度值被记录在历史数据库中,你不希望系统或用户编辑或删除该值。你可以通过在关联链接上添加{frozen}约束来实现,如图 6-39 所示。

image

图 6-39:一个{frozen}示例

现在你已经更好地理解了前四种关系类型的特点,让我们转向最后一种类型:继承。

6.5.6 类继承关系

继承关系(在 UML 中也称为泛化关系)是最强的或最紧密耦合的类关系形式。你对基类字段所做的任何更改都会对子类(继承类)或对象产生直接且显著的影响。^(8) 继承是一种与依赖、关联、聚合或组合不同的关系。这些其他关系描述了一个类或对象如何使用另一个类或对象;而继承描述了一个类如何包含另一个类的所有内容。

对于继承关系,我们使用一条线,其中一端带有空心箭头。箭头指向基类(通用项),而线的另一端连接到继承类(派生类),如图 6-40 所示。

image

图 6-40:继承

在这个例子中,spaMonitormainPoolMonitor派生类,继承了基类(祖先类)poolMonitor的所有字段(这些派生类很可能还会添加新的属性和操作)。

继承关系不同于依赖、关联、聚合或组合,因为像多重性、角色和可导航性这样的特征不适用。

6.6 对象

到目前为止,你在所有的图示中看到了两种类型的参与者:角色。具体来说,大多数项目是类。然而,从面向对象编程的角度来看,类只是数据类型,并不是软件可以操作的实际数据项。对象是类的实例——在应用程序中维护状态的实际数据对象。在 UML 中,你使用矩形表示一个对象,就像表示类一样。不同之处在于,你需要指定一个对象名称以及它关联的类名称,并在对象图中将这对名称下划线,如图 6-41 所示。

image

图 6-41:一个对象

6.7 更多信息

Bremer, Michael. The User Manual Manual: How to Research, Write, Test, Edit, and Produce a Software Manual. Grass Valley, CA: UnTechnical Press, 1999。可在 www.untechnicalpress.com/Downloads/UMM%20sample%20doc.pdf 获取样本章节。

Larman, Craig. Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development. 第 3 版. Upper Saddle River, NJ: Prentice Hall, 2004.

Miles, Russ, 和 Kim Hamilton. Learning UML 2.0: A Pragmatic Introduction to UML. Sebastopol, CA: O’Reilly Media, 2003.

Pender, Tom. UML Bible. Indianapolis: Wiley, 2003.

Pilone, Dan, 和 Neil Pitman. UML 2.0 in a Nutshell: A Desktop Quick Reference. 第 2 版. Sebastopol, CA: O’Reilly Media, 2005.

Roff, Jason T. UML: A Beginner’s Guide. Berkeley, CA: McGraw-Hill Education, 2003.

Tutorials Point. “UML 教程。” www.tutorialspoint.com/uml/.

第九章:UML 交互图**

Image

交互图模拟系统中不同对象(参与者)之间发生的操作。UML 中有三种主要类型的交互图:时序图、协作图(通信图)和时序图。本文大部分内容将聚焦于时序图,接下来简要讨论协作图。

7.1 时序图

时序图显示参与者(演员、对象)之间的交互,按照发生的顺序。与活动图描述单一操作的细节不同,时序图将多个操作的顺序关联起来。从设计的角度来看,时序图比活动图更具信息性,因为它们展示了系统的整体架构;然而,在活动图(较低级别)中,系统架构师通常可以放心地假设实现系统的软件工程师能够理解设计所需的活动。

7.1.1 生命周期

在时序图的顶部,你需要绘制参与者集,使用矩形或简笔画(参见图 7-1),然后从每个参与者绘制一条虚线到底部,表示该对象的生命周期。生命周期显示从执行的最早(最上面)到最新(最下面)时间点的流动。然而,仅凭生命周期并不能指示经过的时间量,它仅表示时间的流逝从图表的顶部到底部,等长的线段不必对应相同的时间量——某个地方的 1 厘米段可能是几天,而另一个地方的 1 厘米段可能是微秒。

image

图 7-1: 基本时序图

7.1.2 消息类型

参与者之间的通信以消息(有时我称之为操作)的形式存在,它们由绘制在生命周期之间的箭头组成,甚至可能从一个生命周期指向其自身。

有四种类型的消息箭头可以使用,如图 7-2 所示。

image

图 7-2: 时序图中的消息类型

同步消息是大多数程序使用的典型调用/返回操作(用于执行对象方法、函数和过程)。发送者会暂停执行,直到接收者返回控制。

返回消息表示控制从同步消息返回到消息发送方,但在顺序图中返回消息是完全可选的。在同步消息完成之前,对象无法继续执行,因此在同一时间线上出现的其他消息(无论是接收还是发送)本身就暗示了一个返回操作。由于大量返回箭头可能会混淆顺序图,因此如果图表开始变得混乱,最好省略它们。然而,如果顺序图相对简洁,返回箭头有助于准确显示发生的事情。

异步消息会触发接收方代码的调用,但消息发送方在继续执行之前不需要等待返回消息。因此,在顺序图中,异步调用不需要画出明确的返回箭头。

平面消息可以是同步的,也可以是异步的。当类型对设计无关紧要,并且你希望将选择留给实现代码的工程师时,可以使用平面消息。作为一般规则,平面消息不绘制返回箭头,因为那样会暗示实现者必须使用同步调用。

注意

平面消息仅为 UML 1.x 实体。在 UML 2.0 中,异步消息使用完整的开放箭头。

7.1.3 消息标签

绘制消息时,必须为消息的箭头附加标签。这个标签可以只是消息的描述,如图 7-3 所示。

image

图 7-3:消息标签

消息的顺序通过其垂直位置来表示。在图 7-3 中,“选择即时池清理”标签是图中的第一条消息线,意味着它是第一个执行的操作。向下移动,“开启泵”是第二条消息线,所以它接着执行。“开启泵”的返回是第三个操作,“选择即时池清理”的返回是第四个操作。

7.1.4 消息编号

随着顺序图变得更复杂,仅凭消息位置可能很难确定执行顺序,因此在每个消息标签上附加额外的指示符(如编号)可能会有所帮助。图 7-4 使用了顺序整数,尽管 UML 并不要求如此。你也可以使用像 3.2.4 这样的编号,甚至是非数字指示符(例如 A、B、C)。然而,目标是使得消息顺序易于确定,所以如果在此处过于复杂,可能会适得其反。

image

图 7-4:消息编号

虽然到目前为止你看到的消息标签是相对简单的描述,但也不罕见将实际操作名称、参数和返回值用作消息箭头的标签,如图 7-5 所示。

image

图 7-5:消息参数和返回值

7.1.5 保护条件

你的消息标签也可以包含保护条件:用括号括起来的布尔表达式(参见图 7-6)。如果保护条件的结果为true,系统就会发送消息;如果结果为false,系统则不会发送消息。

image

图 7-6:消息保护条件

在图 7-6 中,只有当pumpPowerontrue)时,pMon对象才会向pump发送pump(100)消息。如果pumpPowerofffalse)并且pump(100)消息没有执行,那么相应的返回操作(序列项 3)也不会执行,控制将转移到pMon生命线中的下一个外发箭头项(序列项 4,返回控制到用户对象)。

7.1.6 迭代

你还可以通过在序列图中提供迭代次数来指定消息执行的次数。要指定迭代,可以使用星号符号(*),后跟保护条件或for循环迭代次数(参见图 7-7)。只要保护条件为true,系统就会重复发送消息。

image

图 7-7:带有迭代的部分序列图

在图 7-7 中,消息执行 100 次,变量i的值从1变到100,并在每次迭代时递增。如果pumpPwrAndDelay函数应用了作为参数指定的百分比功率,并且延迟 1 秒,那么大约 1 分 40 秒后,水泵将以全速运行(每秒增加总速度的 1%)。

7.1.7 长时间延迟和时间约束

序列图通常只描述消息的顺序,而不描述每条消息执行所需的时间。然而,有时设计者可能希望指出某个特定操作相对于其他操作可能需要更长时间。这在一个对象向另一个位于当前系统之外的对象发送消息时尤为常见(例如,当一个软件组件向远程服务器上的某个对象发送消息时),我们稍后会讨论。通过将消息箭头稍微指向下方来表示一个操作需要更长时间。例如,在图 7-8 中,你会预期scheduledClean()操作的执行时间会比典型操作更长。

image

图 7-8:带有时间约束的定时消息

你还必须通过在图表中添加某种约束来指定每个消息的预期时间量。图 7-8 通过一条从scheduledClean()操作开始到系统将控制权交还给计时器模块演员(很可能是泳池监控系统上的物理计时器)的位置的虚线垂直箭头来演示这一点。所需的时间约束显示在虚线箭头旁边的花括号内。

7.1.8 外部对象

有时,序列图中的某个组件必须与系统外部的某个对象进行通信。例如,泳池监控中的一些代码可能会检查盐度水平,如果过低,则向所有者的手机发送短信。实际发送短信的代码可能由物联网(IoT)设备处理,因此超出了泳池监控软件的范围;因此,短信代码是一个外部对象。

你需要为外部对象绘制一个粗边框,并使用实线表示它们的生命线,而不是使用虚线(参见图 7-9)。

image

图 7-9:序列图中的外部对象

在图 7-9 中,计时器模块对salinity对象进行异步调用,并且salinityCheck()操作没有返回值。之后,计时器模块可以执行其他任务(在这个简单的示意图中未显示)。十分钟后,如时间约束所示,salinity对象对计时器模块进行异步调用,并要求其更新显示上的盐度值。

由于sendMsg()操作没有明确的时间约束,因此它可能在salinityCheck()操作之后、updateSalinityDisp()操作之前的任何时刻发生;这可以通过sendMsg()消息箭头在另外两个消息之间的位置来表示。

7.1.9 激活条

激活条表示一个对象已实例化并处于活动状态,它们呈现为横跨生命线的开放矩形(参见图 7-10)。它们是可选的,因为通常你可以仅通过观察发送到和接收到对象的消息来推测对象的生命周期。

image

图 7-10:激活条

注意

大多数情况下,激活条会让序列图显得杂乱,因此本书不会使用它们。它们在这里的描述仅仅是为了防止你在其他来源的序列图中遇到它们。

7.1.10 分支

如“守卫条件”部分所述,见第 131 页,你可以将守卫条件应用于一条消息,这相当于说,“如果true,则执行消息;否则,继续沿这个生命周期。”另一个有用的工具是分支——等同于 C 风格的 switch/case 语句,你可以根据一组守卫条件选择执行的消息,每条消息都有一个守卫条件。为了根据泳池使用氯或溴作为消毒剂来执行不同的消息,你可能会倾向于按图 7-11 所示的方式绘制分支逻辑。

image

图 7-11:不良的分支逻辑实现

从一个角度来看,这个图表是完全合理的。如果这个特定泳池的消毒剂是溴而不是氯,则第一个消息不会执行,控制流程会转到第二个消息,第二个消息会执行。这个图表的问题在于两个消息出现在生命周期的不同点,因此它们可能会在完全不同的时间执行。尤其是当你的序列图变得更复杂时,可能会有其他消息调用插入到这两者之间,从而在getBromine()消息之前执行。相反,如果消毒剂不是,你应该立即检查是否是,并且不允许有任何干扰消息。图 7-12 展示了正确绘制这种逻辑的方法。

image

图 7-12:良好的分支逻辑实现

使用从相同垂直位置开始的箭头尾部和在相同垂直位置结束的箭头头部来绘制分支逻辑,可以避免执行顺序的任何歧义(前提是守卫条件是互斥的——也就是说,不可能同时使两个条件为true)。

分支使用斜向的消息箭头,类似于长时间延迟,但是长延迟项会有相关的时间约束。^(1)

7.1.11 替代流程

分支还有另一个潜在问题:当你需要向同一个目标对象发送两种不同消息中的一种时该怎么办?由于箭头的尾部和头部必须分别在相同的垂直位置开始和结束,这两条箭头会相互重叠,并且无法表明分支的发生。解决这个问题的方法是使用替代流程

在替代流程中,一个单独的生命周期会在某个点分裂成两个独立的生命周期(见图 7-13)。

image

图 7-13:替代流程

在这个示例中,定时器模块必须在获取当前盐度(NaCl)或氢氧化钠(NaOH)之间做出选择。getSalinity()getNaOH()操作是同一类中的方法,因此它们的消息箭头都会指向ClGen生命线的同一个位置。为了避免消息箭头重叠,图 7-13 将ClGen生命线分成了两个生命线:原始生命线和一个备用流程。

在消息调用之后,如果需要,您可以将两个流程重新合并在一起。

7.1.12 对象创建与销毁

到目前为止的示例中,所有对象都在序列图的生命周期内存在;也就是说,所有对象在执行第一条消息(操作)之前就已存在,并且在执行最后一条消息之后仍然存在。在实际设计中,您需要创建和销毁一些在程序执行的整个过程中并不一直存在的对象。

对象的创建与销毁和其他消息一样。UML 中的常用约定是使用特殊消息«create»和«destroy»(参见图 7-14)来显示序列图中的对象生命周期;然而,您也可以使用任何您喜欢的消息名称。在cleanProcess生命线末尾,紧接着«destroy»操作的 X,表示该生命线的结束,因为对象不再存在。

image

图 7-14:对象创建与销毁

该示例使用了一个掉落标题框来表示新创建对象的生命周期开始。如 Russ Miles 和 Kim Hamilton 在《学习 UML 2.0》(O'Reilly, 2003)中指出,许多标准化的 UML 工具不支持使用掉落标题框,导致您只能将对象标题框放置在图表的顶部。对于这个问题,存在一些解决方案,应该适用于大多数标准 UML 工具。

您可以将对象放置在图表的顶部,并添加注释,明确指出对象创建和销毁发生的点(参见图 7-15)。

image

图 7-15:使用注释表示对象生命周期

您还可以使用替代流程来表示对象的生命周期(参见图 7-16)。

image

图 7-16:使用替代流程表示对象生命周期

激活条提供了一个第三种选择,这里可能会更加清晰。

7.1.13 序列片段

UML 2.0 增加了序列片段来表示循环、分支和其他替代流程,使您能够更好地管理序列图。UML 定义了几种标准序列片段类型,您可以使用,这些类型简要定义在表 7-1 中(完整描述稍后会在本节中出现)。

表 7-1: 序列片段类型的简要描述

alt 仅执行true的替代片段(可以类比于if/elseswitch语句)。
assert 表示如果守卫条件为 true,片段中的操作是有效的。
break 退出循环片段(基于某些守卫条件)。
consider 提供序列片段中有效消息的列表。
ignore 提供序列片段中无效消息的列表。
loop 执行多次,守卫条件决定片段是否重复。
neg 永不执行。
opt 仅在关联条件为 true 时执行。与 alt 类似,但只有一个替代片段。
par 并行执行多个片段。
ref 表示调用另一个序列图。
region (也称为 critical)定义一个临界区,在该区域中只能有一个执行线程。
seq 表示在多任务环境中操作必须按特定顺序执行。
strict seq 的严格版本。

通常,你会画出一个包围消息的矩形序列片段,矩形的左上角有一个特殊的五边形符号(一个右下角被裁剪的矩形),其中包含 UML 片段的名称/类型(参见 图 7-17;在此图中,用实际的片段类型替代 typ)。

image

图 7-17:通用序列片段形式

例如,如果你想多次重复一系列消息,可以将这些消息放入一个 loop 序列片段中。这告诉实现程序的工程师根据 loop 片段指定的次数重复这些消息。

你还可以包括一个可选的 附加信息 项,通常是守卫条件或迭代次数。以下小节将详细描述 表 7-1 中的序列片段类型以及它们可能需要的任何附加信息。

7.1.13.1 ref

ref 序列片段有两个组成部分:UML 交互发生和引用本身。一个 交互发生 是与代码中的子例程(过程或函数)对应的独立序列图。它被一个序列片段框包围。框的左上角的五边形矩形包含 sd(表示 序列图),后面是 ref 片段的名称以及你希望分配给它的任何参数(参见 图 7-18)。

image

图 7-18:交互发生示例

最左侧的输入箭头对应于 子例程入口点。如果没有这个,你可以假设控制流将进入其生命线顶部的最左侧参与者。

接下来我们讲解 ref 序列片段的第二个组成部分:在不同的序列图中引用交互发生(参见 图 7-19)。

image

图 7-19:ref 序列片段示例

这对应于代码中的子程序调用(过程或函数)。

7.1.13.2 consider 和 ignore

consider序列片段列出了在序列图的某一部分中有效的所有消息;所有其他消息/操作符都是非法的。ignore操作符列出了在序列图的某一部分中无效的消息名称;所有其他操作符/消息是合法的。

considerignore可以作为操作符与现有的序列片段一起使用,或者作为独立的序列片段使用。considerignore操作符的格式如下:

consider{ comma-separated-list-of-operators }

ignore{ comma-separated-list-of-operators }

considerignore操作符可以出现在交互发生时sd 名称 标题之后(参见图 7-20),在这种情况下,它们适用于整个图表。

image

图 7-20:一个 consider 操作符示例

你也可以在另一个序列图中创建一个序列片段,并用considerignore操作对该片段进行标注。在这种情况下,considerignore仅适用于该特定序列片段中的消息(参见图 7-21)。

image

图 7-21:一个忽略序列片段示例

如果这些片段类型看起来很奇怪,可以考虑创建一个非常通用的ref片段,仅处理某些消息,然后从多个不同的地方引用该ref,这些地方可能会传递未处理的消息以及已处理的消息。通过向ref添加considerignore操作符,你可以让该片段简单地忽略它未明确处理的消息,这样你就可以在不必为系统添加任何额外设计的情况下使用该ref

7.1.13.3 assert

assert序列片段告诉系统实现者,只有在某个保护条件计算结果为true时,里面的消息才是有效的。在assert片段的末尾,通常会提供某种布尔条件(即保护条件),这个条件在序列完成后必须为true(参见图 7-22)。如果assert片段执行完毕后该条件不是true,则设计无法保证结果正确。assert提醒工程师通过例如使用 C++的assert宏调用(或者其他语言中的类似方法,甚至是if语句)来验证该条件确实为true

image

图 7-22:一个 assert 序列片段示例

在 C/C++中,你可能会使用如下代码来实现图 7-22 中的序列:

Object3->msg1();                  // Inside example

Object4->msg2();                  // Inside Object3::msg1

assert( condition == TRUE );      // Inside Object3::msg1
7.1.13.4 循环

loop序列片段表示迭代。你将在与序列片段相关的五边形矩形中放置loop操作符,并且还可以在序列片段的顶部包含一个括起来的保护条件。loop操作符和保护条件的组合控制迭代次数。

这种序列片段的最简单形式是无限循环,由没有任何参数且没有保护条件的loop运算符组成(见图 7-23)。大多数“无限”循环实际上并非真无限,而是在某个条件为真时通过break序列片段终止(我们将在下一节讨论break序列)。

image

图 7-23:一个无限循环

图 7-23 中的循环大致等同于以下的 C/C++代码:

// This loop appears inside Object3::msg1

for(;;)

{

      Object4->msg2();

} // endfor

或者,替代的方式是:

while(1)

{

      Object4->msg2()

} // end while

注意

个人来说,我更喜欢以下这种方式:

define ever ;;

.

.

.

for(ever)

{

Object4->msg2();

} // endfor

我觉得这是最易读的解决方案。当然,如果你是“无论如何都反对宏”的人,你可能会不同意我选择使用无限循环!

确定循环执行固定次数,可以有两种形式。第一种是loop(integer),它是loop(0, integer)的简写;也就是说,它将至少执行零次,最多执行integer次。第二种是loop(minInt, maxInt),表示循环将至少执行minInt次,最多执行maxInt次。如果没有保护条件,最小次数无关紧要;循环将始终执行maxInt次。因此,大多数确定循环使用loop(integer)的形式,其中integer是要执行的迭代次数(参见图 7-24)。

image

图 7-24:一个确定循环

图 7-24 中的循环大致等同于以下的 C/C++代码:

// This code appears inside Object3::msg1

for( i = 1; i<=10; ++i )

{

      Object4->msg2();

} // end for

你也可以使用乘法符号*来表示无限。因此,loop(*)等同于loop(0, *),也等同于loop(换句话说,你得到的是一个无限循环)。

不确定循环会执行不确定次数(对应于编程语言中的whiledo/whilerepeat/until等循环形式)。不确定循环包括一个保护条件作为loop序列片段的一部分,^(2)这意味着loop序列片段会始终执行minInt次循环(如果minInt不存在,则执行零次)。在执行了minInt次之后,循环序列片段将开始测试保护条件,只有当保护条件为true时,才会继续执行循环。循环序列片段最多会执行maxInt次(总共,不包括minInt次)。图 7-25 展示了一个传统的while类型循环,最低执行零次,最高执行无限次,只要保护条件([cond == true])的值为true

image

图 7-25:一个不确定的 while 循环

图 7-25 中的循环大致等同于以下的 C/C++代码:

// This code appears inside Object3::msg1

while( cond == TRUE )

{

      Object4->msg2();

} // end while

你可以通过将minInt值设置为1,将maxInt值设置为*,然后指定布尔表达式来继续循环执行(参见图 7-26)。

image

图 7-26:一个不确定的 do..while 循环

图 7-26 中的循环大致等同于以下 C/C++代码:

// This code appears inside Object3::msg1

do

{

      Object4->msg2();

} while( cond == TRUE );

当然,你可以创建许多其他复杂的循环类型,但我会把这个留给感兴趣的读者作为练习。

7.1.13.5 break

break序列片段由一个带有保护条件的五边形矩形框中的单词break组成。如果保护条件评估为true,则系统执行break序列片段中的序列,然后控制立即退出封闭的序列片段。如果封闭的序列片段是loop,控制会立即执行到loop之后的第一条消息(就像 Swift、C/C++和 Java 等语言中的break语句)。图 7-27 提供了这样一个循环的示例。

image

图 7-27:一个 break 序列片段的示例

图 7-27 中的循环大致等同于以下 C++代码片段:

// This code appears inside Object3::msg1

while( cond == TRUE )

{

     Object4->msg2();

     if( bcnt >= 10 )

     {

          Object4->msg3();

          break;

     } // end if

     Object4->msg4();

} // end while loop

如果最近的break兼容的外层序列是子例程,而不是循环,则break序列片段的行为类似于从子例程操作中返回。

7.1.13.6 opt 和 alt

optalt序列片段允许你通过一个单一的保护条件来控制一组消息的执行——特别是当组成保护条件的组件的值可能在序列执行过程中发生变化时。

opt序列片段就像一个简单的if语句,没有else子句。你附加一个保护条件,系统只有在保护条件评估为true时,才会执行opt片段中包含的序列(参见图 7-28)。

image

图 7-28:opt序列片段的示例

图 7-28 中的示例大致等同于以下 C/C++代码:

// Assumption: Class2 is Object2's data type. Because control

// transfers into the Object2 sequence at the top of its 

// lifeline, example must be a member function of Object2/Class2

void Class2::example( void )

{

      Object3->msg1();

} // end example

--snip--

//    This code appears in Object3::msg1 

if( cond == TRUE )

{

      Object4->msg2();

} // end if

对于更复杂的逻辑,使用alt序列片段,它类似于if/elseswitch/case。要创建alt序列片段,你可以将多个矩形组合在一起,每个矩形都有自己的保护条件和一个可选的else,从而形成多路决策(参见图 7-29)。

image

图 7-29:一个 alt 序列片段

图 7-29 中的交互发生大致等同于以下代码:

// Assumption: Class2 is Object2's data type. Because control

// transfers into the Object2 sequence at the top of its

// lifeline, example must be a member function of Object2/Class2

void Class2::example( void )

{

      Object3->msg1();

} // end example

--snip--

//    This code appears in Object3::msg1 

if( cond1 == TRUE )

{

      Object4->msg2a();

}

else if( cond2 == TRUE )

{

      Object4->msg2b();

}

else if( cond3 == TRUE )

{

      Object3->msg2c();

}

else

{

      Object4->msg2d();

} // end if
7.1.13.7 neg

你使用neg序列片段来封装一段不包含在最终设计中的序列。实际上,使用neg就是注释掉封闭的序列。如果一段序列最终不包含在设计中,为什么还要包括它呢?至少有两个很好的理由:代码生成和未来功能。

尽管大多数情况下,UML 是一种图示语言,旨在帮助在像 Java 或 Swift 这样的编程语言实现之前进行系统设计,但也有一些 UML 工具可以直接将 UML 图转换为代码。在开发过程中,你可能想要包含一些图示来说明某些内容,但这些图示尚未完成(当然还没到生成可执行代码的程度)。在这种情况下,你可以使用neg序列片段来关闭那些尚未准备好投入使用的序列的代码生成。

即使你不打算直接从 UML 图生成代码,你可能也想为未来的功能使用neg。当你将 UML 图交给工程师实现设计时,它们代表着一个合同,表明:“这就是代码应该如何编写。”然而,有时候,你希望图示中显示的是你计划在未来版本中加入的功能,而不是在当前(或第一)版本中。neg序列片段是告诉工程师忽略该部分设计的一种清晰方式。图 7-30 展示了neg序列片段的一个简单示例。

image

图 7-30:neg 序列片段的示例

图 7-30 中的示例大致等同于以下 C/C++代码:

// Assumption: Class2 is Object2's data type. Because control

// transfers into the Object2 sequence at the top of its

// lifeline, example must be a member function of Object2/Class2

void Class2::example( void )

{

      Object3->msg1();

} // end example
7.1.13.8 par

par序列片段,示例见图 7-31,指出被包含的序列^(4)(操作)可以并行执行。

image

图 7-31:par 序列片段的示例

图 7-31 展示了三个操作数:包含{msg2amsg2bmsg2c}的序列,包含{msg3amsg3bmsg3c}的序列,以及包含{msg4amsg4bmsg4c}的序列。par序列片段要求给定序列中的操作必须按出现的顺序执行(例如,msg2a,然后是msg2b,然后是msg2c)。然而,系统可以自由交错不同操作数中的操作,只要它保持这些操作数的内部顺序。所以,在图 7-31 中,顺序{msg2amsg3amsg3bmsg4amsg2bmsg2cmsg4bmsg4cmsg3c}是合法的,{msg4amsg4bmsg4cmsg3amsg3bmsg3cmsg2amsg2bmsg2c}也是合法的,因为包含的序列的顺序匹配。然而,{msg2amsg2cmsg4amsg4bmsg4cmsg3amsg3bmsg3cmsg2b}是不合法的,因为msg2c出现在msg2b之前(这与图 7-31 中指定的顺序相反)。

7.1.13.9 seq

par序列片段强制执行以下限制:

  • 系统保持操作数内操作的顺序。

  • 系统允许来自不同操作数的不同生命线上的操作按任何顺序执行。

seq序列片段又添加了另一层:

  • 不同操作数中相同生命线上的操作必须按它们在图中出现的顺序执行(从上到下)。

例如,在图 7-32 中,Operand1Operand3有发送到同一对象(生命线)的消息。因此,在seq序列片段中,msg2amsg2bmsg2c必须在msg4a之前执行。

image

图 7-32:一个seq序列片段的示例

图 7-32 展示了一个独立的seq序列片段。然而,在典型使用中,seq序列片段会出现在par内,以控制par的部分操作数的执行顺序。

7.1.13.10 strict

strict序列片段强制操作按它们在每个操作数中出现的顺序进行;不允许操作数之间的操作交叉执行。strict序列片段的格式类似于parseq(参见图 7-33)。

image

图 7-33:一个strict序列片段的示例

strict并行操作允许操作数按任何顺序执行,但一旦某个操作数开始执行,所有操作必须按指定的顺序完成,才能开始执行其他操作数。

在图 7-33 中,有六种不同的操作序列可能性:{Operand1Operand2Operand3};{Operand1Operand3Operand2};{Operand2Operand1Operand3};{Operand2Operand3Operand1};{Operand3Operand1Operand2};和{Operand3Operand2Operand1}。

然而,操作数内部的操作不能交叉执行,必须从上到下执行。

7.1.13.11 region

在第 99 页的“扩展 UML 活动图”部分中,我用活动图中的一个自制临界区示例来演示如何扩展 UML 以满足自己的需求。我指出了为什么这是一个不好的主意(可以重新阅读该部分获取详细信息),并提到有另一种方法可以使用标准 UML 实现你想做的事情:region序列片段。UML 活动图不支持临界区,但序列图支持。

region序列片段指定一旦执行进入该区域,同一并行执行上下文中的其他操作不能交叉执行,直到执行完成。region序列片段必须始终出现在其他并行序列片段中(通常是parseq;技术上它可以出现在strict内部,尽管最终这没有实际意义)。

作为一个示例,考虑图 7-34—系统可以自由地交织任何操作数消息的执行,遵循par序列片段给出的规则,但一旦系统进入临界区域(执行msg4a操作),par序列片段中的其他线程将无法执行。

image

图 7-34: 区域序列片段

7.2 协作图

协作图(或通信图)提供与序列图相同的信息,但形式更紧凑一些。与其在生命线之间绘制箭头,在协作图中,我们直接在对象之间绘制消息箭头,并为每条消息附上编号以表示顺序(见图 7-35)。

image

图 7-35: 一个协作图

图 7-35 中的图表大致等同于图 7-9 中的序列图(不考虑 10 分钟的时间限制)。在图 7-35 中,salinityCheck消息首先执行,sendMsg第二执行,updateSalinityDisplay最后执行。

图 7-36 显示了一个更复杂的协作图,它更好地展示了这一选项的紧凑性。这个示例中的六条消息在序列图中需要六条线,而在这里只需要三条通信链接。

image

图 7-36: 更复杂的协作图

注意

同时拥有协作图和序列图可能是 UML 创建过程中合并不同系统的产物。你使用哪一个实际上只是个人偏好的问题。然而,请记住,随着图表的复杂性增加,协作图会变得更加难以理解。

7.3 更多信息

Bremer, Michael. 用户手册手册:如何研究、编写、测试、编辑和制作软件手册. Grass Valley, CA: UnTechnical Press, 1999. 你可以在 www.untechnicalpress.com/Downloads/UMM%20sample%20doc.pdf 下载示例章节。

Larman, Craig. 应用 UML 与模式:面向对象分析与设计及迭代开发导论. 第三版. Upper Saddle River, NJ: Prentice Hall, 2004.

Miles, Russ, 和 Kim Hamilton. 学习 UML 2.0:UML 的实用入门. Sebastopol, CA: O’Reilly Media, 2003.

Pender, Tom. UML 圣经. 印第安纳波利斯: Wiley, 2003.

Pilone, Dan, 和 Neil Pitman. UML 2.0 概述:桌面快速参考. 第二版. Sebastopol, CA: O’Reilly Media, 2005.

Roff, Jason T. UML:初学者指南. 伯克利, CA: McGraw-Hill Education, 2003.

Tutorials Point. “UML 教程。” https://www.tutorialspoint.com/uml/

第十章:杂项 UML 图表**

Image

本章通过描述五种额外的图表,完成了本书关于 UML 的讨论,这些图表对于 UML 文档非常有用:构件图、包图、部署图、复合结构图和状态图。

8.1 构件图

UML 使用构件图来封装可重用的构件,如库和框架。尽管构件通常比类更大,责任也更多,但它们支持与类相同的许多功能,包括:

  • 与其他类和构件的泛化与关联

  • 操作

  • 接口

UML 使用矩形和«component»构件标注来定义构件(参见图 8-1)。一些用户(和 CASE 工具)也使用«subsystem»标注来表示构件。

image

图 8-1:UML 构件

构件使用接口(或协议)来促进封装和松耦合。通过使设计独立于外部对象,接口提高了构件的可用性。构件与系统的其余部分通过两种预定义接口进行通信:提供接口和需求接口。提供的接口是构件提供的,外部代码可以使用它。需求的接口则是外部代码为构件提供的接口,这可能是构件调用的外部函数。

正如你现在从 UML 中所期望的那样,绘制构件的方式不止一种:可以使用构件标注(其中有两种版本)或球窝标注

表示具有接口的 UML 构件最简洁的方式可能是图 8-2 中所示的简单构件标注形式,其中列出了构件内部的接口。

image

图 8-2:构件标注的简单形式

图 8-3 显示了更完整的(尽管较为庞大的)构件标注形式,图中有单独的interface对象。当你想列出接口的单独属性时,这种选项更为适用。

image

图 8-3:更完整的构件标注形式

球窝标注提供了一种替代构件标注的方式,使用圆形图标()表示提供的接口,使用半圆形(插座)表示需要的接口(参见图 8-4)。

image

图 8-4:球窝标注

球窝标注的优点在于,连接的构件在视觉上可以更加美观(参见图 8-5)。

image

图 8-5:连接两个球窝构件

正如你所看到的,component1的所需接口与component2的提供接口在此图中很好地连接在一起。但尽管球窝符号比刻板符号更加紧凑和吸引人,它在超出少数接口时扩展性较差。当你添加更多的提供和所需接口时,刻板符号通常是更好的解决方案。

8.2 包图

UML 包是其他 UML 项(包括其他包)的容器。UML 包相当于文件系统中的子目录、C++和 C#中的命名空间,或者 Java 和 Swift 中的包。要在 UML 中定义一个包,可以使用带有包名的文件夹图标(参见图 8-6)。

image

图 8-6:UML 包

以池监控应用为一个更具体的例子,可能有一个有用的包sensors,其中包含与 pH 和盐度传感器相关的类/对象。图 8-7 展示了该包在 UML 中的样子。phSensorssaltSensor对象前面的+前缀表示这些是可以在包外访问的公共对象。^(1)

image

图 8-7:传感器包

要引用包外的(公共)对象,可以使用packageName::objectName的形式。例如,在sensors包之外,你可以使用sensors::pHSensorsensors::saltSensor来访问内部对象。如果一个包嵌套在另一个包中,你可以使用像outsidePackage::internalPackage::object这样的顺序来访问最内层包中的对象。例如,假设你有两个核电站通道,分别名为 NP 和 NPP(来自第四章中的用例示例)。你可以创建一个名为instruments的包来包含这两个包NPNPPNPNPP包可以包含与 NP 和 NPP 仪器直接相关的对象(参见图 8-8)。

image

图 8-8:嵌套包

请注意,NPNPP包都包含名为calibrate()pctPwr()的函数。在这些包之外调用这些函数时不会产生歧义,因为你必须使用限定名称来访问这些函数。例如,在instruments包外,你需要使用类似instruments::NP::calibrateinstruments::NPP::calibrate这样的名称,以避免混淆。

8.3 部署图

部署图呈现了系统的物理视图。物理对象包括 PC、外设如打印机和扫描仪、服务器、插入式接口板和显示器。

为了表示物理对象,UML 使用节点,即一个 3D 盒子图像。在盒子内部放置刻板印象«device»和节点名称。图 8-9 提供了一个来自 DAQ 数据采集系统的简单示例。它显示了一台主机 PC 与 DAQ_IF 和 Plantation Productions 的 PPDIO96 96 通道数字 I/O 板连接。

image

图 8-9:一个部署图

这个图中缺少的一个内容是系统中实际安装的软件。在这个系统中,可能至少有两个应用程序在运行:一个在主机 PC 上运行,负责与 DAQ_IF 模块通信(我们称之为daqtest.exe),另一个是运行在 DAQ_IF 板上的固件程序(frmwr.hex),它可能才是部署图所描述的真实软件系统。图 8-10 展示了一个扩展版,图中小图标标示了安装在各个机器上的软件。部署图使用刻板印象«artifact»来表示二进制机器代码。

image

图 8-10:一个扩展的部署图

请注意,PPDIO96 板是由 DAQ_IF 板直接控制的:PPDIO96 板上没有 CPU,因此也没有安装任何软件。

部署图其实有更多内容,但本书中需要的内容已经足够。如果你有兴趣,可以查看 “更多信息” 在第 165 页,其中有更详细的部署图参考资料。

8.4 复合结构图

在某些情况下,类图和顺序图无法准确描述一些类中组件之间的关系和动作。请参阅图 8-11,它说明了 PPDIO96 的一个类。

image

图 8-11:PPDIO96 类组成

这个类组成图告诉我们,PPDIO96类包含(由)两个子类:portInitializationwritePort。它没有告诉我们的是这两个PPDIO96的子类如何相互作用。例如,当你通过portInitialization类初始化端口时,可能portInitialization类还会调用writePort中的一个方法,通过该方法将端口初始化为某个默认值(例如0)。裸类图并没有展示这一点,也不应该展示。让portIntialization通过writePort调用写入默认值,可能只是PPDIO96内部可能出现的众多不同操作之一。任何试图展示PPDIO96内部允许和可能的通讯方式,都会产生一个非常混乱、难以理解的图。

复合结构图提供了解决方案,它只关注那些感兴趣的通信链接(可能只有一个通信链接,或者几个,但通常不会太多,以免图表变得难以理解)。

图 8-12 展示了一个初步(但有问题)的复合结构图。

image

图 8-12:尝试的复合结构图

该图的问题在于,它没有明确指出portInitialization与哪个writePort对象进行通信。请记住,类只是通用的类型,而实际的通信发生在显式实例化的对象之间。在实际系统中,图 8-12 的意图可能更好地通过图 8-13 来传达。

image

图 8-13:实例化的复合结构图

然而,图 8-12 和图 8-13 都没有暗示port``InitializationwritePort实例化的对象特定属于PPDIO96对象。例如,如果有两组PPDIO96portInitializationwritePort对象,那么图 8-14 中的拓扑结构在图 8-12 中的类图基础上是完全有效的。

image

图 8-14:奇怪但合法的通信链接

在这个例子中,i1(属于对象d1)调用w2(属于对象d2)将数字值写入其端口;i2(属于d2)调用w1将其初始值写入其端口。这可能不是原始设计者的初衷,尽管图 8-12 中的通用复合结构图在技术上允许这样做。尽管任何合理的程序员都会立即意识到i1应该调用w1,而i2应该调用w2,但是复合结构图并没有明确这一点。显然,我们希望尽可能消除设计中的歧义。

为了纠正这个不足,UML 2.0 提供了(真实的)复合结构图,它将成员属性直接包含在封装类图中,如图 8-15 所示。

image

图 8-15:复合结构图

该图清楚地表明,PPDIO96的实例化对象将约束portInitializationwritePort类之间的通信,只能与该实例相关联的对象进行通信。

portInitializationwritePort两侧的小方块是端口。这个术语与writePort对象或 PPDIO96 上的硬件端口无关;这是 UML 中的一个概念,表示 UML 中两个对象之间的交互点。端口可以出现在复合结构图和组件图中(见“组件图”在第 155 页)中,用于指定与对象的所需或提供接口。在图 8-15 中,portInitialization一侧的端口(可能)是所需接口,而连接的writePort一侧的端口(可能)是提供接口。

注意

在连接的两侧,一个端口通常是所需接口,另一个是提供接口。

在图 8-15 中,端口是匿名的。然而,在许多图表中(特别是在列出系统接口的情况下),你可以为端口添加名称(见图 8-16)。

image

图 8-16:命名的端口

你还可以使用球和插座符号表示通信链路的哪一侧是提供者,哪一侧有所需接口(记住,插座侧表示所需接口;球侧表示提供接口)。如果需要,你甚至可以为通信链路命名(见图 8-17)。典型的通信链路形式是 name:type,其中 name 是唯一的名称(在组件内),type 是通信链路的类型。

image

图 8-17:指示提供和需要的接口

8.5 状态图

UML 状态图(或状态机)图与活动图非常相似,因为它们显示了系统中的控制流。主要区别在于,状态图仅显示系统可能的各种状态以及系统如何从一个状态过渡到另一个状态。

状态图不引入任何新的图表符号;它们使用来自活动图的现有元素——特别是起始状态、结束状态、状态过渡、状态符号和(可选的)决策符号,如图 8-18 所示。

image

图 8-18:状态图的元素

给定的状态图将有一个起始 状态符号;这是活动开始的地方。状态图中的状态符号始终有一个关联的状态名称(显然,这表示当前状态)。状态图可以有多个结束状态符号,这是一个特殊状态,标志着活动的结束(进入任何结束状态符号会停止状态机)。过渡箭头表示状态机中状态之间的流动(见图 8-19)。

image

图 8-19:一个简单的状态图

转换通常是对系统中的某些外部事件或触发器的响应。触发器是引起系统从一个状态转换到另一个状态的刺激。您可以像在图 8-19 中所示那样,将守卫条件附加到转换上,以指示导致转换发生的触发器。

转换箭头有一个箭头头和箭头尾。当在状态图中发生活动时,转换总是从附加在箭头尾部的状态发生,指向箭头头部指示的状态。

如果您处于某个特定状态,并且发生了一个没有对应离开转换的事件,则状态机会忽略该事件。^(2) 例如,在图 8-19 中,如果您已经处于“系统处于活动状态”状态,并且发生了on button事件,系统将保持在“系统处于活动状态”状态。

如果一个状态的两个转换具有相同的守卫条件,则该状态机是非确定性的。这意味着转换箭头的选择是任意的(可以随机选择)。在 UML 状态图中,非确定性是一个不好的特性,因为它会引入歧义。在创建 UML 状态图时,您应该始终努力保持其确定性,确保所有转换都具有相互排斥的守卫条件。从理论上讲,对于每一个可能发生的事件,您应该有一个确切的离开转换;然而,大多数系统设计师假设,如前所述,如果发生一个没有离开转换的事件,那么该状态会忽略该事件。

从一个状态到另一个状态可以没有附加守卫条件的转换;这意味着系统可以从第一个状态(在转换尾部)任意移动到第二个状态(在箭头头部)。当您在状态机中使用决策符号时,这很有用(请参见图 8-20)。在状态图中,决策符号并非必须存在——就像在活动图中,您也可以直接从一个状态发出多个转换(比如图 8-20 中的“系统处于活动状态”状态)——但是,使用决策符号有时可以让您的图表更加简洁。

image

图 8-20:状态图中的决策符号

8.6 更多 UML

一如既往,这只是对 UML 的简要介绍。本书不会使用更多的图表和其他功能,比如对象约束语言(OCL),因此本章没有讨论它们。然而,如果您有兴趣使用 UML 来记录您的软件项目,您应该花更多的时间学习它。请参见下一节的推荐阅读。

8.7 更多信息

Bremer, Michael. 用户手册手册:如何研究、编写、测试、编辑并制作软件手册. Grass Valley, CA: UnTechnical Press, 1999。可以在 www.untechnicalpress.com/Downloads/UMM%20sample%20doc.pdf 获取示例章节。

Larman, Craig. 应用 UML 与模式:面向对象分析与设计及迭代开发导论. 第 3 版. Upper Saddle River, NJ: Prentice Hall, 2004.

Miles, Russ, 和 Kim Hamilton. 学习 UML 2.0:UML 实用入门. Sebastopol, CA: O’Reilly Media, 2003.

Pender, Tom. UML 圣经. 印第安纳波利斯: Wiley, 2003.

Pilone, Dan, 和 Neil Pitman. UML 2.0 精要:桌面快速参考手册. 第 2 版. Sebastopol, CA: O’Reilly Media, 2005.

Roff, Jason T. UML:初学者指南. Berkeley, CA: McGraw-Hill Education, 2003.

Tutorials Point. “UML 教程.” www.tutorialspoint.com/uml/.

第三部分

文档

第十一章:系统文档**

Image

系统文档指定系统需求、设计、测试用例和测试过程。在大型软件系统中,系统文档通常是最昂贵的部分;例如,瀑布式软件开发模型往往生成比代码更多的文档。此外,通常你必须手动维护系统文档,因此,如果你在某个文档中更改了描述(如需求),你需要搜索整个系统文档并更新每个引用该描述的文档,以确保一致性。这是一个困难且成本高昂的过程。

在本章中,我们将探讨常见的系统文档类型,如何在文档中强制执行一致性,以及减少与开发相关的一些成本的文档策略。

注意

本章讨论的是 系统 文档,而非 用户 文档。如需详细了解用户文档,请参阅 第 184 页 的“更多信息”。

9.1 系统文档类型

传统的软件工程通常使用以下类型的系统文档:

系统需求规格说明书 (SyRS)

SyRS(参见 “系统需求规格说明书” 在 第 193 页)是系统级别的需求文档。除了软件需求外,它可能还包括硬件、业务、程序、手册和其他非软件相关的需求。SyRS 是面向客户/管理层/利益相关者的文档,避免详细描述,呈现需求的“宏观视图”。

软件需求规格说明书 (SRS)

SRS(参见 “软件需求规格说明书” 在 第 194 页)从 SyRS 中提取软件需求^(1),并深入探讨高层需求,介绍更详细的新需求(适合软件工程师)。

注意

SyRS 和 SRS 是 需求 文档,其内容在范围和细节上可能有所不同。许多组织会生成一个文档,而不是两个单独的文档,但本书将它们分别讨论,因为 SyRS 涉及的需求范围更广(例如,硬件和业务需求),而 SRS 主要集中在软件需求上。

软件设计说明书 (SDD)

SDD(参见 第十一章)讨论系统将如何构建(与 SyRS 和 SRS 不同,它们描述的是系统将做什么)。理论上,任何程序员都应该能够使用 SDD,并编写相应的代码来实现软件系统。

软件测试用例 (STC)

STC(见 “软件测试用例文档” 在 第 274 页)描述了验证系统是否包含所有需求,并且在需求列表之外正确功能所需的各种测试值。

软件测试过程(STP)文档

STP(见 “软件测试过程文档” 在 第 288 页)描述了高效执行软件测试用例(来自 STC)以验证系统正确操作的程序。

需求(或反向)追溯矩阵(RTM)文档

RTM(见 “需求/反向追溯矩阵” 在 第 178 页)将需求与设计、测试用例和代码联系起来。使用 RTM,利益相关者可以验证需求是否在设计和代码中实现,并且测试用例和程序是否正确检查了该需求的实现。

注意

某些组织也可能有一个 功能需求规格说明 文档;这通常指的是外部客户提供的需求,或者它也可以仅仅是 SRS 或 SyRS 的同义词。本书后续将不再使用此术语。

还有许多其他类型的文档,但这些是任何(至少非 XP)项目中你所期望的基本文档,它们对应于瀑布模型的不同阶段(见 “瀑布模型” 在 第 44 页),如图 9-1 所示。

image

图 9-1:系统文档依赖关系

如你所见,SRS 是从 SyRS 构建的,SDD 是从 SRS 构建的,STC 也是如此(在某些情况下,它也受到 SDD 的影响,如灰色箭头所示^(2))。STP 是从 STC 构建的。

9.2 追溯性

系统文档中可能最大的后勤问题就是一致性。一个需求通常会生成一些设计项和测试用例(这些测试用例是 STP 中的一部分)。这是在严格的瀑布模型下的直观和自然的进展——首先编写 SRS,然后是 SDD、STC 和 SDD。然而,当你需要对这个链中的早期文档进行更改时,问题就出现了。例如,当你更改一个需求时,可能需要更改 SDD、STC 和 STP 文档中的条目。因此,最佳实践是使用追溯性,它允许你轻松地将一个文档中的项追溯到其他所有系统文档。如果你能将需求追溯到设计元素、测试用例和测试过程,你就可以在修改需求时快速定位和更改这些元素。

反向可追溯性允许你将测试程序追溯到相应的测试用例,并将测试用例和设计项目追溯到相应的需求。例如,你可能会遇到需要修改测试程序的测试问题,这时可以找到相应的测试用例和需求,以确保你对测试程序的修改能够涵盖所有问题。通过这种方式,反向可追溯性还帮助你判断是否需要修改测试用例或需求。

9.2.1 将可追溯性融入文档的方式

有几种方法可以实现可追溯性和反向可追溯性。一种方法是将可追溯性内建于与需求、设计元素、测试用例或测试程序文档相关联的标识符标签中。该标签可以是段落(或项目)编号、描述性词语或其他能够唯一标识引用文本的符号集合。使用标签的软件文档通过直接引用其他文档来避免浪费空间。

作者们常常使用段落编号作为标签,在文字处理系统中这样做非常简单。然而,许多文字处理软件并不支持跨多个文档类型的交叉引用。而且,你想要使用的标签机制或格式可能与文字处理软件提供的格式不匹配。

尽管可以编写自定义软件,或使用数据库应用程序来提取和维护交叉引用信息,但最常见的解决方案是手动维护标签。这听起来可能需要相当大的努力,但只需稍加规划,实际上并不难。

也许最好的解决方案是创建 RTM(参见“需求/反向可追溯性矩阵” 在第 178 页),它跟踪系统文档中各个项目之间的关联。尽管 RTM 是你需要维护的又一份文档,但它提供了一种完整且易于使用的机制,用于跟踪系统中所有组件。

我们将首先讨论常见的标签格式,然后再探讨如何构建 RTM。

9.2.2 标签格式

标签语法没有特定的标准;标签可以采用任何你喜欢的形式,只要语法一致且每个标签唯一。为了自己的需要(以及为了这本书),我创建了一种语法,将可追溯性元素直接融入标签中。以下标签格式按文档类型进行组织。

9.2.2.1 SyRS 标签

对于 SyRS,标签的格式为[productIDSYRSxxx],其中:

productID 指代产品或项目。例如,对于游泳池监控应用,productID 可能是“POOL”。你不想使用过长的 ID(最多四到五个字符),因为它将频繁输入。

SYRS 表明这是来自 SyRS 文档的标签(这可能是一个系统需求标签)。

xxx 表示一个或多个数字,如果使用多个整数,则用句点分隔。这个数字序列在 SyRS 中唯一标识该标签。

在理想情况下,所有 SyRS 需求(以及其他需要标签的项目)应从 1 开始按顺序编号,且数字与它们所指代的文本块的含义无关。

在 SyRS 文档中请考虑以下两个需求:

[POOL_SYRS_001]: 泳池温度监控

系统应监控泳池温度。

[POOL_SYRS_002]: 最高泳池温度

如果泳池温度超过 86 华氏度,系统应开启“高温”LED 指示灯。

假设在[POOL_SYRS_002]之后有 150 个额外需求。

现在假设有人建议在泳池温度降到 70 华氏度以下时启动泳池加热器。你可以添加以下需求:

[POOL_SYRS_153]: 最低泳池温度

如果泳池温度降到 70 华氏度以下,系统应开启泳池加热器。

[POOL_SYRS_154]: 最高加热器启动温度

如果泳池温度超过 70 华氏度,系统应关闭泳池加热器。

在 SyRS 中,将相关需求靠近安排是有意义的,这样读者就能在文档中一个地方找到与某个功能相关的所有需求。你可以理解为什么不应该按标签排序需求——这样做会将泳池加热器的新需求推到文档的末尾,远离其他泳池温度需求。

没有什么可以阻止你将需求移到一起;然而,看到这样的需求集有点令人困惑:

[POOL_SYRS_001]: 泳池温度监控

系统应监控泳池温度。

[POOL_SYRS_153]: 最低泳池温度

如果泳池温度降到 70 华氏度以下,系统应开启泳池加热器。

[POOL_SYRS_154]: 最高加热器启动温度

如果泳池温度超过 70 华氏度,系统应关闭泳池加热器。

[POOL_SYRS_002]: 最高泳池温度

如果泳池温度超过 86 华氏度,系统应开启“高温”LED 指示灯。

一个更好的解决方案是使用点序列重新编号标签,以扩展标签编号。点序列由两个或更多整数组成,整数之间用点分隔。例如:

[POOL_SYRS_001]: 泳池温度监控

系统应监控泳池温度。

[POOL_SYRS_001.1]: 最低泳池温度

如果泳池温度降到 70 华氏度以下,系统应开启泳池加热器。

[POOL_SYRS_001.2]: 最高加热器启动温度

如果泳池温度超过 70 华氏度,系统应关闭泳池加热器。

[POOL_SYRS_002]: 最高泳池温度

如果游泳池温度超过 86 华氏度,系统应打开“高温”LED 指示灯。

这样可以让你在任何地方插入新的需求或变更。请注意,001.1 和 001.10 是不同的。这些数字不是浮动点数字值;它们是两个用句点分隔的整数。数字 001.10 可能是序列 001.1 到 001.10 中的第 10 个值。同样,001 与 001.0 也不同。

如果你需要在 001.1 和 001.2 之间插入一个需求,你可以简单地在序列的末尾添加一个句点,如 001.1.1。你也可以在标签号之间留空,以便将来插入更多标签,例如:

[POOL_SYRS_010]: 游泳池温度监控

系统应监控游泳池温度。

[POOL_SYRS_020]: 最大游泳池温度

如果游泳池温度超过 86 华氏度,系统应打开“高温”LED 指示灯。

所以当你决定添加另外两个要求时,你会有:

[POOL_SYRS_010]: 游泳池温度监控

系统应监控游泳池温度。

[POOL_SYRS_013]: 最低游泳池温度

如果游泳池温度低于 70 华氏度,系统应打开游泳池加热器。

[POOL_SYRS_017]: 最大加热器开启温度

如果游泳池温度超过 70 华氏度,系统应关闭游泳池加热器。

[POOL_SYRS_020]: 最大游泳池温度

如果游泳池温度超过 86 华氏度,系统应打开“高温”LED 指示灯。

请记住,确保所有标签唯一是很重要的。

注意

到目前为止,在本节中,标签一直是段落标题的一部分,这在需要在文档中搜索标签时非常有用(尤其是当文档不是电子版时)。不过,你也可以将标签放置在段落中。

9.2.2.2 SRS 标签

对于仅包含 SRS 而不是 SyRS 作为需求文档的系统文档集,“SRS”可以简单地替换标签中的“SYRS”:[POOL_SRS_010]: 游泳池温度监控。

然而,当一个项目的文档集同时包含 SyRS 和 SRS 时,本书使用一种约定,将 SRS 标签中的反向追溯性直接构建到 SyRS 中。这样的 SRS 标签格式为[productIDSRSxxx_yyy]。

productID与 SyRS 标签相同:SRS 表示软件需求规格标签(与系统需求规格标签相对),xxxyyy是十进制数字,其中xxx是对应的 SyRS 标签的编号(参见“SyRS 标签”在第 172 页)。

包含父 SyRS 需求的标签编号将反向可追溯性信息直接嵌入其标签中。因为几乎所有 SRS 需求都是从相应的 SyRS 标签派生的,并且 SyRS 需求和 SRS 需求之间是一个一对多的关系,一个 SyRS 需求可以生成一个或多个 SRS 需求,但每个 SRS 需求只能追溯到一个 SyRS 需求,如图 9-2 所示。

image

图 9-2:SyRS 到 SRS 的关系

yyy组件是 SRS 标签值。作为一般规则(也是本书遵循的约定),yyy不必在所有 SRS 标签中是唯一的,但xxx_yyy的组合必须是唯一的。以下是所有有效(且唯一)的 SRS 标签:

[POOL_SRS_020_001]

[POOL_SRS_020_001.5]

[POOL_SRS_020_002]

[POOL_SRS_030.1_005]

[POOL_SRS_031_003]

本书还采用了在每个xxx值中重新开始yyy编号的约定。

通过这种方式构造 SRS 标签,你可以将从 SRS 到 SyRS 的自动反向可追溯性直接嵌入标签标识符中。要查找与 SRS 需求相关联的 SyRS 需求,只需提取xxx值并在 SyRS 文档中搜索相应的标签。也可以轻松地查找与 SyRS 标签相关联的 SRS 标签。例如,要找到所有与 POOL_SYRS_030 相关联的 SRS 需求,只需在 SRS 文档中搜索所有“SRS_030”的实例。

有可能 SRS 文档会产生一些全新的需求,这些需求并非基于某个特定的 SyRS 需求。如果是这样,则不会有xxx编号作为 SRS 标签的一部分。本书保留了 SyRS 标签编号 000(即,永远不会有 SyRS 标签[productID_SYRS_000]),任何不基于 SyRS 需求的全新 SRS 需求将采用[productIDSRS_000yyy]的形式。

注意

本书使用的另一个约定是用星号()代替000值。*

将所有与软件相关的 SyRS 需求直接包含在 SRS 中是一个好主意。^(3) 这样,SRS 就能作为一个独立的文档供软件开发人员使用。当将 SyRS 需求直接复制到 SRS 时,我们将使用语法[productIDSRSxxx_000]来表示复制的需求标签。也就是说,yyy值为 000 表示这是一个复制的标签。

9.2.2.3 SDD 标签

不幸的是,SRS 需求和 SDD 设计元素之间并不是一对多的关系。^(4) 这使得从 SDD 标签到相应 SRS 标签的反向可追溯性更难通过 SDD 标签语法构建。你必须依赖外部的 RTM 文档来提供 SRS 和 SDD 文档之间的链接。

鉴于在 SDD 标签中反向追踪性并不实用,本书使用简化的 SDD 标签格式[productIDSDDddd],其中 productID 具有通常的含义,而 ddd 是类似于 SyRS 标签中 xxx 的唯一标识符。

9.2.2.4 STC 标签

SRS 要求与 STC 测试用例之间应具有一对多的关系,如图 9-3 所示。

image

图 9-3:SRS 到 STC 标签的关系

这意味着你可以将 STC 到 SRS 的反向追踪性编码到标签中,就像你之前从 SRS 到 SyRS 那样。对于 STC 标签,本书使用语法[productIDSTCxxx_yyy_zzz]。如果所有的 yyy 值都是唯一的(而不是 xxx_yyy 的组合唯一),你可以去掉标签中的 xxx,但保留 xxxyyy 组合有助于同时提供对 SRS 和 SyRS 的反向追踪性,这在某些情况下很方便(尽管会增加输入 STC 标签的工作量)。

尽管这种情况很少发生,但确实有可能创建一个不基于任何 SRS 要求的唯一测试用例。^(5) 例如,使用 SDD 实现代码的软件工程师可能会根据他们编写的源代码创建测试用例。在这种情况下,本书使用之前为那些不基于 SyRS 要求的 SRS 要求所示的方案:我们将 xxxyyy 的值保留为 000_000 或 **,任何不基于要求标签的新 STC 标签将使用 000 作为标签编号后缀。一个 xxx_000 组件意味着该测试用例是基于 SyRS 要求,但不是任何基础的 SRS 要求(或者可能是基于通过前述语法从 SyRS 复制过来的 SRS 标签);这不是一个独立的测试用例。

数值形式为 000_000 的 STC 标签不包含任何追踪性信息。在这种情况下,你需要明确提供链接信息以描述测试用例的来源。以下是一些建议:

  • 在标签后使用 :source 来描述测试用例的来源(其中 source 是包含生成测试用例信息的文件或其他文档的名称)。

  • 使用 RTM 提供源信息(请参见下一节,“要求/反向追踪性矩阵”获取更多详细信息)。

  • 确保包含测试用例来源的文档中有注释或其他链接,明确指定 STC 标签。

9.2.2.5 STP 标签

STC 测试用例与 STP 测试程序之间具有多对一的关系,如图 9-4 所示。

image

图 9-4:STC 到 STP 标签的关系

这意味着,和 SDD 一样,你不能将反向追踪性信息编码到 STP 标签中。因此,对于 STP 标签,本书使用语法[productIDSTPppp],其中 productID 具有通常的含义,ppp 是唯一的 STP 标签值。

9.2.3 要求/反向追踪性矩阵

如前所述,无法将反向可追溯性构建到 SDD 和 STP 标签中,因此你需要使用需求/反向可追溯性矩阵(RTM)。

如其名称所示,RTM 是一个二维矩阵或表格,其中:

  • 每一行指定了需求、设计项、测试用例或测试过程之间的关联。

  • 每一列指定了一个特定的文档(SyRS、SRS、SDD、STC 或 STP)。

  • 每个单元格包含与之关联的文档类型的标签。

表格中的一典型行可能包含如下条目:

image

一般来说,SyRS 或 SRS 需求标签是 RTM 的驱动因素,你通常会通过这些列对表格进行排序。

由于 SyRS 需求和 SRS 需求之间存在一对多的关系,你可能需要在多个行中复制 SyRS 需求,如以下示例所示:

image

第 1、2 和 3 行共享相同的 SyRS 标签,但有不同的 SRS 标签;第 3 和 4 行共享相同的 SRS 标签(以及 SyRS 标签),但有不同的 STC 标签。

有时,当 SyRS 和 SRS 标签可以从前面的行推断出来时,省略重复的标签会更简洁,如下所示:

image

虽然你可以使用文字处理器(例如 Microsoft Word 或 Apple Pages)创建 RTM,但一个更好的解决方案是使用电子表格程序(例如 Microsoft Excel 或 Apple Numbers)或数据库应用程序,它们可以让你根据当前需求轻松地对表格中的行进行排序。本书假设你正在使用电子表格程序。

9.2.3.1 添加额外的列

至少,你会希望 RTM 中为每种系统文档类型设置一列——SyRS(如果有的话)、SRS、SDD、STC 和 STP——但你可能还希望在 RTM 中包含其他信息。例如,你可以考虑增加一个“描述”列,帮助理解所有标签的含义。

或者,如果你有一个 SyRS 文档,“分配”列可能会有用,用来指定一个 SyRS 条目是硬件、软件还是其他。请注意,SRS、SDD、STP 和 STC(按定义)始终与软件相关,因此这些标签的分配项将是“无”(不适用)或始终为“软件”。

另一个有用的列可能是“验证”,它描述了如何在系统中测试(或验证)特定需求。验证类型的示例可能是测试(作为软件测试过程的一部分)、审查、检查、设计、分析、其他或无法测试。

另一个选项是添加一个额外的列(或多列),包含一些行号,你可以用它们快速地按不同方式对数据进行排序。例如,你可能会添加一个从 1 到n(其中n是行数)编号的列,排序后按需求(SyRS 和 SRS)列出行;另一个从 1 到n编号的列,可以按 SDD 标签值排序行,依此类推。

9.2.3.2 排序 RTM

当然,如果你填写了矩阵中的每一个单元格,你可以通过列值(或多个列值)进行排序。例如,假设你正在使用 Microsoft Excel,且列按如下方式组织:

A: 描述

B: SyRS 标签

C: 分配

D: SRS 标签

E: 测试方法

F: SDD 标签

G: STC 标签

H: STP 标签

按照列 B 排序,然后是列 D,再然后是列 G,将按需求顺序对文档进行排序。按照列 F 排序,然后是列 B,再然后是列 D,将按设计元素顺序对文档进行排序。按照列 H 排序,然后是列 D,再然后是列 G,将按测试程序顺序对文档进行排序。

要使用 RTM 从 SyRS 或 SRS 需求追溯到 SRS 需求、SDD 设计项、STC 测试用例或 STP 测试程序,只需按照需求顺序对矩阵进行排序,找到你感兴趣的 SyRS 或 SRS 标签,然后从同一行中挑选出其他文档的对应标签。你可以使用这个方案从 STC 标签追溯到相应的测试程序(因为需求排序也会排序测试用例标签)。

从 STC 到 SRS 再到 SyRS 的反向可追溯性是标签语法的固有特性,因此进行此操作不需要特别的处理。从 SDD 到 SRS(或 SyRS)以及从 STP 到 STC/SRS/SyRS 的反向可追溯性则稍微复杂一些。首先,按 SDD 标签顺序或 STP 标签顺序对矩阵进行排序。这将给你一个所有 SDD 或 STP 标签按字典顺序排列的列表。现在,包含特定 SDD 或 STP 标签的行上的所有标签就是你需要关注的标签。以下示例显示了按测试程序排序的前述 RTM 示例:

image

在这个表格中,你可以很容易地看到测试程序 005 与 SyRS 标签 020 和 SRS 标签 020_001 以及 020_002 相关联。在这个简单的例子中,你不需要排序数据就能确定这些链接。但是,如果是更复杂的 RTM(包含数十、数百甚至上千个需求),如果表格没有按照 STP 标签排序,那么手动查找这些反向链接将会是相当繁琐的工作。

9.3 验证、确认与评审

验证(参见“迭代模型”在第 46 页)是证明产品满足最终用户需求的过程(即,“我们是否在构建正确的产品?”),而验证是确保你已按照项目规范构建它的过程(即,“我们是否在正确地构建产品?”)。验证通常在需求阶段结束时和整个开发周期结束时进行(参见“通过验证降低成本”在第 182 页)。而验证通常发生在软件开发过程的每个阶段结束时,以确保该阶段符合所有输入需求。例如,SDD 的验证将包括确保它覆盖了 SRS 文档中的所有需求(SRS 需求是 SDD 阶段的输入)。

每个阶段的验证步骤如下:

SyRS/SRS 确保文档中的需求充分覆盖了客户提供的所有需求——可能来自 UML 用例(参见“UML 用例模型”在第 74 页)或客户的功能规格。

SDD 确保设计覆盖了所有需求。输入是来自 SRS 的需求。

STC 确保每个(可测试的)需求都有至少一个测试用例。输入是来自 SRS 的需求。

STP 确保所有测试用例都被测试过程涵盖。输入是来自 STC 的测试用例(间接地,也是基于测试用例的需求)。

为了验证每个前置阶段,你需要审查由该阶段生成的文档。在这些审查中,RTM 将非常有用。例如,在审查 SDD 时,你会在 SRS 中查找每个需求,找到相应的 SDD 标签,然后验证设计元素是否实现了指定的需求。你将使用相同的过程验证 STC 文档是否通过测试用例覆盖了所有需求。

在审查代码时,最安全的方法是逐一检查每个阶段的所有输入(即 SDD 和 STC 的需求,以及 STP 的测试用例),并在验证正确处理后将每个输入标记为已完成。这个最终清单将成为该阶段审查文档的一部分。

在审查过程中,你还应确认各阶段输出的正确性。例如,在审查 SRS 时,你应该检查每个需求,确保其有用(参见《软件需求规格说明书》第 194 页);在审查 SDD 时,应该确保每个设计项是正确的(例如,使用了合适的算法并恰当处理了并发操作);在审查 STC 文档时,应该确保每个测试用例能够正确测试相关需求;在审查 STP 时,应该验证每个测试程序能够正确测试其关联的测试用例。

如果可能的话,为了获得最佳效果,最好由文档作者以外的工程师进行最终的正式审查,或者由另一位工程师参与审查过程。文档作者由于与项目的某一部分过于接近,可能会忽略遗漏的内容,在审查时他们可能会在脑海中补充缺失的元素。因此,文档作者应该在提交正式审查之前,先对文档进行自我审查。

9.4 使用文档减少开发成本

文档成本通常是项目整体成本的一个主要组成部分。部分原因是文档数量过多。但另一个原因是这些文档相互依赖,这使得它们难以更新和维护。在《Code Complete》(微软出版社,2004)一书中,Steve McConnell 报告称,与需求阶段相比,在设计(架构)阶段修正错误的成本是其三倍,在编码阶段是五到十倍,而在系统测试阶段则是十倍。造成这种情况有几个原因:

  • 如果在开发过程的早期修复缺陷,你就不会浪费时间编写额外的文档、编写代码和测试有缺陷的设计。例如,为了某个需求编写 SDD 文档、编写实现该需求的代码、编写测试用例和测试程序并执行这些测试,都是需要时间的。如果该需求本身就有问题,那么你就浪费了所有这些努力。

  • 如果在系统的某个阶段发现了缺陷项,你必须找到并编辑所有与该缺陷相关的内容,这项工作可能会很繁琐,且容易遗漏修改,进而导致不一致性和其他后续问题。

9.4.1 通过验证减少成本

没有任何地方比需求阶段(SyRS 和 SRS 开发)更需要验证活动。如果你坚持要求客户在进入后续阶段之前理解并批准所有需求,你可以确保没有不必要的需求,并且你正在解决客户的问题。没有什么比花费几个月时间记录、编码和测试程序的功能,最终客户却说:“这不是我们要求的”更糟糕了。一个良好的验证过程可以帮助减少这种情况的发生概率。

验证应在需求阶段结束和开发周期结束时进行,涉及提出以下问题:

SyRS(如果存在)

  1. 每个现有需求是否重要?该需求是否描述了客户想要的某个功能?

  2. 每个需求是否正确?它是否明确地(没有歧义)说明了客户的要求?

  3. 是否存在遗漏的需求?

SRS

  1. SyRS(如果存在)中列出的所有软件需求是否也列在 SRS 中?

  2. 每个现有需求是否重要?这个功能是否对系统架构师重要,并且已得到客户的同意?

  3. 每个需求是否正确?它是否明确地(没有歧义)说明了软件必须做什么才能有效?

  4. 是否存在遗漏的需求?

在最终验收测试过程中,测试工程师应持有 SRS 中所有需求的清单,并以复选框的形式列出。当每个需求被测试时,他们应勾选该需求(可能是在执行 STP 中的测试程序时),以确保软件正确地实现了该需求。

9.4.2 通过验证减少成本

如在第 181 页的“验证、确认与评审”中提到的,验证应在软件开发过程的每个阶段之后进行。特别是,在 SRS 之后,每个系统文档都应有一个验证步骤。以下是完成每个文档后你可能会问的一些问题:

SDD

  1. 设计组件是否完全覆盖了 SRS 中的所有需求?

  2. 需求(多个)与软件设计元素(一个)之间是否存在多对一(或一对一)的关系?虽然一个设计项可能满足多个需求,但不应需要多个设计元素来满足单一需求。

  3. 某个软件设计元素是否提供了一个准确的设计来实现给定的需求?

STC

  1. 需求与测试用例之间是否存在一对多(或一对一)的关系?(也就是说,一个需求可以有多个相关的测试用例,但不应有多个需求共享同一个测试用例。^(6))

  2. 某个特定的测试用例是否准确测试了相关的需求?

  3. 与某个需求相关的所有测试用例是否完全测试了该需求的正确实现?

STP

  1. 测试用例在 STC 与测试程序在 STP 之间是否存在多对一关系?也就是说,是否一个测试程序实现一个或多个测试用例,而每个测试用例仅由一个测试程序处理?

  2. 给定的测试程序是否准确实现了所有关联的测试用例?

9.5 了解更多信息

Bremer, Michael. 用户手册手册:如何研究、编写、测试、编辑和制作软件手册. 草谷,加利福尼亚州:UnTechnical 出版社,1999 年。样章可在 www.untechnicalpress.com/Downloads/UMM%20sample%20doc.pdf 下载。

IEEE. “IEEE 标准 830-1998:IEEE 软件需求规范推荐实践。” 1998 年 10 月 20 日。 doi.org/10.1109/IEEESTD.1998.88286

Leffingwell, Dean, 和 Don Widrig. 管理软件需求. 波士顿:Addison-Wesley 专业出版,2003 年。

McConnell, Steve. 代码大全. 第二版. 雷德蒙德,WA:微软出版社,2004 年。

Miles, Russ, 和 Kim Hamilton. 学习 UML 2.0:UML 实用入门. 塞巴斯托波尔,加利福尼亚州:O’Reilly Media,2003 年。

Pender, Tom. UML 圣经. 印第安纳波利斯:Wiley 出版社,2003 年。

Roff, Jason T. UML: 初学者指南. 伯克利,加利福尼亚州:麦格劳-希尔教育,2003 年。

Wiegers, Karl E. 软件需求. 雷德蒙德,WA:微软出版社,2009 年。

———. “编写质量需求。” 软件开发 7,5 期(1999 年 5 月):44–48。

第十二章:需求文档

图片

需求说明了软件必须做什么,以满足客户的需求,具体来说:

  • 系统必须执行的功能(功能性需求

  • 系统必须如何执行这些功能(非功能性需求

  • 软件必须运行的资源或设计参数(约束,这也是非功能性需求)

如果某个软件未能满足特定需求,你不能认为该软件是完整或正确的。因此,一组软件需求是软件开发的基础起点。

10.1 需求来源与可追溯性

每个软件需求必须有一个来源。这可以是更高层次的需求文档(例如,软件需求规格说明书[SRS]中的需求可能来自系统需求规格说明书[SyRS],或者 SyRS 中的需求可能来自客户提供的功能需求文档),特定的用例文档,客户的“工作声明”,客户的口头沟通,或头脑风暴会议。你应该能够追溯每个需求的来源;如果不能,可能说明该需求不必要,应该移除。

反向可追溯性是指能够追溯需求的来源。如在第九章中讨论的,反向可追溯性矩阵(RTM)是列出所有需求及其来源的文档或数据库。通过 RTM,你可以轻松识别需求的来源,以确定其重要性(有关 RTM 的详细描述,请参见《需求/反向可追溯性矩阵》,以及第 178 页)。

10.1.1 建议的需求格式

书面需求应采取以下一种形式:

  • [触发器] 演员动作对象[条件]

  • [触发器] 演员必须动作对象[条件]

方括号内的项是可选的。单词shall表示功能性需求;单词must表示非功能性需求。每个项根据此示例需求描述如下:

当泳池温度在 40 华氏度到 65 华氏度范围内时,除非大气温度超过 90 华氏度,否则泳池监控员应关闭“良好”指示。

触发器触发器是指示需求何时适用的短语。没有触发器意味着该需求始终适用。在本例中,触发器是“当泳池温度在 40 华氏度到 65 华氏度范围内时”。

演员演员是执行动作的人或事物——在本例中,“泳池监控员”。

动作动作是需求导致的活动(“关闭”)。

对象对象是被作用的事物(“‘良好’指示”)。

条件 条件通常是一个负面情况,它会阻止某个行动(如果是正面条件引发行动,那它就是触发条件)。在这个例子中,条件是“除非大气温度高于 90 华氏度”。

一些作者允许用shouldmay代替shallmust;然而,这些术语表明该需求是可选的。本书坚持认为所有需求都是必要的,因此不应使用shouldmay

10.1.2 良好需求的特征

本节讨论了描述良好需求的特征。

10.1.2.1 正确性

需求必须是正确的,这不言而喻,但研究表明,项目成本的约 40%是由于需求中的错误。因此,花时间审查需求并纠正任何错误是确保软件质量的最具成本效益的方式之一。

10.1.2.2 一致性

需求必须相互一致;也就是说,一个需求不能与另一个需求相矛盾。例如,如果一个池温监控器声明温度降到 70 度以下时必须触发警报,而另一个要求温度降到 65 度以下时触发相同的警报,这两个需求就是不一致的。

请注意,一致性指的是同一文档中的需求。如果一个需求与更高级别文档中的需求不一致,那么这个需求是错误的——更不用说不一致了。

10.1.2.3 可行性

如果你无法实际实现一个软件需求,那么它就不是一个需求。毕竟,需求说明了为了提供一个令人满意的软件解决方案必须做什么;如果需求不可行,那么也就无法提供软件解决方案。

10.1.2.4 必要性

根据定义,如果一个软件需求不是必要的,那它就不是一个需求。需求的实现成本很高——它们需要文档、代码、测试程序和维护——因此,除非是必要的需求,否则不应包含在内。不必要的需求通常是“过度完善”的结果,或者仅仅因为有人觉得这些功能很酷而添加,而不考虑实现这些功能的成本。

如果一个需求是必要的,那么:

  • 使产品具有市场竞争力;

  • 满足客户、最终用户或其他利益相关者表达的需求;

  • 区分产品或使用模型;或者

  • 由商业战略、路线图或可持续性需求所决定。

10.1.2.5 优先级

软件需求指定了你必须做的一切,以生产出期望的应用程序。然而,鉴于各种约束(如时间、预算等),你可能无法在软件的第一次发布中实现每个需求。此外,随着时间的推移(和资金的消耗),一些需求可能会被放弃,因为情况发生了变化。因此,一个好的需求应该有相应的优先级。这有助于推动进度,因为团队首先实现最关键的功能,将不太重要的功能推迟到项目开发周期的最后阶段。通常,三个或四个优先级层次就足够了:关键/强制性、重要、可取和可选是不错的例子。

10.1.2.6 完整性

一个好的需求应该是完整的;即它不应该包含任何TBD(待确定)项。

10.1.2.7 明确性

需求不应有任何解释的余地(请注意,TBD 是这一点的特殊情况)。明确无误意味着需求有唯一的解释。

由于大多数需求是用自然语言(如英语)编写的,而自然语言是模糊的,因此在编写需求时必须特别小心,以避免歧义。

一个模糊的需求示例:

当游泳池温度过低时,软件应发出警报。

一个明确无误的示例:

当游泳池温度低于 65 华氏度时,软件应发出警报。

当以下自然语言特征出现在需求中时,便会产生歧义:

模糊性 发生在需求中使用弱词——那些没有确切含义的词时。本节将很快讨论弱词。

主观性 是指不同的人根据个人经验或观点对术语(弱词)赋予不同的含义。

不完整性 由使用待定项、部分规格或无界列表导致。在本节稍后将讨论无界列表。

可选性 发生在使用使要求变为可选而非必需的短语时(例如,由……引起使用应该可能如果可能在适当情况下根据需要)。

欠规范性 发生在需求未能充分指定要求时,通常是由于使用了弱词(如支持分析响应基于)所致。

请考虑这个需求:

游泳池监视器应支持华氏和摄氏温标。

支持在这个上下文中究竟是什么意思?一个开发人员可能会将其解释为最终用户可以选择输入和输出为华氏度或摄氏度(固定),而另一个开发人员则可能解释为两种温标都用于输出,并且输入允许使用任一温标。一个更好的需求可能是:

游泳池监视器的设置应允许用户选择华氏或摄氏温标。

引用不足 是指需求提供了对另一个文档的不完整或缺失引用(例如需求的来源)。

过度概括 是指需求中包含诸如任何所有总是每个等普遍限定词,或者在否定意义上,使用从不仅仅

不 intelligibility 是由糟糕的写作(语法)、未定义的术语、复杂的逻辑(例如双重否定)和不完整性造成的。

被动语态 是指需求未将行动者指派给某个动作。例如,一个使用被动语态的糟糕需求可能是:

如果温度低于 65 华氏度,则应触发警报。

谁负责触发警报?不同的人可能会有不同的理解。更好的需求可能是:

如果温度低于 65 华氏度,泳池监控软件应触发警报。

在需求中使用模糊词汇往往会导致歧义。模糊词汇的例子包括:支持通常某种大多数相当稍微有点某种程度上各种几乎快速简单及时之前之后用户友好有效多个尽可能适当正常能力可靠最先进轻松

例如,“泳池监控器应提供多个传感器”的需求是模糊的,因为多个是一个模糊词。它是什么意思?两个?三个?十几个?

另一种创建模糊需求的方法是使用无界列表——一个缺少起始点、终止点或两者的列表。典型的例子包括类似至少包括但不限于或更晚或更多例如等等的措辞。

例如:“泳池监控器应支持三个或更多传感器。” 它必须支持四个传感器吗?十个传感器吗?无限多个传感器吗?这个需求没有明确说明支持的传感器最大数量是多少。更好的需求可能是:

泳池监控器必须支持三个到六个传感器。

无界列表是无法设计和测试的(因此它们无法满足可行性和可验证性属性)。

10.1.2.8 与实现无关

需求必须仅基于系统的输入和输出。它们不应涉及应用程序的实现细节(这是软件设计描述[SDD]文档的目的)。需求必须将系统视为一个黑箱,其中输入被提供并输出结果。

例如,一个需求可能会指出系统的输入是一个数字列表,该列表的输出是一个已排序的列表。该需求不应写成“应使用快速排序算法。” 软件设计师可能有充分的理由使用不同的算法;需求不应强迫软件设计师或程序员做出选择。

10.1.2.9 可验证性

“如果无法测试,那就不是一个要求”是要求编写者应遵循的座右铭。如果你无法为其创建测试,就无法验证该要求是否已在最终产品中得到实现。事实上,如果你无法找到测试方法,要求可能根本无法实现。

如果你无法创建可以在最终软件产品上运行的物理测试,那么你的要求很可能不是完全基于系统输入和输出。例如,如果你有一个要求是“系统应使用快速排序算法对数据进行排序”,那么你该如何测试呢?如果你不得不依赖“通过检查代码来测试此要求”,那么你可能没有一个好的要求。并不是说要求不能通过检查或分析来验证,但实际的测试总是验证要求的最佳方式,特别是如果你能够自动化该测试。

10.1.2.10 原子要求

一个好的要求陈述不得包含多个要求——也就是说,它不能是一个复合要求。要求应尽可能独立;其实现不应依赖于其他要求。

一些作者声称这两个词在要求中绝不应出现。严格来说,这并不准确。你只是想避免使用fanboys 连词(forandnorbutoryetso)将多个独立的要求合并为一个陈述。例如,以下内容并不是一个复合要求:

当温度在 70 华氏度85 华氏度之间时,泳池监控器应设置“良好”指示。

这是一个单一的要求,而不是两个。一词的出现并不会产生两个要求。如果你真想挑剔并去掉,你可以这样重写该要求:

当温度在 70 华氏度到 85 华氏度的范围内时,泳池监控器应设置“良好”指示。

然而,实际上第一个版本并没有什么问题。以下是一个复合要求的例子:

当温度低于 70 华氏度高于 85 华氏度时,泳池监控器应清除“良好”指示。

这应当被重写为两个独立的要求:^(1)

当温度低于 70 华氏度时,泳池监控器应清除“良好”指示。

当温度超过 85 华氏度时,泳池监控器应清除“良好”指示。

请注意,复合要求在构建可追溯性矩阵时会造成问题,正如本章在“更新包含要求信息的可追溯性矩阵”一节中讨论的那样,见第 222 页。复合要求还会导致测试问题。一个要求的测试必须给出单一的答案:通过或失败。你不能有一个要求的部分通过,而另一个部分失败。这是复合要求的明显标志。

10.1.2.11 唯一性

需求规范中不得包含任何重复的需求。重复内容使得文档的维护变得更加困难,特别是在修改需求时,如果忘记修改重复的部分,会导致问题。

10.1.2.12 可修改

期望项目的需求在其生命周期中保持不变是不现实的。预期会发生变化,技术会变化,市场会变化,竞争也会变化。在产品开发过程中,你可能需要修改一些需求,以适应不断变化的条件。特别是,你不希望选择那些强加某些系统约束的需求,而这些约束会成为其他需求的基础。例如,考虑以下需求:

游泳池监控器应使用 Arduino Mega 2560 单板计算机作为控制模块。

基于此需求,其他需求可能包括“游泳池监控器应使用 A8 引脚作为水位指示”以及“游泳池监控器应使用 D0 引脚作为低温输出”。这些基于使用 Mega 2560 板的需求的问题在于,如果将来出现新的板(例如 Teensy 4.0 模块),那么修改第一个需求就需要同时修改所有依赖它的其他需求。一组更好的需求可能是:

游泳池监控器应使用一块支持 8 个模拟输入、4 个数字输出和 12 个数字输入的单板计算机。

游泳池监控器应使用一个数字输出引脚作为低温报警。

游泳池监控器应使用一个模拟输入引脚作为池水水位输入。

10.1.2.13 可追溯

所有需求必须是可追踪的,包括正向和反向追踪。反向追踪意味着该需求可以追溯到其来源。为了能够追溯到其他对象,该需求必须具有一个标签(一个独特的标识符,如第四章中介绍的)。

每个需求必须包括其来源,作为需求文本或标签的一部分;否则,必须提供一个单独的 RTM 文档(或数据库),来提供这些信息。通常,你应该在需求本身中明确列出需求的来源。

正向追踪提供了与所有基于(或由)需求文档派生的文档之间的链接。通常,正向追踪通过 RTM 文档来处理;在每个需求文档中维护这些信息会太过繁琐(会有过多重复信息,如前所述,这会使得文档的维护变得困难)。

10.1.2.14 表述为正面

需求应陈述必须满足的条件,而不是陈述不允许发生的事情。大多数以否定方式表述的需求无法验证。例如,以下是一个不好的需求:

游泳池监控器不得在低于冰点的环境温度下运行。

该需求建议当温度降至冰点以下时,游泳池监控器必须停止运行。这是否意味着系统会感知温度并在低于冰点时关闭?还是仅仅意味着系统不能期望在低于冰点时产生合理的值?更好的需求可能是:

如果温度下降到冰点以下,游泳池监控器应自动关闭。

希望有一个需求讨论当温度回升至冰点以上时应发生什么。如果游泳池监控器已被关闭,它能感知到这个变化吗?

10.2 设计目标

尽管需求不能是可选的,但有时能够在需求文档中列出可选项是有益的。这些项目被称为设计目标。

设计目标违反了许多优秀需求的特性。显然,它们不是必需的,但它们也可能是不完整的、稍微模糊的、指定了实现或不可测试的。例如,一个设计目标可能是使用 C 标准库内置的 sort() 函数(一个实现细节)来减少开发时间。另一个设计目标可能是这样的:

游泳池监控器应支持尽可能多的传感器。

正如你所看到的,这既是可选的,也是开放式的。设计目标是开发人员可以用来指导开发选择的建议。它不应涉及额外的设计工作或测试,这些会导致进一步的开发开销。它应该只是帮助开发人员在设计系统时做出某些开发选择。

与需求类似,设计目标也可以有标签,尽管在文档系统中追踪设计目标的需求不大。然而,因为它们可能在某些时候被提升为需求状态,所以最好为它们关联一个标签,以便它们能作为派生文档中需求的起源。

10.3 系统需求规格说明书

《系统需求规格说明书》文档汇集了与完整系统相关的所有需求。这可能包括业务需求、立法/政治需求、硬件需求和软件需求。SyRS 通常是一个非常高层次的文档,尽管它是组织内部的。其目的是为组织的从属文档(例如 SRS)中出现的所有需求提供一个单一来源

SyRS 的形式与 SRS(在下一节中描述)相同,因此除了指出 SyRS 会生成 SRS(以及在适当的情况下生成硬件需求规格说明书,或 HRS)之外,我不会进一步阐述其内容。SyRS 是可选的,通常在小型纯软件项目中不存在。

SyRS 需求通常表述为“系统应”或“系统必须”。这与 SRS 中的需求通常表述为“软件应”或“软件必须”形成对比。

10.4 软件需求规格说明书

软件需求规格说明书(SRS)是一个包含特定软件项目所有需求和设计目标的文档。互联网上有成百上千个 SRS 文档的示例。许多网站似乎对 SRS 的构成有自己的理解。与其在这个喧嚣的环境中引入另一个新模板,本书决定使用 IEEE 定义的模板:IEEE 830-1998《软件需求规格说明书推荐实践》。

本书认为使用 IEEE 830-1998 推荐实践是一个安全的决策,但请注意,该标准并非完美。它是由一个委员会创建的,因此包含了许多冗余信息(不必要的信息)。委员会设计的标准的问题在于,唯一能通过它们的方式是让每个人都将自己的想法注入文档中,即使这些想法与文档中的其他内容冲突。尽管如此,IEEE 830-1998 推荐实践仍然是一个很好的起点。你不必强制实施其中的所有内容,但在编写 SRS 时应将其作为指导方针。

一个典型的 SRS 使用类似于以下的提纲:

目录

1 引言

1.1 目的

1.2 范围

1.3 定义、缩写和术语

1.4 参考文献

1.5 概述

2 整体描述

2.1 产品视角

2.1.1 系统接口

2.1.2 用户界面

2.1.3 硬件接口

2.1.4 软件接口

2.1.5 通信接口

2.1.6 内存约束

2.1.7 操作

2.2 站点适配需求

2.3 产品功能

2.4 用户特征

2.5 约束条件

2.6 假设与依赖关系

2.7 需求分配

3 特定需求

3.1 外部接口

3.2 功能需求

3.3 性能要求

3.4 逻辑数据库要求

3.5 设计约束

3.6 标准合规性

3.7 软件系统属性

3.7.1 可靠性

3.7.2 可用性

3.7.3 安全性

3.7.4 可维护性

3.7.5 可移植性

3.8 设计目标

4 附录

5 索引

第三部分是最重要的——这里将列出所有的需求以及设计目标。

10.4.1 引言

引言部分包含 SRS 的整体概述。以下子部分描述了引言部分的建议内容。

10.4.1.1 目的

在目的部分,你应该说明 SRS 的目的以及目标受众是谁。对于 SRS,目标受众可能是需要验证 SRS 的客户以及将创建 SDD、软件测试用例和软件测试程序并编写代码的开发人员/设计人员。

10.4.1.2 范围

范围部分通过名称描述软件产品(例如,Plantation Productions Pool Monitor),解释产品的功能,并在必要时说明它将做的事情。(不用担心这不符合“正面陈述”规则,因为这是范围声明,而不是需求陈述。)范围部分还概述了项目的目标、产品的收益和目标,以及为产品编写的应用软件。

10.4.1.3 定义、缩写词和缩写

定义部分提供了 SRS 使用的所有术语、缩写词和缩写的词汇表。

10.4.1.4 参考资料

参考资料部分提供了所有 SRS 引用的外部文档的链接。如果你的 SRS 依赖于外部 RTM 文档,你应该在这里引用该文档。如果文档是公司内部的,你应该提供其内部文档编号/引用。如果 SRS 引用的是公司外部的文档,SRS 应列出该文档的标题、作者、出版商、日期以及如何获取该文档的信息。

10.4.1.5 概述

概述部分描述了 SRS 其余部分的格式及其包含的信息(如果你省略了 IEEE 建议中的某些项目,这一部分尤其重要)。

10.4.2 总体描述

总体描述部分指定了以下方面的要求:

10.4.2.1 产品视角

产品视角部分将产品与其他(可能是竞争的)产品进行比较。如果该产品是更大系统的一部分,产品视角应指出这一点(并描述本文件中的需求如何与更大系统相关)。本节还可以描述产品的各种限制条件,例如:

10.4.2.1.1 系统接口

本节描述了软件如何与系统的其他部分进行接口。这通常包括任何 API,例如软件如何与 Wi-Fi 适配器进行交互,以便远程查看泳池读数。

10.4.2.1.2 用户接口

本节列出了满足需求所需的所有用户界面(UI)元素。例如,在泳池监控场景中,本节可以描述用户如何通过 LCD 显示器和设备上的各种按键与设备进行交互。

10.4.2.1.3 硬件接口

本节可以描述软件如何与底层硬件进行交互。例如,泳池监控 SRS 可以指出,软件将在 Arduino Mega 2560 上运行,使用 A8 至 A15 的模拟输入连接传感器,使用 D0 至 D7 的数字线路作为连接按钮的输入。

10.4.2.1.4 软件接口

本节描述了实现系统所需的任何附加/外部软件。这可能包括操作系统、第三方库、数据库管理系统或其他应用程序系统。例如,池监控器 SRS 可能会描述使用厂商提供的库来读取来自各种传感器的数据。对于每个软件项,你应在本节中包含以下信息:

  • 名称

  • 规格编号(如有,供应商提供的值)

  • 版本号

  • 来源

  • 目的

  • 相关文档的引用

10.4.2.1.5 通信接口

本节列出了产品将使用的任何通信接口,如以太网、Wi-Fi、蓝牙和 RS-232 串行接口。例如,池监控器 SRS 可能会在本节中描述 Wi-Fi 接口。

10.4.2.1.6 内存约束

本节描述了内存和数据存储的所有约束。对于在 Arduino Mega 2560 上运行的池监控器,SRS 可能会说明程序存储存在 1K EEPROM 和 8K RAM 的限制,此外还包括 64K 到 128K 的 Flash 存储。

10.4.2.1.7 操作

本节(通常与 UI 部分合并)描述了产品的各种操作。它可能详细说明不同的操作模式——如正常、低功耗、维护或安装模式——并描述交互式会话、无人值守会话和通信功能。

10.4.2.2 站点适配要求

本节描述了任何特定于站点的适应性。例如,池监控器 SRS 可能会在本节中描述带有水疗池的池的可选传感器。

10.4.2.3 产品功能

产品功能部分描述了软件的(主要)功能。例如,池监控器 SRS 可能会使用本节来描述软件如何监控池水位、池水温度、大气温度、水导电率(用于盐水池)、通过过滤系统的水流量,以及自上次滤网清洁以来的过滤时间。

10.4.2.4 用户特征

用户特征部分描述了将使用该产品的人。例如,池监控器 SRS 可能会定义一个工厂测试技术员(负责测试和修理设备)、一个现场安装技术员、一个高级终端用户和一个普通终端用户。可能会有不同的软件需求仅适用于某些类型的用户。

10.4.2.5 约束

约束部分描述了可能影响开发者在设计和实现软件时选择的任何限制,例如:

  • 监管政策

  • 硬件限制(例如,信号时序要求)

  • 与其他应用程序的接口

  • 并行操作

  • 审计功能

  • 控制功能

  • 高级语言要求

  • 信号握手协议(例如,XON-XOFF)

  • 可靠性要求

  • 应用的重要性

  • 安全和保密考虑

10.4.2.6 假设与依赖

假设与依赖部分列出的项目仅适用于需求;它们不对设计施加约束。如果某个假设发生变化,将需要更改需求,而不是设计(尽管需求变化可能也会影响设计)。例如,在泳池监控 SRS 中,一个假设可能是 Arduino Mega 2560 提供足够的计算能力、端口和内存来完成任务。如果这个假设不正确,可能会影响一些关于端口使用、可用内存等方面的需求。

10.4.2.7 需求划分

需求划分部分将需求和特性分成两个或更多组:一组是当前版本要实现的,另一组是计划在未来版本中实现的。

10.4.3 特定需求

特定需求部分应列出所有需求和支持文档。这些文档应编写成系统设计师可以根据记录的需求构建软件设计的方式。

所有需求应具备本章前面讨论的特点。它们还应具有标签和指向其来源的交叉引用(追踪)。由于需求文档将被阅读的次数远远超过写作次数,你应特别注意使该文档尽可能易读。

10.4.3.1 外部接口

外部接口部分应详细描述软件系统的所有输入和输出,但不要重复产品视角部分接口子章节中的信息。每项列表应包含以下信息(根据系统的需要):

  • 标签

  • 描述

  • 输入源或输出目的地

  • 有效值范围及必要的精度/容差

  • 测量单位

  • 时序和容差

  • 与其他输入/输出项的关系

  • 屏幕/窗口格式(仅列出实际需求的屏幕要求——不要在此设计用户界面)

  • 数据格式

  • 命令格式、协议及任何必要的哨兵消息

许多 SRS 作者会将此部分从特定需求部分中提取出来,并放到产品视角部分,以避免重复,尽管 IEEE 830-1998 标准建议该部分应作为特定需求部分的一部分。然而,IEEE 文档仅为推荐实践,因此最终选择权在于你。最重要的是,这些信息出现在 SRS 中。

10.4.3.2 功能需求

功能需求部分包含大多数人立刻识别为需求的项目。此部分列出了输入时系统执行的基本活动,并描述了系统如何利用输入生成输出。按照惯例,功能需求始终包含助动词。例如,“软件在池低输入激活时触发警报。”

典型的功能需求包括以下内容:

  • 输入有效性检查及对无效输入的响应

  • 操作顺序

  • 异常情况响应,包括:溢出、下溢、算术异常、通信故障、资源超限、错误处理和恢复,以及协议错误

  • 软件执行期间数据的持久性

  • 参数的影响

  • 输入/输出关系,包括:合法和非法输入模式、输入与输出的关系,以及如何根据输入计算输出(但要小心,不要将软件设计融入需求中)

10.4.3.3 性能需求

性能需求部分列出了规定软件必须达到的静态或动态性能目标的非功能性需求。像大多数非功能性需求一样,性能需求通常包含助动词必须——例如,“软件必须能够控制内部显示和远程显示。”

静态性能需求是为整个系统定义的,不依赖于软件的能力。池监控器的一个好例子是“池监控器必须能够读取来自 5 到 10 个模拟传感器的输入数据。”这是一个静态需求,因为在特定安装下,传感器的数量是静态的(例如,软件效率提高也不会改变传感器数量)。

动态性能需求是软件在执行过程中必须满足的要求。一个好的例子可能是“软件必须每秒读取每个传感器 10 到 20 次。”

10.4.3.4 逻辑数据库需求

逻辑数据库需求部分描述了规定应用程序必须访问的数据库记录和字段格式的非功能性需求。通常,这些需求涉及外部访问的数据库。内部数据库(即对外部不可见的数据库)通常不在软件需求的范畴内,尽管 SDD 可能会涵盖这些内容。

10.4.3.5 设计约束

标准合规性是设计约束的一个例子。任何限制软件设计师无法使用任意实现的方法的约束都应该列出在设计约束部分。例如,限制从 16 位 A/D 转换器读取数据仅为 13 位,因为 A/D 芯片/电路噪声较大,低三位可能不可靠。

10.4.3.6 标准合规性

标准合规性部分应描述并提供所有软件必须遵循的标准的链接。标准编号和文档描述应允许读者在必要时研究这些标准。

10.4.3.7 软件系统属性

软件系统属性部分列出了软件系统的特性,包括:

10.4.3.7.1 可靠性

需求部分将指定软件系统的预期正常运行时间要求。可靠性是一个非功能性需求,通常以百分比形式描述系统在没有故障的情况下能运行的时间。一个典型的例子是“预期可靠性为 99.99%”,意味着软件在 0.01%的时间内可能会出现故障。与许多非功能性需求一样,很难提供测试以确保可靠性目标得到满足。

10.4.3.7.2 可用性

可用性属性指定最终应用程序中可以接受的停机时间(实际上,它指定的是停机时间的反向)。可用性指定用户随时访问软件系统的能力。当系统宕机时,用户无法访问。这一非功能性需求可能区分计划停机时间和非计划停机时间(例如,硬件故障导致系统重启)。

10.4.3.7.3 安全性

安全性属性是一种非功能性需求,指定预期的系统安全性,可能包括加密要求和网络套接字类型等内容。

10.4.3.7.4 可维护性

可维护性是另一项非功能性需求,可能很难指定和测试。在大多数规范中,通常有一个模糊的陈述,例如“软件应易于维护”。这种说法没有实际意义。相反,这个属性应该明确表示:“一个有经验的维护程序员不超过一周就能掌握该系统并进行修改。”

需求组织

任何足够复杂的系统都会有大量的需求,因此如果没有正确组织,SRS(软件需求规格说明书)可能会变得笨重。应用程序类型多样,组织需求的方式也有很多种。没有一种特定的组织方式是正确的;你需要根据 SRS 的受众选择以下选项之一。

按系统模式组织

一些系统在不同模式下运行——例如,嵌入式系统可能有低功耗模式和常规模式。在这种情况下,你可以将系统需求组织成这两类。

按用户类别组织

一些系统支持不同类别的用户(例如,初学者、高级用户和系统管理员)。在一个复杂的系统中,你可能会有普通用户、高级用户、维护人员和程序员访问该系统。

按对象类别组织

对象是软件系统中的实体,代表了现实世界中的对象。你可以根据这些对象的类型或类别来组织需求。

按功能组织

组织 SRS 需求的常见方法之一是根据它们实现的功能特性进行分类。这种方法在应用程序为系统中的所有功能提供用户界面时特别有用。

按输入刺激组织

如果处理不同输入是应用程序的主要活动,那么你可以考虑根据应用程序处理的输入类型来组织你的 SRS。

按输出响应组织

类似地,如果生成广泛的输出是应用程序的主要活动,那么按输出响应来组织需求可能更为合理。

按功能层次结构组织

另一种常见的 SRS 组织方法是按功能进行组织。这通常是当没有其他合适的组织方式时,SRS 编写者所采用的备选方案。按共同输入、命令输出、常见数据库操作以及程序中的数据流对需求进行分组,是组织 SRS 的合理方式。

10.4.3.7.5 可移植性

可移植性描述了将软件移植到不同环境所涉及的内容。本节应讨论跨 CPU、操作系统和编程语言方言的可移植性。

10.4.3.8 设计目标

在编写软件需求规格说明书(SRS)时,通常很容易把所谓的可选需求放入其中。然而,正如本章前面所提到的,需求的定义本身就不可以是可选的。尽管如此,在某些时候,你可能会想说:“如果可能的话,请添加此功能。”你可以将这些请求表述为设计目标,并由设计师或软件工程师决定该功能是否值得添加。将设计目标放在单独的章节中,并在 SRS 中清楚地表述“作为设计目标,软件应该……。”

10.4.4 支持信息

任何优秀的软件需求规格说明书都应包含支持信息,如目录、附录、词汇表和索引。还应有一个需求标签表(按数字或字典顺序排序),列出每个标签、需求的简短描述以及需求在文档中出现的页码(这也可以放在 RTM 中,而不是 SRS 中)。

10.4.5 一个示范软件需求规格说明书

本节提供了一个游泳池监控器的示例 SRS,类似于本章至今提供的示例。由于篇幅原因,该游泳池监控器 SRS 被大大简化;其目的是提供一个示范性的大纲,而非完整的规格说明。

目录

1 引言

1.1 目的

游泳池监控设备将跟踪池水水位,并在水位较低时自动补充水池。

1.2 范围

游泳池监控器的软件将根据本规范编写。

硬件和软件开发的目标是根据分配给游泳池监控器系统的需求,提供功能、状态信息、监控和控制硬件、通信和自检功能。

1.3 定义、缩略语和缩写

术语 定义
准确度 与真实值的一致程度,对于数字显示的输入,以读数的百分比表示(ANSI N42.18-1980)。
异常 在软件的文档或操作中观察到的任何偏离预期的情况。(来源:IEEE Std 610.12-1990。)
灾难性事件 一种没有预警且无法恢复的事件。灾难性事件包括导致计算和处理错误的硬件或软件故障。处理器将在灾难性事件发生后,根据配置项,停止或重置。
处理的条件 系统设计用于处理并继续处理的条件。这些条件包括异常、故障和失败。
SBC 单板计算机
软件需求规格说明书(SRS) 记录软件及其外部接口的基本需求(功能、性能、设计约束和属性)(IEEE Std 610.12-1990)。
SPM 游泳池监控器
系统需求规格说明书(SyRS) 体现系统需求的结构化信息集合(IEEE Std 1233-1998)。一份记录需求的规格说明书,用于建立设计基础和系统或子系统的概念设计。

1.4 参考文献

[无]

1.5 概述

第二部分提供了游泳池监控器(硬件和软件)的总体描述。

第三部分列出了游泳池监控器系统的具体需求。

第 4 和第五部分提供了任何必要的附录和索引。

在第三部分中,需求标签采用以下格式:

<空白> [POOL_SRS_xxx]
<空白> [POOL_SRS_xxx.yy]
<空白> [POOL_SRS_xxx.yy.zz]
<等等>

其中xxx是一个三位或四位的 SRS 需求编号。

如果需要在两个其他值之间插入新的 SRS 需求标签(例如,在 POOL_SRS_040 和 POOL_SRS_041 之间添加一个需求),则应在 SRS 标签编号后附加一个小数分数(例如,POOL_SRS_040.5)。如果需要,可以添加多个小数点后缀(例如,POOL_SRS_40.05.02)。

2 总体描述

游泳池监控器(SPM)的目的是提供一个自动系统,用于保持池中的水位。这个任务足够简单,可以编写一个简短的 SRS,以便适应本章内容。

2.1 产品视角

在现实世界中,SPM 可能提供许多额外功能;将这些功能添加到此处只会增加 SRS 的大小,而不会提供太多额外的教育益处。此规格已故意简化,以符合本书的编辑要求。

2.1.1 系统接口

SPM 设计假定使用 Arduino 兼容的单板计算机(SBC)。因此,软件将通过 Arduino 兼容的库与硬件进行接口。

2.1.2 用户界面

用户界面应包括一个小型四行显示屏(每行最少 20 个字符)、六个按键(上、下、左、右、取消/返回、选择/确认)和一个旋转编码器(旋转旋钮)。

2.1.3 硬件接口

本文件没有指定使用特定的 SBC。然而,SBC 必须至少提供以下功能:

  • 16 个数字输入

  • 1 个模拟输入

  • 2 个数字输出

  • 少量非易失性可写内存(例如 EEPROM),用于存储配置值。

  • 一个实时时钟(RTC;这可以是外部模块)

  • 一个看门狗定时器,用于监控系统的软件操作。

SPM 提供泳池传感器,用于判断泳池水位的高低。它还提供一个电磁阀接口,允许 SPM 开启或关闭泳池的水源。

2.1.4 软件接口

SPM 软件是自包含的,不提供任何外部接口,也不需要任何外部软件接口。

2.1.5 通信接口

SPM 是自包含的,不与外界通信。

2.1.6 存储限制

由于 SPM 运行在 Arduino 兼容的 SBC 上,具体型号的选择(例如,Arduino Mega 2560 SBC 只提供 8KB 的静态 RAM)将决定(严重的)内存限制。

2.1.7 操作

SPM 以始终开启模式运行,全天候 24/7/365 监控泳池。因此,模块本身不应消耗过多电力。然而,它将通过电源供应连接到市电,因此不需要极低功耗操作。它将持续监控泳池水位,并在水位较低时自动开启水源。为了避免在传感器故障时发生水灾,SPM 将限制每天引入泳池的水量(时间限制可由用户选择)。

2.2 现场适配要求

对于这一特定版本的 SPM,几乎没有现场适配要求。没有可选的传感器或操作,SPM 本身之外唯一的接口是为系统提供电源和水源(通过电磁阀接口)。

2.3 产品功能

产品将使用七个水位传感器来确定池水水位:三个数字传感器提供低水位指示,三个数字传感器提供高水位指示,以及一个模拟传感器提供池水深度指示(可能只有几英寸或几厘米的范围)。三个低水位数字传感器在水位达到传感器水平时处于激活状态。水位低时,系统将开始注水。为避免传感器故障导致洪水,三个传感器以三分之二的配置工作,意味着必须至少有两个传感器指示低水位,SPM 才会尝试注水。三个高水位传感器在水位较高时以相同的方式工作,提示 SPM 停止注水。模拟传感器提供一个小范围的深度;SPM 将使用模拟传感器作为备份,以验证池水水位低于注水阈值。SPM 还将使用模拟传感器来确认在开启水源时池水确实在注入。

2.4 用户特征

SPM 用户分为两类:技术人员和最终用户。技术人员负责安装和调整 SPM,最终用户是游泳池的所有者,负责日常使用 SPM。

2.5 限制条件

SPM 应小心设计,防止因操作不当造成洪水和过度用水。特别是,软件必须足够强大,能够判断游泳池是否未被正确注满,并在传感器未显示正常工作时停止注水。如果任何传感器出现故障,软件应足够智能,避免盲目保持水流开启(否则可能导致水灾)。例如,如果 SPM 连接到一个地上游泳池,而该池有泄漏,可能永远无法将池水注满。软件应能够处理这种情况。

系统应具备故障安全功能,即在停电时应自动关闭水阀。某种类型的看门狗定时器还应检查软件是否正常运行,如果发生超时(例如,软件卡死),应关闭水阀。

为了避免由于继电器故障引发的洪水,SPM 应使用两个串联的继电器来打开水阀。两个继电器都必须通过软件触发才能打开电磁阀。

2.6 假设和依赖关系

本文档中的要求假设 SBC 拥有足够的资源(计算能力)来处理任务,并且设备能够在 24/7/365 的实时环境中合理运行。

2.7 要求的分配

这些要求定义了一个非常简单的游泳池监控器,用于展示完整的 SRS。由于这是一个为非常小的 SPM 设定的最小要求集,因此假设基于这些要求构建的产品将实现所有这些要求。一个实际的产品可能会包括许多超出这里列出的附加功能,并相应地增加该文档中的要求数量。

3 特定要求

3.1 外部接口

[POOL_SRS_001]

SPM 应提供一个数字输入,用于导航 按钮。

[POOL_SRS_002]

SPM 应提供一个数字输入,用于导航 按钮。

[POOL_SRS_003]

SPM 应提供一个数字输入,用于导航 按钮。

[POOL_SRS_004]

SPM 应提供一个数字输入,用于导航 按钮。

[POOL_SRS_005]

SPM 应提供一个数字输入,用于 取消/返回 按钮。

[POOL_SRS_006]

SPM 应提供一个数字输入,用于 选择/输入 按钮。

[POOL_SRS_007]

SPM 应提供四个数字输入,用于旋转编码器(正交)输入。

[POOL_SRS_008.01]

SPM 应提供一个数字输入,用于主 水位低 传感器。

[POOL_SRS_008.02]

SPM 应提供一个数字输入,用于次级 水位低 传感器。

[POOL_SRS_008.03]

SPM 应提供一个数字输入,用于三级 水位低 传感器。

[POOL_SRS_009.01]

SPM 应提供一个数字输入,用于主 水位高 传感器。

[POOL_SRS_009.02]

SPM 应提供一个数字输入,用于次级 水位高 传感器。

[POOL_SRS_009.03]

SPM 应提供一个数字输入,用于三级 水位高 传感器。

[POOL_SRS_011]

SPM 应提供一个模拟输入(最低 8 位分辨率)用于水位传感器。

[POOL_SRS_012]

SPM 应提供两个数字输出,用于控制水源电磁阀。

3.2 功能要求

[POOL_SRS_013]

SPM 应允许用户通过用户界面设置 RTC 日期和时间。

[POOL_SRS_014]

SPM 应具有最大填充时间,指定在 24 小时内水阀可以激活的最大时间(小时:分钟)。

[POOL_SRS_015]

用户应能够通过 SPM 用户界面设置最大填充时间(使用导航和输入按钮)。

[POOL_SRS_015.01]

一旦用户从用户界面选择了最大填充时间,用户应能够使用导航按钮选择小时或分钟字段。

[POOL_SRS_015.02]

用户应能够在选择小时字段后,使用旋转编码器独立设置最大填充时间的小时值。

[POOL_SRS_015.03]

用户应能够在选择分钟字段后,使用旋转编码器独立设置最大填充时间的分钟值。

[POOL_SRS_015.04]

软件不应允许最大填充时间超过 12 小时。

[POOL_SRS_016]

SPM 应每 24 小时在特定时间检查一次泳池水位,以确定是否需要向泳池加水。

[POOL_SRS_017]

用户应能够在 SPM 用户界面中设置 SPM 检查泳池水位的时间(因此,也就是 SPM 填充泳池的时间)。

[POOL_SRS_017.01]

一旦用户从用户界面选择了泳池水位检查时间,用户应能够使用导航按钮选择小时或分钟字段。

[POOL_SRS_017.02]

用户应能够在选择小时字段后,使用旋转编码器独立设置泳池水位检查时间的小时数值。

[POOL_SRS_017.03]

用户应能够在选择分钟字段后,使用旋转编码器独立设置泳池水位检查时间的分钟数值。

[POOL_SRS_017.04]

默认(恢复出厂设置)泳池检查时间应为凌晨 1:00。

[POOL_SRS_018]

每天泳池检查时,系统应读取三个 泳池水位低 传感器,并在至少两个传感器指示水位低时开始泳池填充操作。

[POOL_SRS_018.01]

在泳池填充操作过程中,软件应累计一个运行中的 填充时间

[POOL_SRS_018.02]

在泳池填充操作过程中,如果填充时间超过最大填充时间,软件应停止泳池填充操作。

[POOL_SRS_018.03]

在泳池填充操作过程中,软件应读取 泳池水位高 传感器,如果至少两个传感器指示水位过高,则停止泳池填充操作。

[POOL_SRS_018.04]

在泳池填充操作过程中,软件应读取模拟泳池水位传感器,如果每半小时操作后水位没有增加,则关闭水流。

[POOL_SRS_019]

软件应允许用户选择一个 手动泳池 填充模式,该模式会打开水源至泳池。

[POOL_SRS_019.01]

软件应允许用户选择一个 自动泳池 填充模式,该模式会关闭手动泳池填充模式。

[POOL_SRS_019.02]

在手动泳池填充模式下,软件应忽略最大填充时间。

[POOL_SRS_019.03]

在手动泳池填充模式下,软件应忽略 泳池水位高泳池水位低 传感器(当用户关闭手动填充模式时,填充停止)。

[POOL_SRS_020]

软件应至少以看门狗超时周期的两倍频率更新系统看门狗定时器。

[POOL_SRS_020.01]

看门狗超时时间应不小于 5 秒,不大于 60 秒。

3.3 性能要求

[POOL_SRS_001.00.01]

SPM 应当对所有按钮输入进行去抖动处理。

[POOL_SRS_007.00.01]

SPM 应能够读取旋转编码器输入,而不丢失输入上的任何更改。

[POOL_SRS_015.00.01]

SPM 应保持最大池水填充时间的准确性,误差不超过一分钟。

[POOL_SRS_017.00.01]

SPM 应保持池水位检查时间的准确性,误差不超过一分钟。

3.4 逻辑数据库需求

[POOL_SRS_014.00.01]

SPM 应将最大填充时间存储在非易失性存储器中。

[POOL_SRS_016.00.01]

SPM 应将池水检查时间存储在非易失性存储器中。

3.5 设计约束

[无]

3.6 标准符合性

[无]

3.7 软件系统属性

3.7.1 可靠性

软件将全天候运行(24/7/365)。因此,系统设计的关键因素是其稳健性。特别是,系统应具备故障安全功能,以确保在软件或其他故障发生时,水阀能自动关闭。

3.7.2 可用性

软件应持续运行(24/7/365)。软件不得受计数溢出或与长期执行相关的其他问题影响。最终用户应期待至少 99.99%的正常运行时间。

3.7.3 安全性

系统没有安全要求(封闭的、断开连接的、空气隔离的系统)。

3.7.4 可维护性

除了通常期望的专业软件工程项目中的维护性要求外,其他维护性要求不再列出。

也就是说,这是一个基础的需求文档。如果有人真的构建这个系统,预计将会有未来的增强功能。因此,系统应根据这些预期进行设计和实现。

3.7.5 可移植性

预计软件将运行在 Arduino 类设备上。除非选择不同的 Arduino 兼容模块(例如,Arduino Mega 2560 与 Teensy 4.0)进行实现,否则没有其他可移植性要求。

3.8 设计目标

本项目无要求。

4 附录

[无]

5 索引

鉴于此 SRS 的(较小)规模,文中未列出索引,以减少本书的页数。

10.5 创建需求

到目前为止,本章已经定义了需求和需求文档。但你可能会问,“需求到底是如何最初提出的?”这一节将为这个问题提供一些见解。

现代需求创建方法涉及使用案例,这些案例在第四章中介绍。系统架构师研究最终用户如何使用系统(用户故事),并从该研究中创建一组场景(使用案例)。每个使用案例成为一组一个或多个需求的基础。本节从游泳池监控器场景出发,考虑一个来自现实世界系统的例子——Plantation Productions 数字数据采集与控制(DAQ)系统。^(2)

DAQ 系统由多个相互连接的电路板组成,包括模拟 I/O 板、数字 I/O 板、数字输出板(继电器板)和一个运行系统固件的单板计算机(SBC),Netburner MOD54415。这些组件使系统设计人员能够读取各种模拟和数字输入,计算结果并根据这些输入做出决策,然后通过将数字和模拟输出值发送到外部设备来控制这些设备。例如,DAQ 系统最初设计用于控制 TRIGA^(3)研究反应堆。

DAQ 系统的固件要求过于庞大,无法在此复制,因此本章将限制讨论系统首次启动时必须进行的某些 I/O 初始化。Netburner MOD54415 包括一组八个 DIP 开关,DAQ 系统使用这些开关来初始化各种系统组件。这些 DIP 开关执行以下功能:

  1. 启用/禁用 RS-232 端口命令处理。

  2. 启用/禁用 USB 端口命令处理。

  3. 启用/禁用以太网端口命令处理。

  4. 指定一个以太网连接或五个同时的以太网连接。

  5. 使用两个 DIP 开关指定四种不同的以太网地址;请参见表 10-1。

  6. 启用/禁用测试模式。

  7. 启用/禁用调试输出。

表 10-1: 以太网地址选择

DIP 开关 A DIP 开关 A + 1 以太网地址
0 0 192.168.2.70
1 0 192.168.2.71
0 1 192.168.2.72
1 1 192.168.2.73

关于 DAQ 软件初始化的最后一点需要注意:调试输出使用 Netburner COM1:端口。Netburner 与 USB 端口共享此串口硬件。如果用户启用调试输出和 USB 命令端口,则会发生冲突。因此,要启用调试端口,必须满足两个条件:必须启用调试输出并禁用 USB 端口命令处理。

要启用来自 RS-232 或 USB 端口的命令,软件必须读取开关。如果特定开关指示命令流处于活动状态,则软件必须创建一个新任务^(4)来处理该端口的输入。新创建的任务负责从给定端口读取字符,并在接收到换行符时将整行文本发送到系统的命令处理器。如果相应的 DIP 开关处于禁用位置,软件将不会创建 RS-232 或 USB 任务,系统将忽略这些端口。

启用以太网命令稍微复杂一些。与以太网端口相关联有四个 DIP 开关。以太网初始化操作必须考虑所有四个 DIP 开关的设置。

一个 DIP 开关控制 DAQ 软件支持的并发客户端数量。在某一位置,DAQ 软件仅支持一个以太网客户端;在另一个位置,软件支持最多五个以太网客户端。在某些环境中,您可能需要允许多个主机计算机访问数据采集和控制硬件;例如,在调试过程中,您可能希望有一台测试计算机监控操作。在某些安全应用程序中(部署后),您可能希望限制对 DAQ 系统的访问,仅限一台计算机。

第三和第四个以太网 DIP 开关允许操作员选择四个独立的 IP/以太网地址之一。这允许在同一系统中控制最多四个独立的 Netburner 模块。如表 10-1 所示,四个可选的以太网地址为 192.168.2.70 到 192.168.2.73(当然,要求可以更改以支持不同的 IP 地址,但这些是为初始 DAQ 系统设计的便捷地址)。

10.6 用例

根据前述的用户故事,下一步是构建一组用例,描述这些操作。请记住,用例不仅仅是几张 UML 图,它们还包括描述性的叙述(见“用例叙述”第 80 页)。

角色 在以下用例中,只有一个角色,即系统用户

触发器 在以下所有用例中,触发每个用例的触发器是系统启动。系统在启动时读取 DIP 开关设置,并根据这些设置进行初始化(见图 10-1)。

场景/事件流程 这些是给定用例中发生的活动。

关联要求 关联要求提供对 DAQ 系统 SRS 的交叉引用。要求出现在以下部分(见“(选定的)DAQ 软件要求(来自 SRS)”第 219 页)。您必须在填写此部分之前创建要求;否则,您将只能猜测所需的要求。

image

图 10-1:读取 DIP 开关用例

10.6.1 启用/禁用调试模式

目标 启用和禁用 DAQ 系统中的调试输出。

前提条件 系统已启动。

结束条件 调试模式根据需要处于激活或非激活状态。

10.6.1.1 场景/事件流程

启用/禁用调试模式

  1. 在系统初始化过程中,读取 DIP 开关。

  2. 保存 DIP 开关 8 的值(on = 调试模式开启,off = 调试模式关闭)。

  3. 如果 DIP 开关 8 为on且 DIP 开关 2(USB 模式)为off,则启用调试模式。

  4. 启动maintPrintf任务。

10.6.1.2 关联要求

DAQ_SRS_721_001:PPDAQ 调试模式启用

DAQ_SRS_721_002:PPDAQ 调试模式禁用

10.6.2 启用/禁用以太网

目标 启用和禁用 DAQ 系统中的以太网命令处理。

前提条件 系统已启动。

结束条件 以太网通信处于活动或非活动状态,根据需要。如果处于活动状态,则以太网输入处理任务正在运行。

10.6.2.1 场景/事件流

启用/禁用以太网

1. 在系统初始化期间,读取 DIP 开关。

2. 使用 DIP 开关 3 的值来确定以太网是否启用(开关为on)或禁用(开关为off)。

3. 保存 DIP 开关 4 的值以确定系统是否支持一个连接(开关为off)或五个并发连接(开关为on)。

4. 使用 DIP 开关 5 和 6 的值来确定 IP 地址。

5. 如果以太网已启用(DIP 开关 3 为on),则:

5.1 根据 DIP 开关 5 和 6 的值设置以太网地址,如下所示:

5.1.1 192.168.2.70

5.1.2 192.168.2.71

5.1.3 192.168.2.72

5.1.4 192.168.2.73

5.2 使用优先级为 ETHL_PRIO 启动 ethernetListenTask 任务。

6. 否则(如果以太网未启用):

6.1 不要启动 ethernetListenTask

ethernetListenTask

1. 初始化一个包含五个描述符的数组,初始值为零(空描述符插槽)。

2. 等待在以太网套接字 0x5050 上的外部连接请求。

3. 如果发起了连接请求:

3.1 在描述符数组中搜索一个空插槽(包含零的数组元素)。

3.2 如果没有可用的插槽:

3.2.1 拒绝连接。

3.2.2 转到步骤 2。

3.3 如果有可用插槽:

3.3.1 接受连接并将其文件描述符存储在可用插槽中。

3.3.2 创建与新连接关联的新的以太网命令任务;新任务的优先级应为 ETH1_PRIOETH5_PRIO,通过描述符插槽数组的索引选择;注意 SER_PRIO < ETHL_PRIO < ETH1_PRIOETH5_PRIO < USB_PRIO(较小的数字意味着任务在任务队列中的优先级更高)。

3.3.3 转到步骤 2。

4. 如果监听连接断开,终止监听任务。

10.6.2.2 关联要求

DAQ_SRS_708_000: PPDAQ 以太网 IP 地址

DAQ_SRS_709_000: PPDAQ 以太网 IP 地址 192.168.2.70

DAQ_SRS_710_000: PPDAQ 以太网 IP 地址 192.168.2.71

DAQ_SRS_711_000: PPDAQ 以太网 IP 地址 192.168.2.72

DAQ_SRS_712_000: PPDAQ 以太网 IP 地址 192.168.2.73

DAQ_SRS_716_000: PPDAQ 以太网启用

DAQ_SRS_716.5_000: PDAQ 以太网禁用

DAQ_SRS_716_001: PPDAQ 以太网任务

DAQ_SRS_716_002: PPDAQ 以太网任务优先级

DAQ_SRS_717_000: PPDAQ 以太网端口

DAQ_SRS_718_000: PPDAQ 以太网多客户端启用

DAQ_SRS_718_001: PPDAQ 以太网多客户端禁用

DAQ_SRS_728_000: PPDAQ 命令源 #3

DAQ_SRS_737_000: PPDAQ 最大以太网连接 #1

DAQ_SRS_738_000: PPDAQ 最大以太网连接 #2

DAQ_SRS_738_001: PPDAQ 以太网命令处理任务

DAQ_SRS_738_002: PPDAQ 以太网命令任务优先级

10.6.3 启用/禁用 RS-232

(类似于前面的使用案例;已删除以简化内容。)

10.6.4 启用/禁用测试模式

(类似于前面的使用案例;已删除以简化内容。)

10.6.5 启用/禁用 USB

(与前述用例类似;已删除以简化内容。)

10.6.6 读取 DIP 开关

(与前述用例类似;已删除以简化内容。)

10.7 从用例创建 DAQ 软件需求

将一个非正式的用例转化为正式需求的过程包括从用例中提取信息、补充缺失的细节,并将结果构建成需求的形式。

考虑“启用/禁用调试模式”的用例。你可能会觉得这个用例生成了一个单一的需求:

如果 Netburner 的 DIP 开关 8 设置为开启位置且 USB(DIP 开关 2)未启用,则 PPDAQ 软件应在特殊调试模式下运行;如果开关 8 在关闭位置或 DIP 开关 2 已启用,则应在非调试模式下运行。

问题在于,这实际上是两个独立的需求——不是因为“和”与“或”组件(你稍后会明白为什么),而是因为分号将两个从句分开。两个独立的需求是:

如果 Netburner 的 DIP 开关 8 设置为开启位置且 USB(DIP 开关 2)未启用,则 PPDAQ 软件应在特殊调试模式下运行。

如果开关 8 在关闭位置或 DIP 开关 2 已启用,则 PPDAQ 软件应在非调试模式下运行。

请注意,“和 USB”和“或 DIP 开关 2”并不意味着这些需求必须拆分成两个独立的需求。句子“如果 Netburner 的 DIP 开关 8 设置为开启位置且 USB(DIP 开关 2)未启用”实际上是该需求的触发条件的一部分。从技术角度来看,该需求可能需要重新措辞。

如果 Netburner 的 DIP 开关 8 设置为开启位置且 USB(DIP 开关 2)未启用,则 PPDAQ 软件应在特殊调试模式下运行。

这将触发条件移至需求的开头,正如在章节“建议的需求格式”中所建议的,在第 186 页中提到的那样。需要注意的是,这只是一个建议的格式;将触发条件放在演员(PPDAQ 软件)、动作(运行)和对象(调试模式)之后也是合理的。

下一节列出了 DAQ 软件系统的各种需求。它提供了一个示例,说明了如何从用例中生成 DAQ 需求。你应该能够自行填写剩余需求的详细信息。

10.8(选择的)DAQ 软件需求(来自 SRS)

实际的 DAQ SRS(而不是《示例软件需求规格》中的 POOL_SRS,见 第 203 页)包含数百个需求;为了保持本章篇幅合理,我选择了以下需求,它们代表了支持先前所示 DIP 开关使用案例所需的需求。请注意,这些 SRS 需求的标签形式为 [DAQ_SRS_xxx_yyy],因为实际的 DAQ 系统需求既有 SyRS 也有 SRS。

注意

DAQ SRS 文档将所有需求放在第三部分,就像所有 SRS 一样。因此,以下章节编号恢复为 3,而不是继续本章的段落编号。

3.1.1.1 PPDAQ 标准软件平台

3.1.1.15 PPDAQ 以太网 IP 地址

[DAQ_SRS_708_000]

PPDAQ 软件应根据 Netburner 上 DIP 开关 5–6 的设置,将以太网 IP 地址设置为 192.168.2.70 到 192.168.2.73 范围内的某个值。

3.1.1.16 PPDAQ 以太网 IP 地址 192.168.2.70

[DAQ_SRS_709_000]

如果 Netburner DIP 开关 5–6 设置为 (OFF, OFF),则 PPDAQ 软件应将以太网 IP 地址设置为 192.168.2.70。

3.1.1.17 PPDAQ 以太网 IP 地址 192.168.2.71

[DAQ_SRS_710_000]

如果 Netburner DIP 开关 5–6 设置为 (ON, OFF),则 PPDAQ 软件应将以太网 IP 地址设置为 192.168.2.71。

3.1.1.18 PPDAQ 以太网 IP 地址 192.168.2.72

[DAQ_SRS_711_000]

如果 Netburner DIP 开关 5–6 设置为 (OFF, ON),则 PPDAQ 软件应将以太网 IP 地址设置为 192.168.2.72。

3.1.1.19 PPDAQ 以太网 IP 地址 192.168.2.73

[DAQ_SRS_712_000]

如果 Netburner DIP 开关 5–6 设置为 (ON, ON),则 PPDAQ 软件应将以太网 IP 地址设置为 192.168.2.73。

3.1.1.20 PPDAQ 以太网启用

[DAQ_SRS_716_000]

如果 Netburner DIP 开关 3 设置为 ON 位置,则 PPDAQ 软件应启用以太网操作。

3.1.1.21 PPDAQ 以太网禁用

[DAQ_SRS_716.5_000]

如果 Netburner DIP 开关 3 设置为 OFF 位置,则 PPDAQ 软件应禁用以太网操作。

3.1.1.22 PPDAQ 以太网任务

[DAQ_SRS_716_001]

如果启用了以太网通信,则应启动以太网监听任务。

3.1.1.23 PPDAQ 以太网任务优先级

[DAQ_SRS_716_002]

以太网监听任务的优先级应低于 USB 任务,但高于串口任务。

3.1.1.24 PPDAQ 以太网端口

[DAQ_SRS_717_000]

PPDAQ 软件应通过以太网使用套接字端口 0x5050(十进制 20560,ASCII PP,表示 Plantation Productions)进行通信。

3.1.1.25 PPDAQ 以太网多个客户端启用

[DAQ_SRS_718_000]

如果 Netburner DIP 开关 4 设置为 ON 位置,则 PPDAQ 软件应允许最多五个以太网客户端。

3.1.1.26 PPDAQ 以太网多个客户端禁用

[DAQ_SRS_718_001]

如果 Netburner DIP 开关 4 设置为 OFF 位置,则 PPDAQ 软件应只允许单个以太网客户端。

3.1.1.29 PPDAQ 单元测试模式 I/O

[DAQ_SRS_721_000]

除非启用了 USB 命令(USB 命令与测试模式输出共享相同的串口 [UART0]),否则 PPDAQ 软件将利用 Netburner MOD54415 MOD-70 评估板上的 UART0 串口进行单元测试通信。

3.1.1.30 PPDAQ 调试模式启用

[DAQ_SRS_721_001]

如果 Netburner DIP 开关 8 设置为开启且未启用 USB(DIP 开关 2),则 PPDAQ 软件将以特殊的调试模式运行。

3.1.1.31 PPDAQ 调试模式禁用

[DAQ_SRS_721_002]

如果 Netburner DIP 开关 8 设置为关闭位置,则 PPDAQ 软件将以正常(非调试)模式运行。

3.1.1.38 PPDAQ 命令源 #3

[DAQ_SRS_728_000]

如果启用了以太网通信,则 PPDAQ 软件将接受来自 Netburner MOD54415 MOD-70 评估板上以太网端口的命令。

3.1.1.40 PPDAQ 最大以太网连接 #1

[DAQ_SRS_737_000]

如果 Netburner DIP 开关 4 处于关闭位置,则 PPDAQ 软件仅会识别以太网端口上的单个连接。

3.1.1.41 PPDAQ 最大以太网连接 #2

[DAQ_SRS_738_000]

如果 Netburner DIP 开关 4 处于开启位置,则 PPDAQ 软件最多会识别以太网端口上的五个连接。

3.1.1.42 PPDAQ 以太网命令处理任务

[DAQ_SRS_738_001]

PPDAQ 软件应为每个连接启动一个新进程来处理命令。

3.1.1.43 PPDAQ 以太网命令任务优先级

[DAQ_SRS_738_002]

每个 PPDAQ 命令处理任务应具有不同的优先级,该优先级高于以太网监听任务的优先级,并低于 USB 命令任务的优先级。

10.9 更新带需求信息的可追溯性矩阵

SyRS 和 SRS 的需求通常会为 RTM 添加四到六列:描述、SyRS 标签(如果有 SyRS)、分配、SRS 标签以及测试/验证类型。描述列提供了需求的简要说明,例如上一节中的需求 DAQ_SRS_700_000 中的PPDAQ 标准软件平台。(请注意,这并不指代在“软件需求规格示例”中在第 203 页呈现的 POOL_SRS 标签。)

SyRS 和 SRS 标签列包含实际的 SyRS(如果存在)和 SRS 标签标识符。通常,您会根据 SyRS(主键)对 RTM 中的行进行排序,然后再按 SRS(次键)排序,除非没有 SyRS 标签,在这种情况下,您只需按 SRS 标签对行进行排序。

分配栏指定了该要求是硬件(H)、软件(S)、其他(O)还是它们的组合。通常,只有 SyRS 要求才会有仅限硬件的分配;毕竟,SRS 要求是软件要求。然而,如果 SRS 要求涉及系统的软硬件两个方面,它也可能有HS分配。其他类别是一个通用分类,涵盖那些不清楚属于硬件或软件的要求(例如,这可能描述了一个手动过程)。

请注意,如果你没有 SyRS,或者你所有的需求分配都是软件分配,你可以删除分配栏;这可以帮助减少 RTM 的大小和复杂性。

RTM 中的验证类型栏指定了你将在系统中如何验证(测试)此要求。可能的条目包括:通过测试T);通过审查R);通过检查I;硬件设计的“通过审查”变体);通过设计D;通常适用于硬件,而非软件);通过分析A);其他O);以及不进行测试,或无法测试N)。

很明显,具有T验证方法的要求将会有一个关联的测试来验证该要求。这通常意味着你将为此要求准备一个相应的测试用例,并有一个测试程序来执行它。

测试某些要求可能很困难、不切实际,或者存在危险。^(5) 在这些情况下,仔细审查代码以验证它是否能正常运行可能会更加容易。对于这样的要求,验证方法将是R,通过审查。

通过分析A)验证方法意味着你在某个地方提供了一个正式的(数学)证明,表明软件符合正式要求。这是一个比通过审查更加严格的过程,且远超本书的范围。然而,对于某些可能导致灾难性事件(如死亡)的要求,这种类型的验证可能是必需的。请参阅来自“(选定的)DAQ 软件要求(来自 SRS)”的第一个要求,见第 219 页:

[DAQ_SRS_700_000]

PPDAQ 软件应在连接到 DAQ_IF 接口板的 Netburner MOD54415 MOD-70 评估板上运行。

要提出一个实际的测试来证明这个要求被满足可能有些困难(除了将软件安装在 Netburner MOD54415 上并验证它是否能够运行)。另一方面,通过查看源代码(和构建文件)并验证这些代码是否是为 Netburner MOD54415 编写的几乎是微不足道的。一种审查测试无疑是处理这个特定要求的最合适方法。

其他验证方法是一个包容性类别,意味着你将提供文档来证明无测试方法或你计划使用的验证方法。

无测试无法测试的验证要求你解释为什么不需要测试。如果你指定N表示无法测试,你应当仔细考虑这一要求是否有效(是否为实际要求)。记住,如果无法测试,它就不是一个要求。

这些是[DAQ_SRS_700_000]将添加到 RTM 中的四列条目。

描述 SRS 标签 分配 验证
PPDAQ 标准软件平台 DAQ_SRS_700_000 HS R

根据“(选择的)DAQ 软件需求(来自 SRS)”在第 219 页的要求,我们可以将需求分为两类:一种是需要通过测试来验证的,另一种是需要通过审查来验证的(因为实际的测试可能难以执行或不便创建)。

10.9.1 需要通过审查验证的需求

表 10-2 列出了来自“(选择的)DAQ 软件需求(来自 SRS)”在第 219 页的需求,这些需求应该通过审查来验证,并且应该提供做出选择的理由。^(6)

表 10-2: DAQ 软件需求验证理由

需求 理由
DAQ_SRS_700_000 虽然你可以争辩说,在 Netburner 上运行软件可以验证它是否在 Netburner 上运行,但审查 make/build 文件是一种更简单且更实际的方式来验证这一要求。
DAQ_SRS_700_000.01 虽然你可以争辩说,在μC/OS 上运行软件可以验证它是否在μC/OS 下运行,但审查 make/build 文件是一种更简单且更实际的方式来验证这一要求。
DAQ_SRS_702_001 编写一个测试来证明一个独立的进程正在运行是困难的,除非实际更改代码(即打印一些输出以证明这一点)。然而,查看代码以确认它启动了一个新的任务来处理 RS-232 通信并不困难。
DAQ_SRS_702_002 编写一个测试来证明 RS-232 进程正在特定优先级级别运行需要修改代码;而审查代码更为简便。
DAQ_SRS_703_001 让这一项通过审查来验证是有争议的。你可以辩称,如果系统接受 RS-232 命令,任务就运行了。然而,这并不能证明是否有独立任务在运行,或者是否没有(主任务可能在处理命令)。因此,这项应该通过审查来验证。
DAQ_SRS_705_001 与 DAQ_SRS_702_001 相同的论点(只不过应用于 USB 输入任务)。
DAQ_SRS_705_002 与 DAQ_SRS_702_002 相同的理由。
DAQ_SRS_706_001 与 DAQ_SRS_705_001 相同的论点(仅为该要求的补充)。
DAQ_SRS_716_001 与 DAQ_SRS_702_001 相同的论点(仅应用于以太网监听任务)。
DAQ_SRS_716_002 与 DAQ_SRS_702_002 相同的论点(仅应用于以太网监听任务的优先级)。
DAQ_SRS_719_000 当前 DAQ 系统中的单元测试模式尚未定义,因此无法测试系统是否已进入此模式。通过审查代码,可以验证内部变量是否已正确设置(DIP 开关的唯一影响)。
DAQ_SRS_720_000 参见 DAQ_SRS_719_000。
DAQ_SRS_723_000 另一个有争议的情况。系统读取 DIP 开关(用于处理其他测试)的事实应该足以表明软件正在读取 Netburner 开关。然而,这个要求的重要性不足,以至于选择的审查/测试方式并不重要。
DAQ_SRS_723_000.01 参见 DAQ_SRS_723_000。
DAQ_SRS_723_000.02 参见 DAQ_SRS_723_000。
DAQ_SRS_725_000 检查 DAQ 是否响应命令并不复杂(容易测试);然而,要求中指出 DAQ 不能主动发起通信(即它是以否定方式表述的,通常来说,这种表述在要求中是不好的)。审查代码是处理负面要求的唯一正确方式(这也是为什么我们要避免负面要求的原因)。
DAQ_SRS_738_001 与 DAQ_SRS_702_001 有类似的理由。
DAQ_SRS_738_002 与 DAQ_SRS_702_002 有类似的理由。

10.9.2 需要通过测试验证的要求

所有在“(选定的)DAQ 软件需求(来自 SRS)”中列出的要求,且在“通过审查验证的要求”中未列出,将通过测试用例和测试程序进行验证。第 219 页。 |

10.10 更多信息

IEEE. “IEEE 标准 830-1998: IEEE 推荐的软件需求规范实践。”1998 年 10 月 20 日。doi.org/10.1109/IEEESTD.1998.88286. |

Leffingwell, Dean, 和 Don Widrig. 管理软件需求. 波士顿: Addison-Wesley Professional, 2003. |

Wiegers, Karl E. 软件需求. Redmond, WA: Microsoft Press, 2009. |

———. “编写高质量需求。” 软件开发 7,第 5 期(1999 年 5 月):44–48。 |

第十三章:软件设计说明文档

Image

软件设计说明书(SDD)文档提供了软件设计的低级实现细节。虽然它不一定深入到实际代码的层面,但它确实提供了软件实现的算法、数据结构和低级流程控制。

关于如何记录软件设计,存在很多不同的观点。本章遵循 IEEE 标准(Std)1016-2009^(1)提出的指南,并使用该标准中描述的许多概念。

IEEE Std 1016-2009 旨在做到语言独立。然而,统一建模语言(UML)几乎涵盖了该标准的所有要求,这也是第四章介绍 UML 的原因,并且我们将在本章中使用它。如果你对其他可用的软件设计建模语言感兴趣,可以在 IEEE Std 1016-2009 文档中查阅它们的描述。

11.1 IEEE Std 1016-1998 与 IEEE Std 1016-2009 的比较

原始的 IEEE SDD 指南于 1998 年定稿,基于 20 世纪 80 年代和 90 年代盛行的结构化编程软件工程概念。这些建议发布时,正值面向对象编程革命的兴起,因此很快就过时了。更新花费了 10 年时间,但修订版 Std 1016-2009 涵盖了面向对象的分析和设计。新指南保留了 1016-1998 标准的特性,但以某种程度上被弃用的形式呈现。不过,值得注意的是,其中一些特性在现代设计中依然有用,因此如果这些特性在你的上下文中适用,仍然没有理由忽视旧标准。

11.2 IEEE 1016-2009 概念模型

SDD 并不是孤立存在的。SDD 中的内容自然来源于软件需求规格说明书(SRS),而逆向可追溯矩阵(RTM)将这两份文档联系起来。图 11-1 展示了这种关系。

image

图 11-1:SRS 与 SDD 的关系

11.2.1 设计问题和设计利益相关者

SRS 中的每个需求最终都与 SDD 中的设计问题相关(参见图 11-2)。设计问题是指对系统设计中的任何利益相关者而言有意义的内容。利益相关者是指在系统设计中有发言权的任何人。需求是指 SRS 中的任何单独需求,详见第十章。

image

图 11-2:需求映射到设计问题

图 11-2 将需求映射到设计问题,具体如下:

0..* 每个需求有零个或多个相关的设计问题。

1..* 单一设计问题对一个或多个设计利益相关者来说是重要的。

1...* 每个利益相关者至少有一个(并可能有多个)设计问题。

IEEE 概念模型指出,需求会引发零个或多个设计关注点。但实际上,需求和设计关注点应该具有一对一关系:每个设计关注点都有一个相关的需求。如果一个需求没有引发任何设计关注点——也就是说,需求对软件设计没有影响——那么这个需求可能不是必要的(因此,也就不是有效的需求)。如果一个需求映射到多个设计关注点,这通常意味着你有一个复合需求,应该将其拆解为原子需求,在你的 SRS 中进行处理(见 “原子” 在 第 190 页)。

利益相关者和设计关注点应该具有多对多关系。一个利益相关者可以(通常会)有多个设计关注点。同样,一个设计关注点也可以(通常会)由多个不同的利益相关者共享。

11.2.2 设计视角和设计元素

最终,设计关注点(或仅需求)是 SDD 的接口点。一个 设计视角 逻辑上将一个或多个设计关注点分组在一起。例如,逻辑视角(参见 “逻辑视角” 在 第 235 页)描述设计中的静态数据结构,因此所有与类和数据对象相关的需求都将与该视角相关联。算法视角(参见 “算法视角” 在 第 239 页)描述设计中使用的某些算法,因此任何指定使用某些算法的需求(这应该是少数情况)将与该视角相关联。

IEEE Std 1016-2009 要求通过以下方式指定每个设计视角:

  • 视角名称

  • 与视角相关的设计关注点

  • 视角使用的设计元素(设计实体类型、属性和约束)列表

  • 讨论基于视角构建设计视图时所使用的分析方法

  • 解释和评估设计的标准

  • 作者姓名或用于该视角的参考资料

图 11-3 显示了设计关注点和设计视角之间的关系。多重性项 1..* 表示一个视角框定(或分组)一个或多个需求。

image

图 11-3:将设计关注点映射到设计视角

设计关注点和设计视角之间具有根本的多对一关系,这提供了 SDD 和 SRS 之间的可追溯性。在 RTM 中,每个需求(设计关注点)将链接到一个设计视角。因此,通常你会将 SDD 标签附加到设计视角上(或者,正如你稍后会看到的,你也可以将标签附加到设计视图上,因为设计视图和设计视角之间有一对一关系)。

设计视角定义了一组设计元素(见图 11-4),其中的示例包括类图、时序图、状态图、包、用例和活动图。

image

图 11-4:设计视角与设计元素的映射

设计元素是指你在设计视图中放置的任何东西,包括设计实体、属性、关系和约束:

  • 设计实体是描述设计主要组件的对象。示例包括系统、子系统、库、框架、模式、模板、组件、类、结构、类型、数据存储、模块、程序单元、程序、线程和进程。IEEE Std 1016-2009 要求每个设计实体在 SDD 中必须具有名称和目的。

  • 设计元素有相关的属性:名称、类型、目的和作者。在你的 SDD 视角中列出设计元素时,必须提供这些属性。

  • 设计关系有一个关联的名称和类型。IEEE Std 1016-2009 并没有预定义任何关系;然而,UML 2.0 定义了几种关系——如关联、聚合、依赖和泛化——你通常会在你的 SDD 中使用这些关系。根据 IEEE 的要求,你必须在设计视角规范中描述你所使用的所有关系。

  • 设计约束是一个元素(元素),它对设计视图中的另一个设计元素(目标元素)施加限制或规则。IEEE 要求你在定义它们的视角中列出所有设计约束的名称和类型(以及源/目标元素)。

你使用正式设计语言定义设计元素(见图 11-5)。如前所述,IEEE Std 1016-2009 尝试保持语言中立,但实际上它是专门围绕 UML 设计的。IEEE 建议的其他(正式的)设计语言包括 IDEFO、IDEF1X 和维也纳定义方法。然而,对于本书来说,你可能最好使用 UML。

image

图 11-5:设计视角、元素和语言之间的关系

IEEE Std 1016-2009 定义了一组常见的设计视角。由于该标准是一组推荐的实践,而非绝对要求,以下列出的视角既不全面也不是必需的。也就是说,在你的 SDD 中,你可以根据需要定义并添加更多的视角,而且不需要包括所有视角(实际上,其中一些视角已经被废弃,仅为与旧版 IEEE Std 1016-1998 兼容而包含)。

11.2.2.1 上下文视角

上下文视角收集需求的设计元素包括参与者(用户、外部系统、利益相关者)、系统提供的服务以及它们的交互(如输入和输出)。上下文视角还管理各种设计约束,如服务质量、可靠性和性能。从某种意义上讲,你在制定 SRS 需求时就开始了这项工作(例如,在创建用例来推动需求时),并在开发 SDD 时完成这项工作。

上下文视角的主要目的是设定系统边界,并定义系统内外的考虑因素。这限制了设计的范围,使得设计师和 SDD 的编写者可以集中精力于系统设计,而不会浪费时间考虑外部因素。

你通常会在 UML 用例图中表示上下文视角(见“用例”,第 214 页)。一个很好的例子是回到图 10-1,该图列出了用户通过数据采集(DAQ)系统的 DIP 开关可以设置的初始化。另一个例子是图 11-6,它展示了主机系统(通常是 PC)与 DAQ CPU 接口板之间的 DAQ 命令的简化用例集。

image

图 11-6:DAQ 命令用例

该图展示了外部系统(主机角色)与 DAQ 系统之间的命令接口。请注意,每个用例——在这个例子中有 16 个——对应着 DAQ SRS 中的需求。^(2)

11.2.2.2 组成视角

组成视角列出了构成系统的主要模块/组件。此视角的主要目标之一是通过在设计中识别那些可以来自现有库或可以在系统中重用的专有设计项目,促进代码重用。

组成视角中的设计实体包括——举几个例子——组成(显然是)、包含、使用和泛化。组成视角通过实现、依赖、聚合、组成和泛化来描述设计实体之间的关系,以及对象之间的任何其他关系。

请注意,这是一种较旧的视角,源自 IEEE 标准 1016-1998。^(3) 大多数情况下,它已被结构视角所取代(见“结构视角”,第 237 页),其次是逻辑视角(见下一节)。组成视角来源于程序主要由过程和函数组成的时代,那时程序库已经组织起来,而这远早于面向对象分析与设计的出现。

现代设计如果包含组成视角,通常将其用来描述系统的主要组件,正如 IEEE Std 1016-2009 所推荐的那样。图 11-7 提供了一个关于 DAQ 系统的组成视角示例,使用简化的组件图。在我看来,组件图并不适合组成视角图——它们过于低级,无法完成此任务。组件图通常包含接口(必需的和提供的),这些接口在组成视角级别上没有意义。然而,显然由于 组成组件 这两个词的相似性,使用简化的 UML 组件图来表示组成视角非常常见。

image

图 11-7:组成视角图

一些工程师使用组件图和部署图的组合(请参见 “部署图” 在 第 159 页)来说明组成视角,如 图 11-8 所示。

image

图 11-8:部署/组件图

请注意,这个图中的节点仍然包括组件符号,表明它们是构成更大系统的组件,而非硬件项。这是 UML 的非标准图示方法,但我在几个示例 SDD 中见过,所以我在这里也包括了它。

11.2.2.3 逻辑视角

逻辑视角描述了设计中使用的预先存在的和新类型,以及它们的类、接口/协议和结构定义。逻辑视角还描述了设计中使用的对象(类型的实例)。

逻辑视角处理类、接口、数据类型、对象、属性、方法、函数、过程(子程序)、模板、宏和命名空间。它还为这些设计实体分配属性——如名称、可见性类型和数值——并附加适当的约束。

通常,您使用 UML 类图来实现逻辑视角。图 11-9 显示了一个 adcClass_t 类的类图,该类可能适用于 图 11-8 中的模拟输入模块。除了这个基本的类图外,您可能还想包括一个 数据字典,或者描述此类所有属性目的的文本。

image

图 11-9:adc 类图

除了基本的类图外,逻辑视角还应包括类之间的关系(如依赖、关联、聚合、组合和继承)。有关这些类关系以及如何将其图示化的更多细节,请参见 “UML 类关系” 在 第 114 页。

11.2.2.4 依赖视角

与组合视角类似,依赖视角是为了与 IEEE Std 1016-1998 兼容而保留下来的已弃用视角;在现代设计中,您通常不会使用此视角,因为其他选项(如逻辑视角和资源视角)可以更合乎逻辑地映射依赖关系。然而,您在适当的情况下使用依赖视角并不受限制,而且很可能会在 SDD 中遇到它们,因此您应该了解它们。

在 SDD 中,依赖视角展示了设计实体之间的关系和互联,包括共享信息、接口参数化和执行顺序,使用如使用提供要求等术语。依赖视角适用于子系统、组件、模块和资源。IEEE Std 1016-2009 建议使用 UML 组件图和包图来描述这一视角。如果您想采用组件图来展示组件或子系统之间的依赖关系(如 图 11-8 所示),使用组合部署/组件图可能是一个不错的选择。如果您描述的是包之间的依赖关系,使用包图则是个好主意,如图 11-10 所示。

image

图 11-10:包依赖关系

11.2.2.5 信息/数据库视角

信息/数据库视角描述了设计中持久数据的使用。它类似于逻辑视角,因为您使用类图来展示数据结构、内容和元数据定义。信息视角还会描述数据访问方案、数据管理策略和数据存储机制。

这是一个已弃用的项,包含在内以保持与 IEEE Std 1016-1998 的兼容性。在现代设计中,您可能会使用逻辑视角或可能的资源视角来代替它。

11.2.2.6 模式使用视角

模式使用视角描绘了项目中使用的设计模式——以及从中实现的可重用组件。有关设计模式的更多信息,请参见更多信息,详见第 260 页。

模式使用视角图采用了 UML 复合结构、类图和包图的组合,同时结合关联、协作使用和连接器来指示从模式中生成的对象。这个视角设计较为宽松,因此如果您选择在 SDD 中使用它,您在创建时将拥有很大的自由度。

11.2.2.7 接口视角

接口视角描述了设计所提供的服务(例如,API)。具体来说,它包括对在 SRS 中没有要求的接口的描述,包括与第三方库、项目的其他部分或同一组织内其他项目的接口。这是其他程序员在与接口视角覆盖的设计部分交互时可以使用的路线图。

IEEE Std 1016-2009 推荐使用 UML 组件图来表示接口视角。图 11-11 展示了两个组件(可能在 DAQ 系统中),它们处理数字 I/O 和继电器输出(数字输出的一种特定形式)。

image

图 11-11:接口视角示例

除了组件图外,接口视角还应包括关于系统如何与这些接口交互的描述,包括数据类型、函数调用、延迟、输入的限制、输出的范围以及其他重要问题。例如,在讨论 Direction 接口时,你可能会包括如下信息:

Direction

Direction(ddir:int, port:int)

调用 Direction 会将指定的数字 I/O 端口(port = 0..95)设置为输入端口(如果 ddir = 0)或输出端口(如果 ddir = 1)。

对于 Read,你可以使用如下描述:

Read

Read(port:int):int

调用 Read 会返回指定数字输入端口(port = 0..95)的当前值(01)。

同样,接口视角在 IEEE Std 1016-2009 中仅作为与旧版 IEEE Std 1016-1998 的兼容性存在。在现代的 SDD 中,考虑将接口项放置在上下文视角和结构视角中。

11.2.2.8 结构视角

结构视角描述了设计中对象的内部组织和构造。它是组成视角的现代版本,后者描述了设计是如何(递归地)分解成各个部分的。你可以使用结构视角将较大的对象拆解为更小的组件,目的是确定如何在设计中重用这些较小的组件。

结构视角通常使用的图示方法包括 UML 复合结构图、UML 包图和 UML 类图。这些图示在 图 11-12、图 11-13 和 图 11-14 中分别展示了游泳池监控器(SPM)的示例。

image

图 11-12:SPM 复合结构图

image

图 11-13:SPM 包图

image

图 11-14:SPM 类图

这些示例说明,通常你会在一个给定的视角中拥有多个图示。同时注意,典型的结构视角将拥有多个复合结构图,(可能)多个包图,以及(肯定)多个类图。

11.2.2.9 交互视角

交互视角是定义软件中发生活动的主要地方。在这里,你会放置大多数交互图—活动图、时序图、协作图等—可能唯一的例外是状态图,因为它们通常出现在状态动态视角中(将在下一部分讨论)。除了交互图外,你还可能在交互视角中使用复合结构图和包图。

交互视角的完整示例出现在《一个样本 SDD》的第 247 页。

11.2.2.10 状态动态视角

状态动态视角描述软件系统的内部操作状态。对于这个视角,通常使用 UML 状态图(参见“状态图”在第 163 页)。

11.2.2.11 算法视角

算法视角是从 IEEE 1016-1998 中延续下来的另一个旧视角。它的目的是描述系统中使用的算法(通常通过流程图、Warnier/Orr 图、伪代码等形式)。这个视角在 Std 1016-2009 文档中基本上被交互视角所替代。

11.2.2.12 资源视角

资源视角描述设计如何使用各种系统资源。这包括 CPU 使用(包括多核使用)、内存使用、存储、外设使用、共享库以及与设计相关的其他安全、性能和成本问题。通常,资源是外部于设计的实体。

这是另一个为兼容性原因包含在 Std 1016-2009 中的 Std 1016-1998 项目。在新的设计中,通常使用上下文视角来描述资源使用情况。

11.2.3 设计视图、叠加层与推理

IEEE Std 1016-2009 规定,SDD 被组织成一个或多个设计视图。因此,设计视图是 SDD 中的基本组织单元。设计视图提供(可能)多个系统设计的视角,帮助相关方、设计师和程序员澄清设计如何满足与关联设计视角指定的需求。

当 SDD 完整时,它会覆盖每个需求(设计关注点),并且在至少一个设计视图中有体现,涵盖所有相关设计视角中的实体和关系,并且符合所有设计约束。通俗来说,这意味着你已经将所有需求与适当的图示和文本讨论对齐,如第 229 页的《设计视角与设计元素》部分所述。

如果设计视图中的任何元素之间没有冲突,那么一个 SDD 就是一致的。例如,如果类图表明一个名为hasValue的属性是布尔型,但活动图将该字段视为字符串,那么就存在不一致。

11.2.3.1 设计视图与设计视点的区别

设计视图和设计视点之间存在一一对应的关系,如图 11-15 所示。关联链接表明,设计视图恰好符合一个设计视点,而设计视点恰好由一个设计视图所控制。

image

图 11-15:设计视图和设计视点

那么,设计视图和设计视点之间有什么区别呢?设计视图是你通常认为是“设计”的实际信息(图形和文本)。设计视点是你创建设计时所采用的视角。在 IEEE 的建议中,设计视点可能是类似于上下文视点或交互视点的内容。这些并非实际的设计视图,而是用于展示视图的格式。就 SDD 的组织而言,目录中的视图/视点部分可能会像以下这样组织:^(4)

1 视点 #1

1.1 视点 #1 规格(见“设计视点与设计元素”第 229 页)

1.2 视图 #1

2 视点 #2

2.1 视点 #2 规格

2.2 视图 #2

3 视点 #3

3.1 视点 #3 规格

3.2 视图 #3

4 等等

按照视点组织视图的原因很简单:视点代表了不同利益相关者的观点,因此这种组织方式可以让利益相关者快速找到 SDD 中与他们相关的部分,而无需阅读全文。

请注意,本大纲中的每个视图不一定对应一个单一的图示或文本描述。一个视图可能由多个独立的 UML 图和中间的文本描述组成。例如,在逻辑视点下,你可能会有多个不同的类图(不仅仅是一个),如果没有其他原因,仅仅因为将多个类合并到一个图中是困难的。即使你能做到,也可能希望逻辑地组织类图,使其更容易阅读。此外,除了类图本身,你还需要提供一些描述这些类成员(属性)的文本。与其先展示一个庞大的类图(可能需要几十页),然后紧跟着非常长的文本描述(又可能占据额外几十页),不如将几个类图放在同一张图中,紧随其后是关于属性的文本信息,然后对剩余需要记录的类执行同样的操作。

11.2.3.2 设计叠加层

设计覆盖层是视角的“逃生条款”。设计视角符合设计覆盖层,或者相反,设计覆盖层支配设计视角,如图 11-16 所示。因此,如果你已经创建了一个逻辑视角,并且希望在该视角中加入一些交互图示以便更好地说明,你会使用设计覆盖层。

设计覆盖层修改视角/视点的组织方式如下:

视角 #1

1.1 视点 #1 规格

1.2 视角 #1

1.3 覆盖层 #1

1.4 覆盖层 #2

1.5 等等

2 等等

image

图 11-16:设计视角/覆盖层/理由关系

设计覆盖层必须被标识为设计覆盖层(以避免与关联视点混淆),并且必须具有唯一的名称,并且仅与一个视点相关联。

设计覆盖层的一个好处是,它可以让你混合搭配设计语言,或在现有设计语言不足以满足需求时进行扩展。设计覆盖层还允许你扩展现有的视角,而不必创建一个全新的视点(这可能需要大量额外工作)。

11.2.3.3 设计理由

设计理由解释了设计背后的目的,并向其他查看者证明设计的合理性。通常,设计理由由设计过程中的评论和注释组成。它可能涉及(但肯定不限于)关于设计的潜在问题、在设计过程中考虑的不同选项和权衡、为什么做出某些决策的论证与理由,甚至是原型或开发阶段进行的变更(因为原始设计未能实现)。图 11-16 显示了设计理由与设计视角之间的关系(聚合符号表示设计理由评论被包含在设计视角中,或是设计视角的一部分)。

11.2.4 IEEE Std 1016-2009 概念模型

图 11-17 和图 11-18 根据 IEEE Std 1016-2009 提供了 SDD 和设计元素的概念模型图。^(5)

image

图 11-17:SDD 概念模型

image

图 11-18:SDD 设计元素概念模型

11.3 SDD 必需内容

一个 SDD 必须包含以下内容(根据 IEEE Std 1016-2009):

  • 一个 SDD 标识

  • 设计利益相关者列表

  • 设计关注点(从产品需求中发展而来)

  • 一组一个或多个设计视点(请注意,每个设计视角在 SDD 中都有一个对应的设计视点)

  • 一组一个或多个设计视角(大致对应不同类型的 UML 图表,尽管设计视角不一定与特定的 UML 图表类型绑定)

  • 所有必要的设计覆盖层

  • 任何必要的设计理由(IEEE 要求至少提供目的)

11.3.1 SDD 标识

至少,SDD 应包括以下识别信息(不一定按照此顺序):

  • 创建日期/发行日期

  • 当前状态

  • 目的/范围

  • 发行组织

  • 作者(包括版权信息)

  • 参考文献

  • 上下文

  • 用于设计视角的设计语言描述

  • 正文

  • 摘要

  • 词汇表

  • 变更历史

大部分信息是模板内容(除了日期外,您通常从组织的通用 SDD 模板中复制这些信息)。显然,某些信息会随着不同的 SDD 而发生变化(如日期、作者和变更历史),但大多数情况下,SDD 标识中几乎不涉及多少知识性活动。它的存在主要是为了使 SDD 成为一个独立的文档。

11.3.2 设计利益相关者及其设计关注点

SDD 必须列出所有为项目贡献需求/设计关注点的人员。这些内容至关重要:如果关于设计合理性的问题在 SDD 中未得到解决,读者应能够确定联系哪位利益相关者,以便了解设计关注点。

11.3.3 设计视图、视角、叠加层和合理性

设计视图、视角、叠加层和合理性构成了 SDD 的主体部分。

11.4 SDD 的可追溯性与标签

我们尚未讨论如何通过 RTM 将 SDD 中的设计元素追溯到 SRS 和其他系统文档(参见“可追溯性”在第 171 页)。如第九章所述,您可以使用标签来追踪文档中设计元素的来源。对于 SDD,您使用的标签形式是projSDDxxx,其中proj是某个项目特定的名称或助记符,而xxx是一个数字(可能是十进制)值(参见“SDD 标签”在第 176 页)。然后,您所需要做的就是确保 SDD 标签唯一(通常通过验证xxx在所有 SDD 标签中是唯一的)并定义 SDD 标签的准确位置。

从技术上讲,SRS 中的需求直接映射到设计关注点(通常是一对一关系),这可能会让你认为应该将 SDD 标签附加到设计关注点上。然而,由于设计视图构成了 SDD 的主体部分,并且设计关注点是通过设计视角与之建立多对一的映射关系(设计视角与设计视图之间是一对一的关系),因此最好将 SDD 标签附加到设计视图或视角上。如果需求与设计元素的映射是多对一或一对多(尤其是避免多对多),那么在创建 RTM 时会更为轻松。

在实践中,给定的设计视角可以分解为多个图像或描述。如果你小心只将设计问题连接到这些图像或描述中的一个,你可以将 SDD 标签分配给设计视角的单个组件。但是,进行此操作时必须小心,因为如果一个设计问题映射到设计视角中的多个组件,你可能会遇到多对多关系。^(6)

11.5 一个建议的 SDD 大纲

IEEE Std 1016-2009, 附录 C 提供了一个建议的大纲,用于组织和格式化符合所需内容的 SDD(见 “SDD 所需内容” 第 244 页)。请注意,这个大纲绝不是强制性的;你可以根据自己的需要组织 SDD,只要它包含这些必需的内容,它仍然是有效的。以下是 IEEE 建议的一个略微修改的变体:^(7)

1 前言

1.1 目录

1.2 发布日期和状态

1.3 发布组织

1.4 作者

1.5 变更历史

2 介绍

2.1 目的

2.2 范围

2.3 目标受众

2.4 上下文

2.5 概览/总结

3 定义、缩略语和缩写

4 参考文献

5 词汇表

6 正文

6.1 确定的利益相关者和设计问题

6.2 设计视角 1

6.2.1 设计视图 1

6.2.2 (可选)设计叠加 1

6.2.3 (可选)设计合理性 1

6.3 设计视角 2

6.3.1 设计视图 2

6.3.2 (可选)设计叠加 2

6.3.3 (可选)设计合理性 2

6.4 设计视角 n

6.4.1 设计视图 n

6.4.2 (可选)设计叠加 n

6.4.3 (可选)设计合理性 n

7 (可选)索引

11.6 一个示例 SDD

本节展示了一个完整的(为了编辑简化而高度简化的)SDD 示例。这个 SDD 描述了前一章节中出现的示例用例和需求文档的设计(见 “用例” 第 214 页)。具体来说,这个 SDD 涵盖了 Plantation Productions 数字数据采集和控制(DAQ)系统组件的设计,这些组件在系统初始化时处理 DIP 开关。

1 Plantation Productions DAQ DIP 开关控制

1.1 目录

[因编辑原因省略]

1.2 发布日期和状态

初次创建于 2018 年 3 月 18 日

当前状态:完成

1.3 发布组织

Plantation Productions, Inc.

1.4 作者

Randall L. Hyde

版权 2019, Plantation Productions, Inc.

1.5 变更历史

2019 年 3 月 18 日:初版 SDD 创建。

2 介绍

2.1 目的

Plantation Productions, Inc. 的 DAQ 系统是一个数字数据采集和控制系统,旨在为工业和科学系统提供模拟和数字 I/O。

本软件设计说明书(SDD)描述了 DAQ 系统中 DIP 开关初始化组件的设计。其目的是让开发人员能够根据软件需求规格(SRS)实现 DIP 开关控制功能,并利用本文件实现该目的。

2.2 范围

本文档仅描述 DAQ 系统中的 DIP 开关设计(由于空间/编辑原因)。完整的 SDD 请参见www.plantation-productions.com/Electronics/DAQ/DAQ.html

2.3 预期受众

预期的 SDD 受众期望

本文档适用于将实现此设计的软件开发人员、希望在实施前审查设计的设计利益相关者,以及软件测试用例(STC)和软件测试程序(STD)文档的编写者。

SDD 的真实预期受众:

本文档旨在为《写出伟大的代码,第 3 卷》的读者提供一个示例 SDD。

2.4 上下文

Plantation Productions 的 DAQ 系统满足了对一个文档齐全的数字数据采集与控制系统的需求,工程师可以将其设计到安全关键系统中,如核研究反应堆。虽然有许多现成的商业系统(COTS)可以使用,但它们存在几个主要缺点:通常是专有的(购买后难以修改或维修),通常在 5 到 10 年内就会过时,且没有办法维修或更换,而且很少有完整的支持文档(例如,SRS、SDD、STC 和 STP),工程师无法使用这些文档来验证和确认系统。

DAQ 系统通过提供一套开放硬件和开源设计以及完整的设计文档来克服这一问题,这些设计已经过安全系统的验证和确认。

虽然最初为核研究反应堆设计,但 DAQ 系统在任何需要基于以太网的控制系统的场合都非常有用,支持数字(TTL 级)I/O、光隔离数字输入、机械或固态继电器数字输出(隔离并调理)、模拟输入(例如,±10v 和 4–20mA)以及(调理过的)模拟输出(±10v)。

2.5 概述/总结

本文档的其余部分按如下方式组织。

第三部分介绍了软件设计,包括:

第 3.1 节:利益相关者与设计关注点

第 3.2 节:上下文视角与整体架构

第 3.3 节:逻辑视角与数据字典

第 3.4 节:交互视角与控制流程

第四部分提供了索引。^(8)

3 定义、缩略语和简称

术语 定义
DAQ 数据采集系统
SBC 单板计算机
软件设计说明书(SDD) 软件系统设计文档(IEEE Std 1016-2009)——即本文档。
软件需求规格说明书(SRS) 记录软件及其外部接口的基本需求(功能、性能、设计约束和属性)(IEEE 标准 610.12-1990)。
系统需求规格说明书(SyRS) 一个结构化的信息集合,体现了系统的需求(IEEE 标准 1233-1998)。一个文档,记录了建立设计依据和系统或子系统概念设计的要求。

4 参考文献

参考 讨论
IEEE 标准 830-1998 SRS 文档标准
IEEE 标准 829-2008 STP 文档标准
IEEE 标准 1012-1998 软件验证和确认标准
IEEE 标准 1016-2009 SDD 文档标准
IEEE 标准 1233-1998 SyRS 文档标准

5 术语表

DIP:双列直插封装

6 软件设计

6.1 利益相关者和设计关注点

DAQ DIP 开关设计的利益相关者是 Plantation Productions, Inc. 和 Randall Hyde。主要的设计关注点是创建一个简化的 SDD,能够符合《写出伟大的代码,第 3 卷》的编辑约束,同时仍能提供一个合理的 SDD 示例。其余的设计问题是 SRS 中描述的 DAQ DIP 开关系统的所有需求(请参阅 第 219 页的“(选定的) DAQ 软件需求(来自 SRS)”)。

6.2 上下文视角与整体架构

DAQ 上下文视角展示了用户和系统之间存在的功能。

名称/标签:DAQ_SDD_001

作者:Randall Hyde

设计元素:此视角采用用例、参与者(主机 PC 和终端用户)、节点、组件和包来描述系统接口。

需求/设计关注点:^(9)

DAQ_SRS_700_000

DAQ_SRS_701_000

DAQ_SRS_704_000

DAQ_SRS_707_000

DAQ_SRS_723_000.1

6.2.1 上下文视图^(10)

DAQ 系统固件运行在连接到 DAQ_IF(DAQ 接口)板的 Netburner MOD54415 SBC 上。终端用户可以设置 DIP 开关来初始化 DAQ 与主机 PC 的接口方式。主机 PC 可以通过 RS-232 串口、USB 或以太网连接与 DAQ 系统通信(请参阅 图 11-19)。此设计预期使用现有的库例程,如 maintPrintfserialTaskInitusbTaskInitethernetTaskInitreadDIPSwitches

image

图 11-19:示例上下文视图

6.2.2 组件/部署叠加视图

以下设计叠加视图通过结合部署/组件图提供了对上下文视图的不同视角。图 11-20 展示了系统的物理组件^(11)及其相互连接。

image

图 11-20:示例设计叠加图

6.2.3 (可选)设计原理

这个视角的目的是展示用户如何控制主机 PC 与 DAQ 系统的通信方式。

6.3 逻辑视角和数据字典

名称/标签:DAQ_SDD_002

作者:Randall Hyde

设计元素使用:该视角使用单一的类图来描述此应用程序的数据存储。

注意

在实际应用中,可能更好使用全局变量来保存 DIP 开关设置,而不是实际的类。

要求/设计考虑:

DAQ_SRS_723_000.2

6.3.1 DIP 开关变量

DAQ(DIP 开关)应用程序的数据存储需求非常简单。一个由 图 11-21 中的 12 个全局变量组成的集合(该 SDD 将其归类为 globals)就是实际需要的全部。

名称 描述
dipsw_g 包含 DIP 开关值的 8 位数组(在一个字节中)
serialEnable_g 如果启用 RS-232 通信,则为 true
USBEnabled_g 如果启用 USB 通信,则为 true
ethEnabled_g 如果启用以太网通信,则为 true
ethMultClients_g 如果为 false,则只允许单一的以太网客户端;如果为 true,则允许五个客户端
ethernetDipSw_g 在位 0 中保存 dipsw_g[5],在位 1 中保存 dipsw_g[6]0..3
unitTestMode_g 如果处于单元测试模式,则为 true
debugMode_g 如果 maintPrintf() 函数将输出发送到 COM1:,则为 true;如果 maintPrintf() 被禁用,则为 false
ethernetAdrs_g 保存 IP 地址(192.168.2.70–192.168.2.73)
maxSockets_g 根据 ethEnabled_gethMultClients_g 的值为 015
slots_g 保存最多五个活动以太网套接字的文件描述符
slot_g 用于索引 slots_g
maintPrintfTask() 外部函数,启动 maintPrintf() 任务(用于处理调试输出)
serialTaskInit() 外部函数,启动 RS-232 命令接收任务
usbTaskInit() 外部函数,启动 USB 命令接收任务
ethTaskInit() 外部函数,启动以太网命令接收任务(最多可以同时运行五个此类线程)

image

图 11-21:DAQ 全局实体

6.3.2 设计叠加

[无]

6.3.3 设计理由

这个逻辑视角使用类图而非一组全局变量,纯粹是因为典型的 read dipswitches 函数对于 Netburner 来说是将所有 8 个读取值作为一个 8 位字节返回(即,作为位数组)。因此,将所有 8 个值视为类的字段是有意义的,因为这些属性通常是通过掩码计算得出的,即通过屏蔽特定的位来计算。

6.4 交互视角和控制流程

名称/标签:DAQ_SDD_003

作者:Randall Hyde

设计元素使用:该视角使用了几个活动图来展示程序中的控制流程(及值计算)。

要求/设计考虑:

DAQ_SRS_702_000

DAQ_SRS_702_001

DAQ_SRS_702_002

DAQ_SRS_703_000

DAQ_SRS_703_001

DAQ_SRS_705_000

DAQ_SRS_705_001

DAQ_SRS_705_002

DAQ_SRS_706_000

DAQ_SRS_706_001

DAQ_SRS_708_000

DAQ_SRS_709_000

DAQ_SRS_710_000

DAQ_SRS_711_000

DAQ_SRS_712_000

DAQ_SRS_716_000

DAQ_SRS_716_001

DAQ_SRS_716_002

DAQ_SRS_716.5_000

DAQ_SRS_717_000

DAQ_SRS_718_000

DAQ_SRS_718_001

DAQ_SRS_719_000

DAQ_SRS_720_000

DAQ_SRS_721_001

DAQ_SRS_721_002

DAQ_SRS_723_000

DAQ_SRS_723_000

DAQ_SRS_723_000

DAQ_SRS_723_000.2

DAQ_SRS_726_000

DAQ_SRS_727_000

DAQ_SRS_728_000

DAQ_SRS_737_000

DAQ_SRS_738_000

DAQ_SRS_738_001

DAQ_SRS_738_002

6.4.1 设计视图

交互视角的设计视图使用 UML 活动图(流程图)来展示应用程序中的控制流。参见 图 11-22,11-23,和 11-24。

image

图 11-22:活动图:读取 DIP 开关

image

图 11-23:活动图续篇 #1

image

图 11-24:活动图续篇 #2

serialTaskInit()usbTaskInit() 函数是外部库代码,这些函数启动了一个任务,ethernetListenTask,来处理 RS-232 和 USB 通信,如 图 11-25 所示。

ethTaskInit() 函数(由外部库提供)会一直运行,直到连接的主机终止以太网连接。届时,ethernetListenTask 任务将把相应插槽的条目设置为 0 并终止任务(线程)。如果监听连接中断,ethernetListenTask 将终止。

image

图 11-25:活动图:ethernetListenTask

6.4.2 顺序图叠加

图 11-26 中的顺序图展示了另一种查看 DAQ 应用程序线程初始化的方式。

6.4.3 设计原理

DAQ DIP 开关项目相对简单(故意设计成这样,以便 SDD 示例不至于过大而无法容纳在本书中)。因此,设计采用了传统的过程式/命令式编程模型(与面向对象设计相对)。

7 索引

[出于编辑原因省略]

image

图 11-26:顺序图:初始化任务

11.7 使用设计信息更新可追溯性矩阵

SDD 在 RTM 中添加了一个单独的列:SDD 标签列。然而,SDD 标签并没有直接嵌入任何可追溯性信息,因此你需要从 SDD 中提取该信息,以确定在 RTM 中放置 SDD 标签的位置。

如在《设计视角与设计观点》一节中提到的,第 240 页,每个 SDS 中的观点必须包括设计关注点和需求信息。在本章中(参见《样本 SDS》,第 247 页),我强烈建议将所有 SRS 需求标签作为观点文档中的设计关注点列表。如果你已完成这一工作,那么你已经建立了从需求到设计的反向追踪。因此,在 RTM 中填写 SDS 标签就变得简单了:只需找到每个需求标签(列在当前观点中),然后将该观点的 SDS 标签复制到 RTM 中的 SDS 标签列中。当然,考虑到你可能会有多个需求与单个观点相关联,那么你也会在 RTM 中看到多个相同的 SDS 标签(每个相关需求一份)。

如果你想要将 SDS 标签追溯到 RTM 中的所有需求(而不查找 SDS 中的列表),只需按 SDS 标签列对 RTM 进行排序。这样,所有相关的需求(以及与该 SDS 标签关联的其他一切)就会聚集在矩阵中的一个连续组中,便于识别与该标签相关的所有内容。

如果你选择一种不涉及将 SRS 标签纳入观点中的其他方法来指定设计关注点,那么在 RTM 中确定 SDS 标签的位置就会变成一个手动(甚至繁琐)的过程。因此,我强烈建议在生成观点时使用 SRS 标签。因为无论如何你在生成观点时必须考虑所有需求,所以同时将这些信息收集到 SDS 中是有意义的。

11.8 创建软件设计

本章花费了大量时间讨论如何创建软件设计说明。在你所看到的例子中,可能会让你觉得这些设计似乎凭空产生。那么这些设计是从哪里来的呢?如果你正在创建一个新系统的设计,你是如何最初构思这个设计的呢?这个问题将在本系列的下一卷《写出伟大的代码,第 4 卷:设计伟大的代码》中讨论。本章为那本书打下了基础。

11.9 更多信息

Freeman, Eric 和 Elizabeth Robson. Head First Design Patterns: A Brain-Friendly Guide. Sebastopol, CA: O'Reilly Media, 2004.

Gamma, Erich 等人. Design Patterns: Elements of Reusable Object-Oriented Software. Upper Saddle River, NJ: Addison-Wesley Professional, 1994.

IEEE. “IEEE Std 1016-2009: IEEE 标准——信息技术——系统设计——软件设计说明。”2009 年 7 月 20 日。ieeexplore.ieee.org/document/5167255/。 (它不便宜——大约 100 美元——而且措辞让只有律师才能理解,但这是 SDS 的黄金标准。)

第十四章:软件测试文档**

Image

本章讨论软件测试文档,主要关注软件测试用例(STC)和软件测试程序(STP)文档。如前几章所述,本讨论基于 IEEE 标准,特别是 IEEE 软件和系统测试文档标准(IEEE Std 829-2008,以下简称Std 829^(1))。

12.1 标准 829 中的软件测试文档

标准 829 实际上描述了许多超出 STC 和 STP 的附加文档,包括:

  • 主测试计划(MTP)

  • 级别 测试计划(LTP)

  • 级别 测试设计(LTD)

  • 级别 测试用例(LTC)

  • 级别 测试程序(LTPr)

  • 级别 测试日志(LTL)

  • 异常报告(AR)

  • 级别 临时测试状态报告(LITSR)

  • 级别 测试报告(LTR)

  • 主测试报告(MTR)

请注意,这些并非实际的文档名称——级别 只是表示软件测试文档范围或程度的占位符。范围可能是在组件组件集成的层次上,适用于整个系统,或专注于验收。例如,级别 测试计划可能指代组件(或单元)测试计划、组件集成(或简单的集成)测试计划、系统(或系统集成)测试计划,或验收测试计划。

注意

测试级别的详细解释请参见《软件开发测试级别》,见第 265 页。

总的来说,标准 829 定义了 31 种不同的文档类型,但这些是主要的。大多数这些文档存在的目的是支持软件管理活动。因为这是一本关于个人软件工程的书,而不是软件项目管理书籍,所以本章不会详细讨论其中的大多数文档。相反,我们将重点讨论与实际软件测试相关的那些级别 测试文档——具体而言,级别 测试用例、级别 测试程序、级别 测试日志和异常报告文档类型。我们将涵盖所有四种级别 分类——组件、组件集成、系统和验收——尽管后两者是本章中主要使用的测试文档。这些级别 测试文档之间的差异相对较小,因此本章应用前面提到的总称:软件测试用例和软件测试程序。请记住,尽管这些是常见的软件工程术语,标准 829 仅涉及级别 测试文档。

12.1.1 过程支持

尽管本章集中在软件测试上,标准 829 描述了更为一般的测试过程。特别是,测试过程还涉及开发过程中每个文档步骤的验证和确认。具体来说,这意味着测试过程不仅测试实际的软件,还测试文档本身。

对于 SyRS 和 SRS,验证步骤确保需求真正满足客户需求(并且满足客户需求,不进行额外的无谓增加)。对于 SDD,验证步骤确保 SDD 涵盖所有需求。对于 STC,验证步骤确保每个需求都有一个或多个测试用例来测试该需求。对于 STP,验证步骤确保测试程序集合完全覆盖所有测试用例。

除了文档,Std 829 还讨论了验证采购(例如购买第三方库和计算硬件)、管理 RFP(提案请求)以及许多其他活动的测试程序。这些测试活动非常重要。然而,如前所述,这些主要是管理活动,而非软件开发活动,因此在此仅简要提及。

Std 829 规定,测试需要支持管理、采购、供应、开发、运营和维护的流程。本章将重点讨论开发和运营流程(并在有限程度上讨论维护流程,后者在很大程度上是开发和运营流程的迭代)。有关其他流程的更多详细信息,请参见 Std 829、IEEE/EIA Std 12207.0-1996 [B21]和 ISO-IEC-IEEE-29148-2011。

请注意,Std 829 允许您合并和省略一些测试文档。这意味着您可以只创建一个文档,同时仍符合 Std 829 规范。实际上,您创建的最终文档数量取决于项目的规模(大型项目需要更多文档)和预期的周转速度(快速项目的文档较少)。

12.1.2 完整性等级和风险评估

Std 829 定义了四个完整性等级,描述了软件组件对风险的敏感性或重要性:

灾难性(等级 4) 这一等级意味着软件必须正常执行,否则可能会发生灾难性的后果(例如死亡、系统的无法修复损坏、环境破坏或巨大的经济损失)。对于灾难性的系统故障没有应急措施。例如,软件控制的自动驾驶车辆中的刹车故障。

关键(等级 3) 这一等级意味着软件必须正常执行,否则可能会出现严重问题,包括永久性伤害、重大性能下降、环境破坏或经济损失。对于关键系统故障,可能存在部分应急措施。例如,自动驾驶车辆中负责传动控制的软件无法从二档换到空档。

边际性(2 级) 这一级别意味着软件必须正常执行,否则可能会产生(轻微的)错误结果并丧失一些功能。可以通过解决方法来解决问题。继续以自动驾驶汽车为例,阻止信息娱乐中心正常运行的软件故障就是一个边际性问题。

微不足道(1 级) 这一级别意味着软件必须正常执行,否则系统中可能会缺少某些次要功能(或者软件可能不像应有的那样“精致”)。微不足道的问题通常不需要解决方法,可以安全地忽略,直到更新发布为止。一个例子是自动驾驶汽车中信息娱乐中心触摸屏的拼写错误。

级别越高,测试过程的重要性就越大;也就是说,4 级(灾难性)问题比 1 级(微不足道)问题需要更高质量和更密集的测试。因此,完整性级别成为决定测试用例数量、质量和深度的依据。对于在失败时可能导致灾难性后果的程序功能,你需要创建足够数量的测试用例,并对该功能进行深入测试。对于潜在后果微不足道的功能,可能不需要任何测试用例,或者只进行非常浅显的测试(例如草率的检查)。^(2)

风险评估 是试图确定系统中哪些地方可能发生故障、其预期频率以及相关的成本。虽然风险评估本质上是预测性的(这意味着它不会是完美的),但你通常可以识别出那些更可能出现问题的程序部分(例如复杂的代码部分、经验较少的工程师编写的代码、来源可疑的代码,如来自互联网的开源库代码,以及使用理解不完全的算法的代码)。如果你能将问题的可能性分类为 可能有可能偶尔不太可能,你可以帮助识别出需要更严格测试的代码(反之亦然,哪些代码只需要最少的测试)。

你可以将完整性级别和风险评估级别结合在一个矩阵中,生成风险评估方案,如表 12-1 所示。在这个示例中,值为 4 表示极其重要,值为 1 表示几乎不重要。

表 12-1: 风险评估方案

后果 可能性
可能 有可能 偶尔 不太可能
灾难性 4 4 3.5 3
严重 4 3.5 3 2.5
边际性 3 2.5 1.5 1
微不足道 2 1.5 1 1

Std 829 并不强制要求在测试文档中使用完整性层级或风险评估方案,尽管它认为这是最佳实践。如果使用完整性层级,Std 829 也不要求使用 IEEE 推荐的方案(例如,您可以使用一个更精细的完整性层级,取值范围为 1 到 10)。然而,如果您“自行设计”完整性层级,IEEE 建议您记录从您的完整性层级到 IEEE 建议层级的映射,以便读者能够轻松进行对比。

12.1.3 软件开发测试层级

此外,与刚才描述的完整性层级相对,IEEE 定义了四个测试层级,每个层级通常描述正在记录的软件测试的范围或程度:

组件(也称为 单元)****^(3) 这一层级处理最低代码层级的子程序、函数、模块和子程序。例如,单元测试包括独立于程序其余部分的测试单个函数和其他小的程序单元。

组件集成(也简称为 集成) 这一层级是您开始将单独的单元组合在一起,形成系统的更大部分的地方,但不一定是整个系统。例如,集成测试发生在您将(已测试的)单元组合在一起,检查它们是否能良好协作(即,是否传递适当的参数、返回适当的函数结果等)。

系统(也称为 系统集成) 这一测试层级是集成测试的最终形式——您已将所有程序单元整合在一起,形成完整的系统。单元测试、集成测试和系统集成测试通常是开发人员在将完整系统交给开发组外的人员之前执行的测试。

验收(变体包括 工厂验收 现场验收) 验收测试AT)是开发后的测试。顾名思义,它指的是客户如何确定系统是否可以接受。根据系统的不同,可能会有几种验收测试的变体。工厂验收测试(FAT)发生在系统离开制造商之前(通常在工厂车间进行,因此得名)。即使一个产品是纯软件,它也可以进行工厂验收测试,客户的代表会来,在软件开发团队的监督下测试软件。这允许团队在客户在 FAT 过程中发现小错误时迅速做出更改。

现场验收测试 (SAT) 在系统安装后于客户现场进行。对于硬件系统,这确保硬件安装正确,软件按预期功能运行。对于纯软件系统,SAT 提供了最终检查(可能是在 AT 或 FAT 之后),确保软件能够被系统的最终用户使用。

12.2 测试计划

软件测试计划是描述测试过程的范围、组织结构和相关活动的文档。这主要是对测试将如何进行、所需资源、时间表、必要工具和目标的管理概述。本章不会详细讨论测试计划,因为它们超出了本书的范围;然而,接下来的部分将提供 IEEE Std 829-2008 中提供的概要作为参考。如需了解更多关于这些测试计划的详细信息,请参阅 Std 829。

12.2.1 主测试计划

主测试计划(MTP) 是一份跨项目(或多个项目)跟踪测试过程的组织级高层管理文档。软件工程师通常不会直接参与 MTP 的工作,它更多是一个由 QA(质量保证)部门使用的综合性文档,用于跟踪项目的质量方面。项目经理或项目负责人可能会了解 MTP,并在日程和资源开发过程中有所贡献,但开发团队通常只在偶然的情况下看到 MTP。

以下大纲来自 IEEE Std 829-2008 第八部分(并使用 IEEE 章节编号):

1 引言

1.1 文档标识符

1.2 范围

1.3 参考文献

1.4 系统概述与关键特性

1.5 测试概述

1.5.1 组织结构

1.5.2 主测试进度表

1.5.3 完整性等级架构

1.5.4 资源摘要

1.5.5 职责

1.5.6 工具、技术、方法与度量标准

2 主测试计划详情

2.1 测试过程,包括测试等级的定义

2.1.1 过程:管理

2.1.1.1 活动:测试工作管理

2.1.2 过程:采购

2.1.2.1 活动:采购支持测试

2.1.3 过程:供应

2.1.3.1 活动:规划测试

2.1.4 过程:开发

2.1.4.1 活动:概念

2.1.4.2 活动:需求

2.1.4.3 活动:设计

2.1.4.4 活动:实施

2.1.4.5 活动:测试

2.1.4.6 活动:安装/检查

2.1.5 过程:操作

2.1.5.1 活动:操作测试

2.1.6 过程:维护

2.1.6.1 活动:维护测试

2.2 测试文档要求

2.3 测试管理要求

2.4 测试报告要求

3 一般

3.1 术语表

3.2 文档变更程序和历史

这些部分的许多内容是 IEEE 文档中常见的信息(例如,参见前几章中的 SRS 和 SDD 示例)。由于 MTP 超出了本章的范围,请参阅 Std 829,以获取此大纲中每一节的具体描述。

12.2.2 级别* 测试计划*

级别 测试计划(LTP) 是指基于开发状态的一组测试计划。正如本章前面所述,文档集中的每个文档通常描述了正在记录的软件测试的范围或程度:组件测试计划(也称为单元测试计划,或 UTP)、组件集成测试计划(也称为集成测试计划,或 ITP)、系统测试计划(也称为系统集成测试计划,或 SITP)和验收测试计划(ATP;可能包括工厂验收测试计划 [FATP] 或现场验收测试计划 [SATP])。^(4)

LTPs 也是管理/质量保证文档,但开发团队(甚至到个别软件工程师的层面)通常会参与其创建和使用,因为这些文档涉及软件设计的详细特性。这些测试计划不是指导性文档——也就是说,软件工程师在实际测试软件时不一定会参考这些文档——但没有开发团队的反馈,这些文档无法创建。与 MTP 类似,LTP 为创建测试用例和测试过程文档提供了路线图(这些文档对开发和测试团队尤为重要),并概述了如何执行测试。LTP 提供了测试过程的良好高层视图,尤其是对于对质量感兴趣的外部组织。^(5)

以下是来自 Std 829 的 LTP 大纲:

1 引言

1.1 文档标识符

1.2 范围

1.3 参考文献

1.4 在整体序列中的级别

1.5 测试类别和整体测试条件

2 本级测试计划的详细信息

2.1 测试项及其标识符

2.2 测试可追溯性矩阵

2.3 测试的特性

2.4 不测试的特性

2.5 方法

2.6 项目通过/失败标准

2.7 暂停标准和恢复要求

2.8 测试交付物

3 测试管理

3.1 计划活动和任务;测试进度

3.2 环境/基础设施

3.3 职责和权限

3.4 各方之间的接口

3.5 资源及其分配

3.6 培训

3.7 日程安排、估算和成本

3.8 风险及应急措施

4 总则

4.1 质量保证程序

4.2 指标

4.3 测试覆盖范围

4.4 术语表

4.5 文档变更程序和历史

你可能会注意到,LTP 和 MTP 之间有相当大的重叠。Std 829 规定,如果你在测试计划中复制了其他地方存在的信息,你可以简单地引用包含该信息的文档,而不必在 LTP(或 MTP)中重复该信息。例如,你可能会有一个总体的逆向可追溯性矩阵(RTM),其中包括所有测试的可追溯性信息。与其在 LTP 的第 2.2 节中重复该可追溯性信息,不如直接引用包含该信息的 RTM 文档。

12.2.3 级别 测试设计文档

测试设计(LTD)文档,顾名思义,描述了测试的设计。再次强调,有四种类型的 LTD 文档,每种文档通常描述了软件测试的范围或程度:组件测试设计(又称单元测试设计,或 UTD)、组件集成测试设计(又称集成测试设计,或 ITD)、系统测试设计(又称系统集成测试设计,或 SITD)和验收测试设计(ATD;这可能包括工厂验收测试设计[ FATD ]或现场验收测试设计[SATD])。

LTD 的主要目的是将常见信息集中在一个地方,这些信息通常会在测试程序中重复。这意味着该文档很容易与测试程序文档合并(虽然这样会导致文档中有一些重复)。本书将采取这种方法,将相关项目直接合并到测试用例和测试程序文档中。^(6) 因此,本节将呈现 IEEE 推荐的大纲,而不做额外的评论,详细内容会保留在 STC 和 STP 文档中。

1 引言

1.1 文档标识符

1.2 范围

1.3 参考文献

2 级别测试设计的详细信息

2.1 要测试的特性

2.2 方法改进

2.3 测试标识

2.4 特性通过/失败标准

2.5 测试交付物

3 一般

3.1 术语表

3.2 文档变更程序和历史

12.3 软件审查清单文档

当你从需求开始构建 RTM 时,通常会创建的其中一列是测试/验证类型列。通常,一个软件需求会有两种相关的验证类型之一:T(表示测试)和 R(表示审查)。^(7) 标记为T的需求将有相关的测试用例和测试程序(有关创建测试用例的详细信息,请参见“使用需求信息更新可追溯矩阵”第 222 页)。标记为R的项将需要进行审查。本节描述了如何创建一个软件审查清单(SRL)文档,用于跟踪系统(通常是源代码)的审查,以验证这些需求。

SRL 相对简单。文档的核心只是一份项目清单,每个项目都在审查后打勾,确保软件正确支持相关需求。

理论上,你可以在四个独立的级别创建级别审查清单文档:组件、组件集成、系统和验收(如同其他 Std 829级别文档的情况)。然而,在现实中,一个适用于系统(集成)和验收使用的单一 SRL 就足够了。

注意

SRL 文档不是 Std 829(或任何其他 IEEE 标准文档)的一部分。Std 829 确实允许你将此文档作为验证包的一部分,但本节中所呈现的格式并非 IEEE 标准格式。

12.3.1 示例 SRL 大纲

尽管 SRL 不是标准的 IEEE 文档,但其大纲与 IEEE 推荐的 SRS、STC 和 STP 格式有些相似:

1 引言(每个文档中一次)

1.1 文档标识符

1.2 文档变更程序和历史

1.3 范围

1.4 目标读者

1.5 定义、缩略语和缩写

1.6 参考文献

1.7 描述符号

2 一般系统描述

3 检查清单(每个审查项一个)

3.1 审查标识符(标签)

3.2 审查项讨论

12.3.2 示例 SRL

本示例 SRL 继续使用前几章中的 DAQ DIP 开关项目。具体来说,该 SRL 基于“(已选)DAQ 软件需求(来自 SRS)”中的需求,参见 第 219 页,以及在 第 223 页 中详细说明的“需通过审查验证的需求”。

1 引言

本软件审查清单为那些需要通过审查验证的 DAQ 系统需求提供了软件审查检查表。

1.1 文档标识符

DAQ_SRL v1.0

1.2 文档变更程序和历史

所有修订应在此注明,包括日期和版本号。

2018 年 3 月 23 日—版本 1.0

1.3 范围

本 SRL 处理的是那些在创建正式测试程序上会比较困难(或在经济上不可行),但其正确性可以通过审查源代码和构建系统轻松验证的 DAQ DIP 开关初始化项目中的需求。

1.4 目标读者

SRL 的正常受众:

本文档主要面向那些将测试/审查 DAQ DIP 开关项目的人员。项目管理和开发团队也可能希望审查此文档。

该 SRL 的实际受众:

本 SRL 适用于《编写优秀代码》第三卷的读者。它提供了一个示例 SRL,可作为他们可能需要创建的 SRL 模板。

1.5 定义、缩略语和缩写

DAQ: 数据采集系统

DIP: 双列直插封装

SDD: 软件设计文档

SRL: 软件审查清单

SRS: 软件需求规格说明书

1.6 参考文献

SDD: IEEE 标准 1016-2009

SRS: IEEE 标准 830-1998

STC/STP: IEEE 标准 829-2008

1.7 描述符号

本文档中的审查标识符(标签)应采用以下形式:

DAQ_SR_xxx_yyy_zzz

其中 xxx_yyy 是从相应要求中提取的一串(可能是十进制)数字(例如,DAQ_SRS_xxx_yyy),zzz 是一个(可能是十进制)数字序列,它通过整个序列创建一个唯一的标识符。请注意,SRL 标签中的 zzz 值通常从 000 或 001 开始编号,并且通常在每个共享相同 xxx_yyy 字符串的附加审查项中递增 1。

2 一般系统描述

DAQ DIP 开关系统的目的是在上电时初始化 DAQ 系统。DAQ DIP 开关系统是更大 Plantation Productions DAQ 系统的一小部分,在本书中作为示例使用。

3 检查清单

在审查过程中,逐项勾选以下内容,确认每项已验证。

3.1 DAQ_SR_700_000_000

验证代码是否为 Netburner MOD54415 评估板编写。

3.2 DAQ_SR_700_000.01_000.1

验证代码是否为 μC/OS 编写。

3.3 DAQ_SR_702_001_000

验证软件是否创建了一个单独的任务来处理串口命令处理。

3.4 DAQ_SR_702_002_000

验证串行任务的优先级是否低于 USB 和以太网任务的优先级(注意:优先级编号越高,优先级越低)。

3.5 DAQ_SR_703_001_000

与 DAQ_SRS_702_001 相同,但如果 DIP 开关 1 处于 OFF 位置,则不会启动 RS-232 任务。

3.6 DAQ_SR_705_001_000

验证软件是否创建了一个单独的任务来处理 USB 端口命令处理。

3.7 DAQ_SR_705_002_000

验证 USB 任务的优先级是否高于以太网和串行协议任务。

3.8 DAQ_SR_706_001_000

验证当 DIP 开关 2 处于 OFF 位置时,软件是否不会启动 USB 任务。

3.9 DAQ_SR_716_001_000

验证只有在启用以太网通信时,才会启动以太网监听任务。

3.10 DAQ_SR_716_002_000

验证以太网监听任务的优先级是否低于 USB 任务,但高于串行任务。

3.11 DAQ_SR_719_000_000

验证软件是否根据 DIP 开关 7 设置将单元测试模式值设为 ON。

3.12 DAQ_SR_720_000_000

验证软件是否根据 DIP 开关 7 设置将单元测试模式值设为 OFF。

3.13 DAQ_SR_723_000_000

验证软件是否提供了读取 DIP 开关的功能。

3.14 DAQ_SR_723_000.01_000

验证系统是否使用 DIP 开关读取值来初始化启动时的 RS-232(串行)、USB、以太网、单元测试模式和调试模式。

3.15 DAQ_SR_723_000.02_000

验证启动代码是否存储了 DIP 开关读取值,以供软件后续使用。

3.16 DAQ_SR_725_000_000

验证当从 USB、RS-232 和以太网端口接收到完整的文本行时,命令处理器是否对命令做出响应。

3.17 DAQ_SR_738_001_000

验证系统是否在每个新的以太网连接上启动一个新的进程(任务)来处理命令。

3.18 DAQ_SR_738_002_000

验证以太网命令处理任务在以太网监听任务和 USB 命令任务之间的优先级。

12.3.3 将 SRL 项目添加到可追溯性矩阵

一旦你创建了 SRL,你需要将所有的SR标签添加到 RTM 中,这样就可以将已审阅的项目追溯到需求,以及 RTM 中的其他所有内容。为此,只需定位与每个审阅项目标签相关联的需求(如果你使用本章推荐的标签编号,这很简单;SRS 标签编号已包含在 SRL 标签编号中),并将 SRL 标签添加到 RTM 中相应行的适当列。

当你拥有 SRL 和 STC 文档时,实际上无需在 RTM 中为两者创建单独的列,因为它们是互斥的,标签将区分它们。(有关更多详细说明,请参见 “软件需求规格样本” 以及 第 203 页的相关评论。)

12.4 软件测试用例文档

对于 RTM 中每个需求验证类型为T的项目,您需要创建一个软件测试用例。软件测试用例(STC)文档是您放置实际测试用例的地方。

与所有 829 标准级别文档一样,级别测试用例文档有四个级别。术语软件测试用例通常指其中任何一个。如本章前面所述,这实际上是一组测试用例,每个文档类型通常描述文档化的软件测试的范围或程度:组件测试用例(也称为单元测试用例,或 UTC)、组件集成测试用例(也称为集成测试用例,或 ITC)、系统测试用例(也称为系统集成测试用例,或 SITC)和验收测试用例(ATC;可能包括工厂验收测试用例 [FATC] 和现场验收测试用例 [SATC])。^(8)

STC 文档列出了项目的所有单个测试用例(测试)。以下是 Std 829 大纲,适用于级别测试用例文档:

1 引言(每个文档一次)

1.1 文档标识符

1.2 范围

1.3 参考文献

1.4 上下文

1.5 描述的符号表示法

2 详细信息(每个测试用例一次)

2.1 测试用例标识符

2.2 目标

2.3 输入

2.4 结果

2.5 环境需求

2.6 特殊程序要求

2.7 测试用例间依赖关系

3 全局(每个文档一次)

3.1 术语表

3.2 文档变更程序和历史

在实际应用中,单元测试用例和集成测试用例通常会合并到同一文档中(这两者的区分通常发生在测试过程级别)。你通常会根据源代码和软件设计文档(SDD)来开发单元测试用例(UTC)和集成测试用例(ITC)(见图 12-1,它是图 9-1 的扩展)。

image

图 12-1:单元和集成测试用例来源

通常,UTC 和 ITC(以及测试程序)文档作为软件存在,而不是自然语言文档。使用自动化测试程序,这是一种运行所有单元和集成测试的软件,是软件工程的最佳实践。通过这样做,你可以显著减少运行测试所需的时间以及人工执行测试程序时引入的错误。^(9)

不幸的是,并不是每个测试用例都能创建自动化测试,因此你通常会有一个 UTC/ITC 文档,涵盖(至少)你必须手动执行的测试用例。

许多组织——尤其是那些采用敏捷开发模型和测试驱动开发(TDD)方法的组织——放弃了正式的 UTC 和 ITC 文档。在这些情况下,非正式编写的程序和自动化测试程序更为常见,因为创建和(尤其是)维护文档的成本迅速失控。只要开发团队能够提供一些文档,证明他们正在执行一组固定的单元/集成测试(也就是说,他们没有进行随意的“临时”测试,这些测试每次执行可能都不同),较大的组织通常会选择不干涉。

无论是正式的、非正式的还是自动化的,拥有可重复的测试程序是关键。回归测试,用于检查自从你对代码做出更改后,是否有任何东西发生了破坏或回归,需要一个可重复的测试过程。因此,你需要某种测试用例来确保可重复性。

对于单元/集成测试,你生成的测试数据将是黑盒生成的测试数据和白盒生成的测试数据的组合。黑盒测试数据通常来自系统需求(SyRS 和 SRS);在创建输入测试数据时,你只考虑系统的功能(需求所提供)。另一方面,生成白盒测试数据时,你需要分析软件的源代码。例如,确保在测试过程中每个语句至少执行一次——即实现完全代码覆盖——需要仔细分析源代码,因此,这是一个白盒测试数据生成技术。

注意

《编写优秀代码,第六卷:测试、调试与质量保证》将更详细地讨论生成白盒和黑盒测试数据的技术。

一旦进入系统集成测试或(更重要的)验收测试阶段,测试用例的正式文档就变得至关重要。如果您为客户创建定制系统,或者您的软件受制于监管或法律限制(如在自动驾驶汽车中的生命威胁环境),您可能需要说服某个监管机构证明您在测试过程中已尽最大努力,并证明系统满足其要求。正因为如此,许多 SITC 和(当然)ATC 文档直接从需求中推导出测试用例(见图 12-2)。因此,带着这个动机,让我们回到级别测试用例文档的讨论中(请参见本节开头的提纲)。

image

图 12-2:SITC 和 ATC 推导

通常,(F)ATC 文档只是 SITC 文档的一个子集。(如果您有 FATC 文档和 SATC 文档,站点变体通常是 FATC 文档的子集。)SITC 文档将包含每个需求的测试用例。在 ATC 文档中,系统架构师可能会合并或删除几乎完全冗余的测试用例,或者对客户和最终用户来说意义不大的测试用例。

12.4.1 STC 文档中的引言

STC(或任何级别测试用例)文档的引言部分应包括以下信息。

12.4.1.1 文档标识符

文档标识符应是唯一的名称/编号,并应包括发行日期、作者标识、状态(例如草稿或最终版)、批准签名以及可能的版本号。一个唯一的 ID 名称/编号至关重要,这样您就可以在其他文档中引用测试用例文档(如 STP 和 RTM)。

12.4.1.2 范围

本节总结了需要测试的软件系统和功能。

12.4.1.3 参考文献

本节应提供与 STC 相关的所有参考文档的清单,包括内部和外部文档。内部参考文献通常包括如 SyRS、SRS、SDD、RTM 以及(如果存在)MTP 等文档。外部参考文献包括标准,如 IEEE Std 829-2008 以及可能适用的任何监管或法律文档。

12.4.1.4 上下文

在本节中,您提供任何不出现在其他文档中的测试用例上下文。例如,可能包括命名用于生成或评估测试用例的自动化测试生成软件或基于互联网的工具。

12.4.1.5 描述的符号

本节应描述您将应用于测试用例的标签(标识符)。例如,本章使用projSTCxxxyyyzzz格式的标签,因此 STC 的这一部分将解释这些标签的含义以及如何生成 STC 标签。

12.4.2 详细信息

您将在 STC 中为每个测试用例重复此部分。

12.4.2.1 测试用例标识符

测试用例标识符是与此特定测试用例相关联的标签。例如,本书使用的标签形式为DAQ_STC_002_000_001,其中DAQ是项目 ID(针对 DAQ DIP 开关项目),002_000来自 SRS 需求标签,而001是特定于测试用例的值,用于使该标签在所有其他标签中唯一。前几章的游泳池监控器(SPM)项目可能会在 STC 中使用类似POOL_STC_002_001的标签。Std 829 并不要求使用此标签格式,只要求所有测试用例标签是唯一的。

12.4.2.2 目标

这是对该特定测试用例目标或重点的简要描述。(请注意,一组测试用例可以具有相同的目标,在这种情况下,此字段可以仅引用另一个测试用例中的目标。)如果相关,您可以在此字段中放置风险评估和完整性级别信息。

12.4.2.3 输入

本节列出执行此测试用例所需的所有输入及其关系(例如时序、顺序等)。某些输入可能是精确的,某些则可能是近似的,在这种情况下,您必须为输入数据提供容差。如果输入集很大,本节可以仅引用输入文件、数据库或提供测试数据的其他输入流^11。

12.4.2.4 输出

本节列出了所有预期的输出数据值和行为,如响应时间、时序关系和输出数据的顺序。测试用例应尽可能提供精确的输出数据值;如果只能提供近似数据值,则测试用例还必须提供容差。如果输出流很大,则本节可以引用外部提供的文件或数据库。

如果测试能够成功运行且没有崩溃——也就是说,自我验证——那么在测试用例中这一部分就不需要了。

12.4.2.5 环境需求

本节描述了测试所需的任何预先存在的软件或数据,例如已知的数据库。它还可以描述任何通过其网址引用的互联网站点,这些站点必须处于活动状态才能执行测试用例。这也可能包括任何特殊的电力需求,比如要求在测试电力故障之前 UPS 必须充满电,或者包括其他条件,比如在运行 SPM 系统测试之前,游泳池必须先注满水。

12.4.2.5.1 硬件环境需求

本节列出执行测试所需的任何硬件,并指定其配置设置。它还可以指定任何特殊硬件,例如测试操作所需的测试夹具。例如,SPM 的测试夹具可能是一个装满水的五加仑桶,以及连接到水源阀门的软管,这些都是 SPM 的一部分。

12.4.2.5.2 软件环境需求

本节列出了运行测试所需的所有软件(及其版本/配置)。这可能包括操作系统/设备驱动程序、动态链接库、仿真器、代码脚手架(如代码驱动程序)、^(12)和测试工具。

12.4.2.5.3 其他环境需求

这是一个通用部分,可以让你添加配置信息或任何你认为需要记录的内容。例如,对于在特定日期或时间进行的测试,你需要考虑夏令时变更,在这种情况下,每日报告可能需要报告 23 或 25 小时的内容,等等。

12.4.2.6 特殊程序要求

本节列出了测试用例的任何特殊条件或约束。这也可以包括任何特殊的前置条件或后置条件。例如,在测试软件是否能正确响应低池水位条件时,SPM 的一个前置条件是水位低于所有三个低池传感器。此处还应列出任何后置条件,例如桶不得溢出。如果你使用自动化测试程序,这是指定特定工具及如何在测试中使用它的好地方。

请注意,本节不应重复测试程序中已经出现的步骤。相反,它应为正确编写将执行该测试用例的测试程序步骤提供指导。

12.4.2.7 用例间依赖关系

本节应列出(按标签标识符)在当前测试之前必须执行的任何测试用例,以便在执行当前测试之前,系统状态条件已准备就绪。Std 829 建议,通过按必须执行的顺序排列测试用例,你可以减少说明用例之间依赖关系的需求。(显然,这些依赖关系应该被清晰地记录。)然而,通常情况下,你不应依赖这种隐式的依赖关系组织方式,应该明确记录任何依赖关系。但在 STP 中,你可以依赖测试步骤的顺序。在 STC 中已经清晰地划定了执行顺序,这有助于减少创建 STP 时的错误。

12.4.2.8 通过/失败标准

在 Std 829 中,IEEE 建议将通过/失败标准放入级别测试设计文档中;它们不是 Std 829 STC 的一部分。然而,特别是在文档集没有 LTD 的情况下,最好在每个测试用例中包含通过/失败标准。

请注意,如果通过/失败标准仅为“所有系统输出必须与结果部分规定的内容匹配”,那么你可能可以省略本节,但在引言部分明确说明这个默认条件也不会有什么坏处。

12.4.3 一般

本节提供了对词汇表和文档更改程序与历史部分的简要介绍和讨论。

12.4.3.1 词汇表

词汇表部分提供了 STC 中使用的所有术语的按字母顺序排列的列表。它应包括所有缩写词及其定义。尽管标准 829 在大纲的末尾列出了词汇表,但通常它出现在文档的开头,接近参考文献部分。

12.4.3.2 文档更改程序与历史

本节描述了创建、实施和批准 STC 更改的过程。这可能仅仅是一个对配置管理计划文档的引用,该文档描述了所有项目文档或所有组织内文档的更改程序。更改历史应包含以下信息的时间顺序列表:

  • 文档 ID(每个版本应有唯一的 ID,可以简单地是附加到文档 ID 的日期)

  • 版本号(你应该按顺序编号,从第一个批准版本的 STC 开始)

  • 当前版本 STC 所做更改的描述

  • 作者及其角色

更改历史通常出现在 STC 文档的开头,或者在封面页后,靠近文档标识符的位置。

12.4.4 一个软件测试案例文档示例

本章继续前几章的主题,提供了一个关于 Plantation Productions DAQ 系统 DIP 开关初始化设计的 STC 示例。这个 STC 将作为一个验收测试(纯功能测试用例),完全基于项目的 SRS(见第 219 页的“(选定的)DAQ 软件需求(来自 SRS)”)。出现在这个示例 STC 中的测试用例是所有未包含在“通过评审验证的需求”中的项目 SRS 要求,后者列出了“通过评审验证”的要求(见第 223 页)。然而,请注意,出于编辑/空间原因,此示例不会为该项目 SRS 中的每个“通过评审验证”的测试要求提供测试用例。^(13)

术语 定义
DAQ 数据采集系统
SBC 单板计算机
软件设计描述(SDD) 软件系统设计的文档(IEEE Std 1016-2009)——即本文件。
软件需求规格说明(SRS) 软件及其外部接口的基本需求(功能、性能、设计约束和属性)文档(IEEE Std 610.12-1990)。
系统需求规格说明(SyRS) 一个结构化的信息集合,体现了系统的需求(IEEE Std 1233-1998)。一个文档,记录了建立系统或子系统设计依据和概念设计的需求。
软件测试用例(STC) 描述测试用例(输入和结果)以验证软件在各种设计考虑/要求下的正确操作的文档(IEEE Std 829-2009)。
软件测试程序(STP) 描述逐步执行一组测试用例,以验证软件在各种设计考虑/要求下的正确操作的文档(IEEE Std 829-2009)。

1 引言

DAQ DIP 开关项目的软件测试用例

1.1 文档标识符(及变更历史)

2018 年 3 月 22 日:DAQ_STC v1.0;作者:Randall Hyde

1.2 范围

本文档仅描述 DAQ 系统中的 DIP 开关测试用例(由于空间/编辑原因)。有关完整的软件设计描述,请参见 www.plantation-productions.com/Electronics/DAQ/DAQ.html

1.3 术语表、缩略语和缩写

注意

这是一个非常简单且简短的示例,目的是减少书籍的页数。请不要将其作为模板使用;您应该仔细挑选出文档中使用的术语和缩写,并在本节中列出。

1.4 参考文献

参考文献 讨论
DAQ STC 一个完整的 Plantation Productions DAQ 系统的 STC 示例可以在 www.plantation-productions.com/Electronics/DAQ/DAQ.html 找到。
IEEE Std 830-1998 SRS 文档标准
IEEE Std 829-2008 STP 文档标准
IEEE Std 1012-1998 软件验证和确认标准
IEEE Std 1016-2009 SDD 文档标准
IEEE Std 1233-1998 SyRS 文档标准

1.5 背景

Plantation Productions, Inc. 的 DAQ 系统满足了对一个有良好文档的数字数据采集和控制系统的需求,工程师可以将其设计到安全关键系统中,例如核研究反应堆。尽管有许多 COTS 系统^(14) 可以使用,但它们存在一些主要缺点,包括:通常是专有的,因此购买后难以修改或修复;通常在 5 到 10 年内就会过时,且无法修复或更换;并且它们很少有完整的支持文档(例如 SRS、SDD、STC 和 STP),工程师可以使用这些文档来验证和确认系统。

DAQ 系统通过提供一套开放硬件和开源设计,解决了这个问题,配有完整的设计文档,并已针对安全系统进行了验证和验证。

尽管最初设计用于核研究反应堆,DAQ 系统在任何需要支持数字(TTL 级)I/O、光隔离数字输入、机械或固态继电器数字输出、(隔离并调理的)模拟输入(例如,±10v 和 4-20mA)以及(调理后的)模拟输出(±10v)的以太网控制系统中都非常有用。

1.6 描述的符号表示

本文档中的测试用例标识符(标签)应采取以下形式:

DAQ_STC_xxxyyyzzz

其中xxx_yyy是对应要求中的(可能是十进制)数字串(例如,DAQ_SRS_xxx_yyy),zzz是(可能是十进制的)数字序列,用于从整个序列中创建唯一的标识符。请注意,STC 标签中的zzz值通常从 000 或 001 开始,并且每添加一个新的测试用例项,通常会递增 1,所有这些项共享相同的xxx_yyy字符串。

2 细节(测试用例)

2.1 DAQ_STC_701_000_000

目标:测试通过 RS-232 接受命令。

输入:

1. DIP 开关 1 设置为 ON 位置。

2. 在串行终端输入help命令。

结果:

1. 屏幕显示help消息。

环境需求:

硬件 正常运行的(已启动)DAQ 系统,PC 通过 RS-232 端口连接到 DAQ

软件 已安装最新版的 DAQ 固件

外部 串行终端仿真程序在 PC 上运行

特殊程序要求:

[无]

测试间依赖:

[无]

2.2 DAQ_STC_702_000_000

目标:测试 DIP 开关 1 ON 时的命令接受。

输入:

1. DIP 开关 1 设置为 ON 位置。

2. 在串行终端输入help命令。

结果:

1. 屏幕显示help消息。

环境需求:

硬件 正常运行的(已启动)DAQ 系统,PC 通过 RS-232 端口连接到 DAQ

软件 已安装最新版的 DAQ 固件

外部 串行终端仿真程序在 PC 上运行

特殊程序要求:

[无]

测试间依赖:

与 DAQ_STC_701_000_000 相同的测试

2.3 DAQ_STC_703_000_000

目标:测试 DIP 开关 1 OFF 时的命令拒绝。

输入:

1. DIP 开关 1 设置为 OFF 位置。

2. 在串行终端输入help命令。

结果:

1. 系统忽略命令,终端程序没有响应。

环境需求:

硬件 正常运行的(已启动)DAQ 系统,PC 通过 RS-232 端口连接到 DAQ

软件 已安装最新版的 DAQ 固件

外部 串行终端仿真程序在 PC 上运行

特殊程序要求:

[无]

测试间依赖:

[无]

注意

由于空间/编辑原因,本示例在此删除了几个测试用例,因为它们的内容与之前的测试用例非常相似。

2.4 DAQ_STC_709_000_000

目标:测试以太网地址,当 DIP 开关 5 和 6 都为 OFF 时。

输入:

1. DIP 开关 3 设置为 ON 位置(4 = 不关心)。

2. DIP 开关 5 设置为 OFF 位置。

3. DIP 开关 6 设置为 OFF 位置

4. 使用以太网终端程序,尝试连接到 IP 地址 192.168.2.70,端口 20560(0x5050)。

5. 发出help命令。

结果:

1. 以太网终端连接到 DAQ 系统。

2. 终端程序显示 DAQ help信息。

环境需求:

硬件 正常工作的(已启动的)DAQ 系统,PC 连接到 DAQ 的以太网端口

软件 安装了最新版本的 DAQ 固件

外部以太网终端仿真程序在 PC 上运行

特殊程序要求:

[无]

案例间依赖:

案例 DAQ_STC_708_000_000 到 DAQ_STC_718_001_000 紧密相关,应一同执行。

注意

由于空间/编辑原因,示例中删除了几个测试案例,因为它们与之前的测试案例内容非常相似。

2.6 DAQ_STC_710_000_000

目标:测试 DIP 开关 5 为 ON 和 6 为 OFF 时的以太网地址。

输入:

1. DIP 开关 3 设置为 ON 位置(4 = 不关心)。

2. DIP 开关 5 设置为 ON 位置。

3. DIP 开关 6 设置为 OFF 位置。

4. 使用以太网终端程序,尝试连接到 IP 地址 192.168.2.71,端口 20560(0x5050)。

5. 发出help命令。

结果:

1. 以太网终端连接到 DAQ 系统。

2. 终端程序显示 DAQ help信息。

环境需求:

硬件 正常工作的(已启动的)DAQ 系统,PC 连接到 DAQ 的以太网端口

软件 安装了最新版本的 DAQ 固件

外部以太网终端仿真程序在 PC 上运行

特殊程序要求:

[无]

案例间依赖:

案例 DAQ_STC_708_000_000 到 DAQ_STC_718_001_000 紧密相关,应一同执行。

2.7 DAQ_STC_711_000_000

目标:测试 DIP 开关 5 为 OFF 和 6 为 ON 时的以太网地址。

输入:

1. DIP 开关 3 设置为 ON 位置(4 = 不关心)。

2. DIP 开关 5 设置为 OFF 位置。

3. DIP 开关 6 设置为 ON 位置。

4. 使用以太网终端程序,尝试连接到 IP 地址 192.168.2.72,端口 20560(0x5050)。

5. 发出help命令。

结果:

1. 以太网终端连接到 DAQ 系统。

2. 终端程序显示 DAQ help信息。

环境需求:

硬件 正常工作的(已启动的)DAQ 系统,PC 连接到 DAQ 的以太网端口

软件 安装了最新版本的 DAQ 固件

外部以太网终端仿真程序在 PC 上运行

特殊程序要求:

[无]

案例间依赖:

案例 DAQ_STC_708_000_000 到 DAQ_STC_718_001_000 紧密相关,应一同执行。

2.8 DAQ_STC_712_000_000

目标:测试 DIP 开关 5 和 6 都为 ON 时的以太网地址。

输入:

1. DIP 开关 3 设置为 ON 位置(4 = 不关心)。

2. DIP 开关 5 设置为 ON 位置。

3. DIP 开关 6 设置为 ON 位置。

4. 使用以太网终端程序,尝试连接到 IP 地址 192.168.2.73,端口 20560(0x5050)。

5. 发出help命令。

结果:

1. 以太网终端连接到 DAQ 系统。

2. 终端程序显示 DAQ help信息。

环境需求:

硬件 正常运行(已启动)的 DAQ 系统,带以太网端口并连接到 DAQ 的 PC

软件 安装了最新版本的 DAQ 固件

外部 在 PC 上运行的以太网终端仿真程序

特殊程序要求:

[无]

案例间依赖关系:

案例 DAQ_STC_708_000_000 到 DAQ_STC_718_001_000 密切相关,应一起执行。

注意

出于篇幅/编辑原因,此示例在此删除了若干测试用例,因为它们与前面的测试用例内容非常相似。

2.9 DAQ_STC_726_000_000

目标:测试来自 RS-232 端口的命令接收。

输入:

  1. DIP 开关 1 设置为 ON 位置。

  2. 在串行终端输入help命令。

结果:

  1. 屏幕显示help消息。

环境需求:

硬件 正常运行(已启动)的 DAQ 系统,带 RS-232 端口并连接到 DAQ 的 PC

软件 安装了最新版本的 DAQ 固件

外部 在 PC 上运行的串行终端仿真程序

特殊程序要求:

[无]

案例间依赖关系:

与 DAQ_STC_701_000_000 相同的测试

3 测试用例文档更改程序

在对本 STC 进行任何修改时,变更的作者必须在此 STC 文档的 1.1 节中做出新的条目,至少列出日期、文档 ID(DAQ_STC)、版本号和作者。

12.4.5 更新 RTM 以包含 STC 信息

由于软件审核和软件测试用例(及分析/其他)验证方法是相互独立的,你只需要在 RTM 中使用单独的一列来关联这些对象的标签与 RTM 中的其他项。在官方 DAQ 系统的 RTM 中(该系统只有测试用例和软件审核项),该列的标签是软件测试/审核用例。当你将 DAQ_SR_xxx_yyy_zzz和 DAQ_STC_xxx_yyy_zzz项目添加到此列时,由于标签明确标识了你使用的验证类型,因此永远不会有歧义。当然,这假设你使用的是本章建议的标签标识符格式。你也可以使用自己的标签格式,只要它能在标签名称中区分审核和测试用例项。

如果你使用本章的 STC 标签格式,在 RTM 中定位要放置测试用例标签的行非常简单。只需找到带有标签 DAQ_SRS_xxx_yyy的需求,并将 STC 标签添加到同一行的适当列中。如果你使用的标签格式不同,且标签名称中不直接包含需求可追溯性,你需要手动确定关联关系(希望它能包含在测试用例中)。

12.5 软件测试程序文档

软件测试程序(STP)指定了执行一组测试用例的步骤,这些测试用例反过来评估软件系统的质量。从某种意义上说,STP 是一个可选文档;毕竟,如果你按适当的顺序执行所有的测试用例,你将全面测试所有测试用例。STP 的目的是简化测试过程。实际上,测试用例通常是重叠的。尽管它们测试不同的需求,但可能多个测试用例的输入是相同的。在某些情况下,甚至输出也是相同的。通过将这些测试用例合并到一个程序中,你可以运行一个单一的测试序列来处理所有的测试用例。

将测试用例合并为一个 STP 的另一个原因是为了方便共同的设置。许多测试用例在执行之前需要(可能是复杂的)设置,以确保特定的环境条件。往往,多个测试用例在执行之前需要相同的设置。通过将这些测试用例合并到一个程序中,你可以为整个测试集执行一次设置,而不是为每个测试用例重复设置。

最后,一些测试用例可能有依赖关系,需要在执行之前先执行其他测试用例。通过将这些测试用例放入测试程序中,你可以确保测试操作满足这些依赖关系。

标准 829 定义了一组级别测试程序(LTPr)。和标准 829 中所有级别的测试文档一样,LTPr 有四种变体,每种变体都是一个大致描述软件测试范围或程度的文档:组件测试程序(即单元测试程序,或 UTP),组件集成测试程序(即集成测试程序,或 ITP),系统测试程序(即系统集成测试程序,或 SITP),以及验收测试程序(ATP;可能包括工厂验收测试程序[FATP]或现场验收测试程序[SATP])。^(15)

UTP 和 ITP 通常是自动化的测试程序或较为非正式的文档,类似于它们的测试用例文档对应物;详细讨论请见《软件测试用例文档》,见第 274 页。

如果你回顾一下图 12-1 和 12-2,你会看到,STP(以及所有的 LTPr)直接来源于 STC(LTC)文档。图 12-1 适用于 UTP 和 ITP。图 12-2 适用于 SITP 和 ATP(需要注意的是,ATPs 来源于严格来自 SyRS/SRS 要求的测试用例,而不是来自 SDD 元素)。

和测试用例文档一样,ATPs 通常是 SITPs 的一个子集,面向客户或最终用户。同样,如果存在 FATP 和 SATP 文档,SATP 通常是 FATP 的一个子集,经过进一步细化以满足最终用户的需求。^(16)

12.5.1 IEEE Std 829-2009 软件测试程序

Std 829 STP 的大纲如下:

1 引言

1.1 文档标识符

1.2 范围

1.3 参考文献

1.4 与其他文档的关系

2 详细信息

2.1 输入、输出和特殊要求

2.2 执行测试用例步骤的有序描述

3 一般

3.1 词汇表

3.2 文档变更程序与历史

12.5.2 扩展的软件测试程序大纲

像 IEEE 标准中典型的那样,您可以增强这个大纲(添加、删除、移动和编辑项目,前提是有适当的理由)。在这个特定的案例中,这种灵活性非常重要,因为这个大纲中缺少了一些内容。

首先,引言中缺少描述符的符号表示法,这在 STC 大纲中有所提及(“软件测试用例文档”见第 274 页)。^(17) 也许 IEEE Std 829 的作者认为文档第二部分(“详细信息”)中的测试程序数量会很少。然而,实际上,测试程序的数量通常较多。将一个大型测试程序拆分成多个较小的程序有一些非常好的理由:

  • 测试可以并行进行。通过将(独立的)测试程序分配给多个测试团队,您可以更快地完成测试。

  • 某些测试可能会占用资源(例如,示波器、逻辑分析仪、测试夹具和信号发生器等测试设备)。通过将一个大型测试程序拆分为多个小的测试程序,您可能能够限制测试团队对某些资源的使用时间。

  • 能够在一个工作日内(甚至在白天的休息时间之间)完成一个测试程序是件不错的事情,这样测试人员在执行测试时就不会失去注意力。

  • 按相关活动(以及活动前所需的设置)组织测试程序可以简化测试程序,减少步骤,并使它们运行得更高效。

  • 许多组织要求如果测试中的任何部分失败,测试团队必须从头开始重新执行测试程序(回归测试)。将测试程序分解为更小的部分可以使重新执行测试程序的成本大大降低。

为了能够追溯这些测试程序到 STC、SRS 以及 RTM 中的其他文档,您需要测试程序标识符(标签)。因此,您应该有一个部分来描述这些标签的符号表示法。

当然,IEEE 大纲中缺少的第二件事是详细信息部分中的测试程序标识符项。为了便于追溯,最好在每个测试程序中都有一个部分,列出它所涵盖的相关测试用例。最后,为了我自己的目的,我喜欢在每个测试程序中包含以下信息:

  • 简要描述

  • 标签/标识

  • 目的

  • 可追溯性(涵盖的测试用例)

  • 通过/失败标准(因为这可能会随着每个程序的不同而变化)

  • 执行此测试程序所需的任何特殊要求(例如,环境要求);这可能包括必须存在的输入/输出文件等内容

  • 执行测试程序前的所有设置要求

  • 执行测试程序时的软件版本号

  • 执行测试程序的步骤

将这些项目整合后,产生了适用于 SIT、AT、FAT 或 SAT 的任意 STP 的扩展大纲:

1 目录

2 引言

2.1 文档标识符和变更历史(已移动)

2.2 范围

2.3 术语表、缩略语和缩写(已移动)

2.4 参考文献

2.5 描述符号

2.6 与其他文档的关系(已删除)

2.7 执行测试的说明(已添加)

3 测试程序(名称已更改为详情

3.1 简要描述(简单短语),程序 #1

3.1.1 程序标识符(标签)

3.1.2 目的

3.1.3 本程序涵盖的测试用例列表

3.1.4 特殊要求

3.1.5 执行程序前的设置要求

3.1.6 本次执行的软件版本号

3.1.7 执行程序的详细步骤

3.1.8 测试程序签字确认

3.2 简要描述(简单短语),程序 #2

  • (与前一节相同的小节)

  • . . .

3.n 简要描述(简单短语),程序 #n

  • (与前一节相同的小节)

4 总则

4.1 文档变更程序

4.2 附件和附录

5 索引

12.5.3 STP 文档中的引言

以下小节描述了 STP 引言的组成部分。

12.5.3.1 文档标识符和变更历史

文档标识符应是某个(组织范围内)唯一的名称;这通常会包括某个项目标识,如DAQ_STP,创建/修改日期,版本号以及作者。一个标识符列表(每次文档修订的一个标识符)将构成变更历史。

12.5.3.2 范围

这里的范围基本上与用于 STC 的定义相同(请参见 “软件测试用例文档” 在 第 274 页)。标准 829 建议根据 STP 的重点及其与 STC 和其他测试文档的关系来描述 STP 的范围。通常,你可以简单地引用 STC 中的范围部分。

12.5.3.3 参考文献

和往常一样,提供任何与 STP 相关的外部文档链接(例如 STC)。标准 829 还建议包括与此过程相关的各个测试用例的链接。然而,只有当 STP 中只包含少量测试过程时,这才有意义。在这种修订后的格式中,STP 将在第三部分(“测试过程”)中将测试用例链接附加到各个测试过程。如果你有一个由多个独立应用程序组成的非常大的系统,你可能会为每个应用程序准备独立的 STP。在 STP 文档的这一部分,你应提供指向其他 STP 的链接。

12.5.3.4 描述的符号表示法

如同在 STC 中一样,你将在此描述你的 STP 标签格式。本书建议使用形如projSTPxxx的 STP 标签,其中proj是某个特定项目的 ID(例如DAQPOOL),xxx是某个唯一的(可能是十进制的)数字序列。

请注意,STC 测试用例与 STP 测试过程之间是多对一的关系。因此,你不能轻易地将可追溯性信息嵌入 STP 标签中(在 SDD 标签中也存在类似情况;参见“SDD 可追溯性与标签”第 245 页)。因此,重要的是在每个测试过程中附加相关的 STC 标签,以便便于追溯回对应的测试用例。

12.5.3.5 与其他文档的关系

在 STP 的修改版本中,我已删除此节。标准 829 建议使用它来描述此 STP 与其他测试过程文档的关系——具体而言,哪些测试过程必须在其他测试过程之前或之后执行。然而,在修改后的版本中,所有测试过程都出现在同一文档中。因此,测试之间的关系描述应随每个单独的测试过程一起出现。(这些信息出现在“特殊要求”部分。)

这是在 STP 的修改版本中包括此部分的原因之一:非常大的系统可能包含多个(且相对独立的)软件应用程序。每个应用程序可能都有单独的 STP 文档。修改后的 STP 的这一部分可以描述此 STP 与其他 STP 的关系,包括测试必须执行这些 STP 的顺序。

12.5.3.6 测试运行说明

本节应包含对执行测试的人员的通用说明。通常,执行测试的人员并非软件开发人员。^(18) 本节可以为那些从未日常使用软件并从头开始接触的人提供关于待测试软件的见解。

这里应该包含的一项重要信息是,如果测试程序失败,应怎么做。测试人员是否应该尝试继续该测试程序(如果可能),希望找到更多问题?测试人员是否应立即暂停测试,直到开发团队解决问题?如果测试已被暂停,恢复测试的过程是什么?例如,大多数 QA 团队至少要求重新从头开始运行测试程序^(19)。有些 QA 团队可能还要求与开发团队开会,确定一组回归测试,以便在从故障点恢复测试程序之前进行运行。

本节还应讨论如何记录测试过程中出现的任何问题/异常,并描述在发生关键性或灾难性事件时,如何将系统恢复到稳定状态或将其关闭。

这里还应该描述如何记录测试程序的成功执行。测试人员通常会记录他们开始测试的日期和时间,提供测试工程师的姓名,并指定他们正在执行的测试程序。测试成功完成后,大多数测试程序要求测试工程师、可能的 QA 或客户代表,以及可能的其他管理人员或项目相关人员签字。本节应描述获取这些签名以及对测试程序成功执行进行签字确认的过程。

12.5.4 测试程序

本文档的这一部分会为每个单独的测试程序重复,针对系统下的每个测试程序进行描述。这是 Std 829 STP 的修改版,后者仅描述文档中的单个(或少数几个)测试程序。假设如果系统需要大量测试程序,可能会有多个 STP 文档。

12.5.4.1 简要描述(用于测试程序 #1)

这是测试程序的标题。它应为简短的短语,如DIP 开关 #1 测试,提供一个快速且可能是非正式的程序标识。

程序标识

这是该测试程序的唯一标识符(标签)。其他文档(例如 RTM)将通过其标签引用此测试程序。

目的

这是对该测试程序的扩展描述:它的存在目的、测试内容及其在大局中的位置。

本程序覆盖的测试用例列表

本节提供回溯追踪到 STC 文档的能力。它仅仅是列出了此测试程序覆盖的所有测试用例。请注意,这些测试用例应与其他测试程序中的测试用例集互相独立——任何测试用例标签不应出现在多个测试程序中。你需要保持测试用例与测试程序之间的多对一关系。这有助于保持 RTM 的整洁,这样你就不需要将多个测试程序附加到 RTM 的同一行。

现在,很可能多个测试程序会提供输入(并验证相应的结果),这些输入测试的是同一个测试用例。这不是问题;只需选择一个程序来负责覆盖该测试用例,并将测试用例分配给该程序。当有人在追踪需求并验证测试程序是否覆盖了特定需求时,他们不会在乎测试程序是否多次测试同一需求;他们关心的只是确定该需求至少在测试程序中的某个地方被测试过一次。

如果你有多个测试程序可供选择,以便将给定的测试用例与之关联,最好将该测试用例包含在一个也处理相关测试用例的测试程序中。当然,通常这种关联——即将相关的测试用例放入同一测试程序——是自动发生的。这是因为你不是随意创建测试程序并将测试用例分配给它们,而是选择一组(相关的)测试用例,并使用它们来生成测试程序。

特殊要求

本节识别执行测试程序所需的任何外部资源,以便成功地执行测试。这包括数据库、输入文件、现有的目录路径、在线资源(如网页)、动态链接库及其他第三方工具,以及自动化测试程序。

运行程序前需要的设置

本节描述了在运行测试程序之前需要执行的任何过程或步骤。例如,一个针对自动驾驶车辆软件的测试程序可能要求操作员将车辆开到测试赛道上的指定起点,然后再开始测试。其他例子可能包括确保互联网或服务器连接可用。在 SPM 中,设置的一个例子可能包括确保测试夹具(五加仑水桶)被加满至某个指定的水位。

此执行的软件下载版本号

这是测试程序中的一个“填写空白”字段。它不要求指定运行测试的特定软件版本;而是测试人员在执行测试之前输入当前的软件版本号。请注意,这个字段必须为每个测试程序填写。你不能仅为整个 STP 写一次这个值。原因很简单:在测试过程中,可能会遇到需要暂停测试的缺陷。一旦开发团队修复了这些缺陷,测试通常会从测试程序的开始处恢复。由于 STP 中的不同程序可能是在不同的软件版本上运行的,因此你需要在运行每个程序时标明所使用的软件版本号。^(20)

运行此程序所需的详细步骤

本节包含执行测试程序所必需的步骤。测试程序中有两种类型的步骤:操作和验证。操作是需要完成的工作项,例如向系统提供某些输入。验证则涉及检查某些结果/输出,并确认系统是否正常运行。

必须对所有程序步骤进行连续编号——通常从 1 开始,虽然你也可以使用像 3.2.1 到 3.2.40 这样的章节编号,适用于具有 40 个步骤的测试程序。至少,每个验证步骤前应有大约三个下划线字符(___)或一个框符号(见图 12-3),以便测试人员在成功完成步骤后可以勾选该步骤。有人倾向于在测试程序中的每个项目(即操作和验证)上都放置复选框,以确保测试人员在完成每个步骤时进行标记。也许可以在操作上使用横线,在验证上使用复选框。然而,这会给过程增加相当多的繁琐工作,因此请仔细考虑是否值得这样做。

image

图 12-3:在验证语句上使用复选框

请注意,详细步骤应包括以下信息(在适当的位置):

  • 启动程序所需的任何操作(显然,这些应出现在程序的前几步中)

  • 如何进行测量或观察输出的讨论(不要假设测试人员对软件的熟悉程度与开发人员一样高)

  • 如何在测试程序结束时关闭系统,并使系统保持稳定状态(如果需要,这显然会出现在程序的最后步骤中)

  • 签署

在测试程序结束时,应留出空行供测试人员、观察员、客户代表和可能的管理人员签署,以确认测试程序的成功完成。签名和日期是应出现的最基本信息。每个组织可能会规定哪些签名是必需的。至少(例如在一人作业的情况下),执行测试程序的人应该签名并注明日期,以确认程序已执行。

12.5.5 一般规定

STP 的最后一部分是一个通用的“所有内容包含”部分,可以在这里放置无法归入其他部分的信息。

12.5.5.1 文档变更程序

许多组织已制定了更改测试程序文档的政策。例如,他们可能要求在对 ATP 进行正式更改之前获得客户批准。本节概述了对 STP 进行更改所需遵循的规则及批准程序和流程。

12.5.5.2 附件和附录

通常,将大型表格、图像和其他文档直接附加到 LTP 上是有用的,这样读者可以随时查看,而不是提供一个无法访问的文档链接。

12.5.6 索引

如果需要,可以在 STP 的末尾添加索引。

12.5.7 软件测试程序示例

本节展示了 DAQ DIP 开关项目的一个简化(出于篇幅/编辑目的)的 STP 示例。

1 目录

[由于篇幅原因省略]

2 引言

2.1 文档标识符

2018 年 3 月 22 日:DAQ_LTP,版本 1.0 Randall Hyde

2.2 范围

本文档描述了 DAQ 系统中一些 DIP 开关的测试程序(由于篇幅/编辑原因进行了简化)。

2.3 术语表、缩写词和缩写

注意

这是一个非常简单且简短的示例,用以保持本书的简洁。请不要将其作为模板;你应当认真挑选出文档中使用的术语和缩写,并将它们列在此部分。

术语 定义
DAQ 数据采集系统
SBC 单板计算机
软件设计描述(SDD) 软件系统设计的文档(IEEE Std 1016-2009)——也就是这本文档。
软件需求规格说明书(SRS) 记录软件及其外部接口的基本需求(功能、性能、设计约束和属性)的文档(IEEE Std 610.12-1990)。
系统需求规格说明书(SyRS) 一个结构化的信息集合,体现了系统的需求(IEEE Std 1233-1998)。这是一个记录系统或子系统设计依据和概念设计需求的规范。
软件测试用例(STC) 描述测试用例(输入和结果)以验证软件在各种设计问题/需求下正确操作的文档(IEEE Std 829-2009)。
软件测试程序(STP) 记录描述逐步执行一组测试用例以验证软件在各种设计问题/需求下的正确操作的文档(IEEE Std 829-2009)。

2.4 参考文献

参考 讨论
DAQ STC 请参见“软件测试用例文档示例”在第 281 页。
DAQ STP 适用于 Plantation Productions DAQ 系统的完整 STP 示例,请参见www.plantation-productions.com/Electronics/DAQ/DAQ.html
IEEE Std 830-1998 SRS 文档标准
IEEE Std 829-2008 STP 文档标准
IEEE Std 1012-1998 软件验证与确认标准
IEEE Std 1016-2009 SDD 文档标准
IEEE Std 1233-1998 SyRS 文档标准

注意

一个可能有用的附加参考(由于该项目较简单,未包含此参考)是与 DAQ 系统相关的任何文档链接,例如编程手册或电路图。

2.5 描述的符号说明

本文档中的测试程序标识符(标签)应采取以下形式:

DAQ_STP_xxx

其中xxx是一个(可能带小数点的)数字序列,通过整个序列创建一个唯一的标识符。请注意,STP 标签的xxx值通常从 000 或 001 开始,每增加一个测试案例项时,xxx字符串通常递增 1。

2.6 执行测试的说明

按照说明精确执行每个测试程序。如果测试人员在程序中遇到错误或遗漏,应使用红色墨水(仅限于标记修改时使用红色墨水)标出正确的信息,并在测试日志中对该修改进行说明(包括日期/时间戳和签名)。测试程序中的所有红线修改必须由所有签署人在测试程序末尾初始化。

如果测试人员发现软件本身存在缺陷(即不仅仅是测试程序中的缺陷),测试人员应在测试日志中记录该异常,并为该缺陷创建一个异常报告。如果缺陷是边际性的或可以忽略不计的,测试人员可以继续执行测试程序(如果可能的话),并尝试在同一测试程序运行中发现其他缺陷。如果缺陷是关键性的或灾难性的,或者缺陷使得无法继续执行测试程序,测试人员应立即暂停测试并关闭系统电源。一旦缺陷被修复,测试人员必须从程序开始重新启动测试程序。

测试程序只有在测试人员完成所有步骤且没有任何失败时才能成功。

3 测试程序

3.1 RS-232(串行端口)操作

3.1.1 DAQ_STP_001

3.1.2 目的

本测试程序测试从 RS-232 端口发送的 DAQ 命令的正确操作。

3.1.3 测试案例

DAQ_STC_701_000_000

DAQ_STC_702_000_000

DAQ_STC_703_000_000

DAQ_STC_726_000_000

3.1.4 特殊要求

本测试程序要求在 PC 上运行一个串行终端仿真程序(例如,Netburner SDK 的一部分,名为MTTY.exe的程序;如果你有强烈的自虐倾向,也可以使用 Hyperterm)。PC 的串行端口与 Netburner 的 COM1 端口之间应该连接一根 NULL 调制解调器电缆。

3.1.5 在执行前的设置要求

Netburner 已开机并运行应用软件。串行终端程序应与 PC 的串行端口正确连接,而该端口又连接到 Netburner。

3.1.6 软件版本号

版本号: ____________

日期: ____________

3.1.7 详细步骤

1. 将 DIP 开关 1 设置为开启位置。

2. 重置 Netburner,并等待几秒钟,直到其完成重启。注意:重启 Netburner 时可能会在串行终端上产生信息,可以忽略这些信息。

3. 在终端仿真器的单独一行按下 ENTER 键。

4. ______ 验证 DAQ 系统是否仅响应一个换行符且没有其他输出。

5. 输入 help,然后在单独的一行按下 ENTER 键。

6. ______ 验证 DAQ 软件是否响应帮助信息(内容不重要,只要明显是帮助响应)。

7. 将 DIP 开关 1 设置为 OFF 位置。

8. 重置 Netburner,并等待几秒钟,直到其完成重启。注意:重启 Netburner 时可能会在串行终端上产生信息,可以忽略这些信息。

9. 在串行终端中输入帮助命令。

10. ______ 验证 DAQ 系统是否忽略了帮助命令。

3.1.8 测试程序签字确认

测试员: _________________ 日期: _________

QA: _________________ 日期: _________

注意

在完整的 STP 文档中,可能会有更多的测试程序;以下测试程序忽略了这一可能性,并继续以 DAQ_STP_002 标号编号。

3.2 以太网地址选择

3.2.1 DAQ_STP_002

3.2.2 目的

此测试程序测试基于 DIP 开关 5 和 6 的以太网 IP 地址初始化。

3.2.3 测试用例

DAQ_STC_709_000_000

DAQ_STC_710_000_000

DAQ_STC_711_000_000

DAQ_STC_712_000_000

3.2.4 特殊要求

此测试程序需要在 PC 上运行一个以太网终端仿真器程序(Hercules.exe 过去是一个不错的选择)。PC 的以太网端口与 Netburner 的以太网端口之间应有一根以太网(交叉或通过集线器)电缆连接。

3.2.5 运行前的设置要求

Netburner 已开启并运行应用软件。DIP 开关 3 在 ON 位置,DIP 开关 4 在 OFF 位置。

3.2.6 软件版本号

版本号: _________

日期: _________

3.2.7 详细步骤

1. 将 DIP 开关 5 和 6 设置为 OFF 位置。

2. 重置 Netburner,并等待几秒钟,直到其完成重启。

3. 从以太网终端程序尝试连接到 IP 地址 192.168.2.70,端口 20560 (0x5050) 的 Netburner。

4. 验证连接是否成功。

5. 输入 help 命令并按下 ENTER 键。

6. ______ 验证 DAQ 系统是否以适当的帮助信息做出响应。

7. 将 DIP 开关 5 设置为 ON 位置,6 设置为 OFF 位置。

8. 重置 Netburner,并等待几秒钟,直到其完成重启。

9. 从以太网终端程序尝试连接到 IP 地址 192.168.2.71,端口 20560 (0x5050) 的 Netburner。

10. ______ 验证连接是否成功。

11. 输入 help 命令并按下 ENTER 键。

12. ______ 验证 DAQ 系统是否以适当的帮助信息做出响应。

13. 将 DIP 开关 5 设置为 OFF 位置,6 设置为 ON 位置。

  1. 重置 Netburner,并等待几秒钟直到其完成重启。

  2. 从以太网终端程序尝试连接到 IP 地址为 192.168.2.72,端口为 20560(0x5050)的 Netburner。

  3. ______ 验证连接是否成功。

  4. 输入help命令并按下 ENTER 键。

  5. ______ 验证 DAQ 系统是否响应适当的帮助信息。

  6. 将 DIP 开关 5 和 6 置于 ON 位置。

  7. 重置 Netburner,并等待几秒钟直到其完成重启。

  8. 从以太网终端程序尝试连接到 IP 地址为 192.168.2.73,端口为 20560(0x5050)的 Netburner。

  9. ______ 验证连接是否成功。

  10. 输入help命令并按下 ENTER 键。

  11. ______ 验证 DAQ 系统是否响应适当的帮助信息。

3.2.8 测试程序签署

测试者: _________________ 日期: _________

QA: _________________ 日期: _________

注意

在完整的 STP 文档中,这里可能会有更多的测试程序。

4 一般

4.1 文档变更程序

每次对本文档进行修改时,至少在 2.1 节列表中添加一行,列出日期、项目名称(DAQ_STP)、版本号和作者信息。

4.2 附件和附录

[为了节省空间,本文中未提供;在实际的 STP 中,提供 DAQ 系统的原理图会是个好主意。]

5 索引

[由于空间原因,已省略。]

12.5.8 使用 STP 信息更新 RTM

因为 STP 标签与 SDD 标签在性质上非常相似,所以将 STP 标签添加到 RTM 的过程也与将 SDD 标签添加到 RTM 的过程非常相似(见“使用设计信息更新可追溯性矩阵”,第 259 页)。

STP 在 RTM 中添加了一个新的列:STP 标签列。不幸的是,STP 标签并不直接嵌入任何可追溯性信息,因此你需要从 STP 中提取这些信息,以确定在 RTM 中放置 STP 标签的位置。

如你在第 294 页的“本程序涵盖的测试用例列表”中所看到的,每个 STP 中的测试程序必须包括其涵盖的测试用例列表。尽管标准 829 并未要求此项内容,但我强烈建议你添加这一部分。如果你已经做了这一点,那么你已经创建了反向可追溯性,回溯到需求中,这使得在 RTM 中填写 STP 标签变得更加容易。为此,只需定位每个测试用例标签(列在当前测试程序中),然后将测试程序的 STP 标签复制到 RTM 中的 STP 标签列(与对应测试用例在同一行)。当然,因为一个测试程序可能与多个测试用例相关联,所以你还会在 RTM 中多次复制相同的 STP 标签(每个相关的测试用例一个)。

如果你希望轻松地将你的 STP 标签追溯到 RTM 中的所有需求,尤其是无需查阅 STP 列表,只需按 STP 标签列排序 RTM 即可。这将把所有需求(以及与该 STP 标签相关的所有内容)汇集到矩阵中的一个连续组中,方便识别与该标签相关的所有内容。

如果你选择一种不涉及在测试程序中嵌入 STC 标签的其他方法来指定测试用例,那么在 RTM 中确定 STP 标签的位置就成了一个手动——且往往非常繁琐——的过程。因此,我强烈建议在首次创建测试程序时就包含 STC 标签编号。

12.6 级别 测试日志

尽管每个测试程序都包含一个签名部分,测试人员(以及任何其他相关人员)可以在其中签署成功完成测试,但仍需要一个单独的测试日志来处理在测试过程中发生的异常,或仅仅记录测试人员在执行测试程序时的评论和疑虑。

也许这个级别的测试日志 (LTL) 最重要的任务是呈现测试过程的时间顺序视图。每当测试人员开始执行测试程序时,他们应首先记录一个条目,说明日期、时间、执行的测试程序及其姓名。在测试执行过程中,测试人员可以根据需要向测试日志中添加条目,指示:

  • 测试程序开始(日期/时间)

  • 测试程序结束(日期/时间)

  • 发现的异常/缺陷(以及测试是否继续或暂停)

  • 由于在测试程序本身中发现错误,需要对测试程序进行修改/变更(例如,测试程序可能列出了不正确的结果;如果测试人员能够证明即使程序输出与测试程序不同,输出也是正确的,他们会修改测试程序并在测试日志中添加适当的理由)

  • 测试人员对程序输出结果的担忧(也许测试程序没有列出任何结果,或者测试程序的结果令人质疑)

  • 人员变动(例如,如果测试人员在测试过程中因休息、轮班或需要不同的经验而更换)

  • 测试程序中的任何休息期间(例如,午休或下班时间)

从技术上讲,测试日志所需的仅仅是一张(最好是有行的)纸张。更多时候,STP 创建者会在 STP 的末尾添加几张有行纸,专门用于记录测试日志。有些组织直接使用文字处理软件或文本编辑器(甚至是专门编写的应用程序)电子化维护测试日志。当然,Std 829 对测试日志提出了正式的建议:

1 引言

1.1 文档标识符

1.2 范围

1.3 参考资料

2 细节

2.1 描述

2.2 活动和事件条目

3 一般

3.1 词汇表

12.6.1 在 Level 测试日志文档中的介绍

除了引入以下的小节外,本节还可能会识别创建该文档的组织和当前状态。

12.6.1.1 文档标识符

本文档的唯一标识符;与所有 Std 829 文档一样,至少应包括日期、一些描述性名称、版本号和作者信息。这里还可能会出现变更历史(仅限大纲/格式,而非具体日志)。

12.6.1.2 范围

范围部分总结了相关测试过程所测试的系统和功能。通常情况下,这将引用测试过程的范围部分,除非此次测试运行有特殊之处。

12.6.1.3 参考

至少,本节应提及创建此测试日志的 STP(特别是具体测试)文档。

12.6.2 详情

本节引入了以下小节,是大多数人认为的“测试日志”。

12.6.2.1 描述

本节(每个测试日志中仅出现一次)描述了适用于所有测试日志条目的内容。这可能包括以下内容:

  • 识别测试对象(例如,通过版本号)

  • 识别在测试之前对测试过程所做的任何更改(例如,修订线)

  • 测试开始的日期和时间

  • 测试结束的日期和时间

  • 执行测试的测试员姓名

  • 解释为何测试被暂停(如果发生此情况)

12.6.2.2 活动与事件条目

这部分测试日志记录了测试过程执行期间的每个事件。此部分(包含多个条目)通常记录以下内容:

  • 测试过程执行的描述(过程 ID/标签)

  • 所有观察/参与测试的人员——包括测试员、支持人员和观察员——以及每个参与者的角色

  • 每个测试过程执行的结果(通过、失败、评论)

  • 记录测试过程中偏离测试程序的任何情况(例如,修订线)

  • 记录测试过程中发现的任何缺陷或异常(如果生成了异常报告,则附上相关报告的参考)

12.6.3 术语表

这部分 LTL 文档包含与所有 Std 829 文档相关的常见术语表。

12.6.4 关于测试日志的几点评论

说实话,Std 829 大纲对于这样一个简单的任务来说实在是太繁琐了。关于如何管理此文档中涉及的工作量,有一些技巧。

12.6.4.1 管理开销

几乎所有创建符合 Std 829 LTL 大纲要求的文档所需的工作,都可以通过将测试日志直接附加到 STP 的末尾来消除。测试日志继承了 STP 中的所有前言信息,因此你只需要记录出现在“级别测试日志”部分的内容,如 第 303 页 所示。

请注意,LTL 有四种变体,这是所有 Std 829 级别文档的典型特征:组件测试日志(也称为单元测试日志)、组件集成测试日志(也称为集成测试日志)、系统测试日志(也称为系统集成测试日志)和验收测试日志(可能包括工厂验收测试日志或现场验收测试日志)。^(21)

实际上,很少会有很多组件或组件集成测试日志。最常见的是,相应的测试程序是自动化测试。即使不是,开发团队通常也会运行这些测试并立即修复发现的任何缺陷。因为这些测试通常频繁运行(尤其是在使用敏捷方法的团队中,往往一天运行多次),所以记录这些测试运行的开销非常大。

系统测试日志和验收测试日志是测试人员(独立于开发团队)执行的 LTL 变体,因此需要创建实际的测试日志。

12.6.4.2 记录保存

测试日志与其他 Std 829 文档在根本上有所不同。大多数 Std 829 文档是静态文档;你做的几乎唯一的事情就是填写像软件版本号这样的详细信息,并勾选验证步骤。如果你重复运行程序,文档的基本结构不会改变。最终,没必要保留任何旧版的测试程序(例如,在执行过程中失败的测试运行)。你真正需要向客户展示的是最后一次成功执行所有步骤并通过整个程序的测试运行。

测试日志,与本章之前提到的其他文档不同,是动态文档。它们在每次测试运行时都会发生显著变化(即使没有其他变化,至少所有日期和时间戳都会变化)。此外,测试日志不是一个模板文档,你只需填写一些空白并勾选一些复选框。它本质上是一个空白的页面,你在实际运行测试时创建它。如果有失败、红线或注释,测试日志会记录这些事件的历史。因此,保存所有测试日志非常重要,即使是记录了失败测试的日志。几乎没有任何系统是完美的;在测试过程中会发现错误和缺陷。测试日志提供了你已经发现、修正并重新测试这些缺陷的证据。

如果你丢弃了所有记录发现的缺陷的旧测试日志,只展示完美的测试日志,那么任何理智的客户都会质疑你在隐藏什么。错误和缺陷是过程中的正常部分。如果你没有展示你已经发现并修正了这些错误,客户会认为你没有足够测试系统以发现缺陷,或者认为你伪造了测试日志。保留旧的测试日志!这证明你为你的产品做了质量保证的尽职调查。

你可以争辩说,保留旧的测试程序以显示测试过程中出现的红线或中断也是很重要的。然而,任何出现在测试程序文档中的红线或中断最好也出现在相应的测试日志中,这样你就不需要保留那些你实际上已经重新执行过的旧测试程序。

注意,这并不意味着你执行过的所有测试程序都应该是完美的。如果你已经适当记录并解释了测试程序中的红线,并且测试执行成功地完成了,那么就没有必要重新编写测试程序,并重新填写所有复选框来在最终文档中包含一个干净的测试程序。如果测试是成功的,即使有红线,也不要做任何修改。^(22) 红线并不意味着软件系统的失败;当然,它们是缺陷,但它们出现在测试程序本身,而不是软件上。测试程序的目标是测试软件,而不是测试程序。如果测试程序只有轻微的变更,就把它标记为红线并继续进行。

在许多组织中,正如我之前所说的,如果测试程序中的任何验证步骤失败,那么在缺陷修复后,整个程序必须从头开始执行(即完全回归测试)。对于一些测试程序或某些组织,可能有一种流程,可以暂时中止测试程序,更新软件,然后在解决缺陷后恢复测试程序。在这种情况下,你可以将验证失败的步骤视为红线:在测试日志中记录原始的失败,记录开发团队修复缺陷的事实,然后记录软件的新版本在失败的验证步骤中正确运行的情况。^(23)

12.6.4.3 纸质日志与电子日志

有些人喜欢创建电子测试日志;有些组织或客户要求纸质测试日志(填写时使用钢笔,而非铅笔)。电子日志的问题在于(尤其是当你使用文字处理软件而不是专门设计的测试过程记录应用程序时),它们很容易被伪造。当然,没有优秀的程序员会伪造测试日志。然而,这个世界上确实有一些水平不高的程序员伪造过测试日志。不幸的是,这些少数人的行为污损了所有软件工程师的声誉。因此,最好是创建不容易伪造的测试日志,而这通常意味着使用纸质日志。

有人可能伪造纸质日志;然而,这需要更多的工作,并且通常更容易被察觉。最终,客户可能会希望获得测试日志的纸质副本;当他们需要电子版时,他们可能希望得到纸质日志的扫描图像。出于法律原因,他们会期望你将这些纸质日志存档保存。

也许最好的解决方案是使用专门为创建测试日志而设计的软件应用程序,该应用程序自动将条目记录到数据库中(使伪造数据变得更困难)。对于客户,你可以从数据库中打印报告提供纸质副本(或者如果客户需要电子副本,则生成 PDF 报告)。

无论测试人员如何生成原始测试日志,大多数组织都会要求他们最终创建纸质测试日志,然后测试人员、观察员和其他与测试运行相关的人员必须签名并注明日期,以证明信息的正确性和准确性。此时,这已成为一份法律文件;任何试图伪造数据的人都可能面临严重的法律风险。

12.6.4.4 纳入到 RTM 中

通常,测试日志不会出现在可追溯性矩阵中。然而,完全没有理由不能将它们包含在其中。测试过程(因此,也包括 STP)与测试日志之间存在一对多的关系。因此,如果你为每个测试报告分配一个唯一标识符(标签),你可以将该标识符添加到 RTM 中的适当列中。

由于测试日志与测试过程之间存在多对一关系,因此可以参考本书中呈现的其他标签 ID 来建模。例如,可以使用类似以下格式的标签:proj_TL_xxx_yyy,其中xxx来自测试过程标签(例如,005来自DAQ_STP_005),而yyy是一个(可能是十进制的)数字序列,用以为测试日志创建唯一的标签。

12.7 异常报告

当测试人员、开发团队成员、客户或其他任何使用系统的人发现软件缺陷时,正确的文档记录方式是通过异常报告(AR),也叫缺陷报告。常见的情况是,异常报告只是某人告诉程序员:“嘿,我在你的代码里发现了问题。”然后程序员跑到自己的机器上修复问题,然而并没有文档来追踪异常。这是非常不幸的,因为追踪系统中的缺陷对于维护系统质量至关重要。

异常报告是跟踪系统缺陷的正式方式。它捕获以下信息:

  • 缺陷发生的日期和时间

  • 发现缺陷的人(或至少是响应某用户投诉记录缺陷报告的人)

  • 缺陷的描述

  • 在系统中重现缺陷的过程(假设问题是确定性的,并且足够容易重现)

  • 缺陷对系统的影响(例如,灾难性、关键性、边际性、可忽略)

  • 缺陷对最终用户的重要性(经济和社会影响),以便管理层可以为其分配修复优先级

  • 针对缺陷的任何可能的临时解决方案(以便用户在开发团队修复缺陷时仍能继续使用系统)

  • 对如何修复缺陷的讨论(包括有关缺陷的建议和结论)

  • 异常的当前状态(例如,“新异常”,“开发团队正在修复中”,“正在测试”,“在软件版本xxx.xxx中修复”)

自然,Std 829 为异常报告提供了建议的大纲。然而,大多数组织使用缺陷追踪软件来记录缺陷或异常。如果你不愿意花钱购买商业产品,有许多开源产品是免费的,例如 Bugzilla。这些产品中的大多数使用的数据库结构合理地与 Std 829 的建议兼容:

1 引言

1.1 文档标识符

1.2 范围

1.3 参考资料

2 详情

2.1 摘要

2.2 发现缺陷的日期

2.3 上下文

2.4 异常描述

2.5 影响

2.6 发起人对紧急性的评估(参见 IEEE 1044-1993 [B13])

2.7 整改措施描述

2.8 异常状态

2.9 结论与建议

3 总则

3.1 文档变更程序与历史

12.7.1 异常报告文档中的引言

以下子节描述异常报告引言的组成部分。

12.7.1.1 文档标识符

这是一个独特的名称,其他报告可以引用它(例如测试日志和测试报告)。

12.7.1.2 范围

范围部分简要描述了异常报告中没有出现的任何内容。

12.7.1.3 参考资料

参考资料包括指向其他相关文档的链接,例如测试日志和测试程序。

12.7.2 详细信息

本节介绍后续的子节。

12.7.2.1 总结

在此简要描述异常。

12.7.2.2 异常发现日期

列出异常发现的日期(如可能/适当,提供时间)。

12.7.2.3 背景

软件版本和安装/配置信息应在背景部分中提供。本节还应提及相关的测试程序和测试日志(如果适用),这应有助于识别此异常。如果此异常没有现有的测试程序,考虑建议在某些测试程序中增加内容,以捕捉此类异常。

12.7.2.4 异常描述

提供缺陷的详细描述,包括(如果可能)如何复现该缺陷。描述可能包括以下信息:

  • 输入

  • 实际结果

  • 结果(特别是,与测试程序中预期结果不同的结果)

  • 故障的过程步骤

  • 环境

  • 缺陷是否可重复?

  • 故障发生前执行的任何测试,这些测试可能影响结果

  • 测试者

  • 观察者

12.7.2.5 影响

描述此缺陷对系统用户的影响。描述任何可能的解决方法,例如更改文档或修改系统的使用方式。如果可能,估算修复该缺陷的成本和时间,以及留下该缺陷所带来的风险。估算修复该缺陷的风险,这可能会影响其他系统功能。

12.7.2.6 提出者对紧急性的评估

说明修复的紧急程度。建议使用“完整性等级与风险评估”中的完整性等级和风险评估尺度,具体见第 263 页,这可能是描述修复紧急程度的最小机制。

12.7.2.7 修正措施说明

本节描述确定缺陷原因所需的时间;修复缺陷的时间、成本和风险估算;以及重新测试系统所需的努力估算。包括任何必要的回归测试,以确保修复不会破坏其他内容。

12.7.2.8 异常状态

列出当前缺陷的状态。Std 829 推荐的状态包括“未解决”、“已批准修复”、“已分配解决”、“已修复”和“已测试并确认修复”。

12.7.2.9 结论与建议

本节应提供关于缺陷发生原因的评论,并建议可能的开发过程改进,以防止未来发生类似缺陷。本节还可能建议额外的需求、测试用例和(修改后的)测试程序,以在未来捕捉该异常;这在测试过程中意外发现异常时尤为重要,而不是通过运行特定的测试程序步骤来捕捉该缺陷。

12.7.2.10 一般信息

这是 Std 829 文档中的通常结尾部分,提供了变更历史(针对 AR 格式,而不是特定 AR)和变更程序。Std 829 不推荐使用术语表。

12.7.3 关于异常报告的几点评论

处理异常报告时,牢记以下几点是值得的。

12.7.3.1 ARs 不会进入 RTM

可追溯性矩阵的目的是能够追踪设计和测试的需求,以确保系统成功地满足所有需求。虽然有人可能会争辩说测试日志应该放入 RTM,但大多数人并不愿意将它们放进去,因为他们通常会将测试日志直接附加到完成的测试程序上。

异常报告(ARs),另一方面,并不是你试图证明其存在的东西;事实上,在一个完美的世界里,你试图反驳异常的存在。这并不意味着你要丢弃 ARs。就像测试日志一样,保留所有旧的 ARs 是非常重要的——它们提供了你在测试系统时已尽职尽责的宝贵证据。更重要的是,你需要保留 ARs 以备回归测试使用。有时,在一个缺陷被发现并修正之后,它可能会再次出现在系统中。拥有 ARs 的历史记录可以让你回溯并检查最初的原因及其解决方案。

12.7.3.2 电子 ARs 与纸质 ARs

正如本章之前提到的,大多数组织使用缺陷跟踪系统来捕捉和跟踪 ARs。尽管 Std 829 并没有特别建议或要求纸质文档(实际上,Std 829 指出你可以使用软件来跟踪异常),但其大纲形式通常暗示着硬拷贝格式。但考虑到大多数组织使用缺陷跟踪软件,为什么还要使用纸质 ARs 呢?主要原因是便携性,即“你可以随身携带”。虽然在系统集成、工厂验收测试以及其他在开发站点进行的测试中,使用缺陷跟踪系统非常合理,因为可以方便地访问该跟踪器,但在某些情况下,在现场验收测试期间,它可能在安装现场不可用或无法访问。^(24) 在这种情况下,创建纸质 ARs 并在可能时将其录入缺陷跟踪系统,可能是最佳的做法。

12.8 测试报告

测试完成后,测试报告总结了测试结果。对于许多其他测试文档,Std 829 描述了你可以生成的各种测试报告。Std 829 定义了级别中期测试状态报告(LITSR)、级别测试报告(LTR)和主测试报告(MTR)。当然,你可以用组件组件集成系统验收代替级别(当然也可以使用常见的名称)。

一个非常大的组织可能需要生成中期测试报告,以便管理层了解一个同样庞大的系统的情况。有关 LITSR 的更多信息,请参阅 IEEE Std 829-2008;坦率地说,它们对于大多数项目来说只是为了文档而生成文档,但大型政府合同可能会明确要求它们。

级别 和主测试报告的格式根据项目的大小而有所不同。大多数小型到中型系统(通常只有一个软件应用程序,因此只有一个 STP)将只有一个测试报告,如果有的话。

一旦系统发展到包含多个主要软件应用程序的规模,通常会为每个主要应用程序准备一个测试报告,然后有一个 MTR 作为对各个测试报告结果的总结。因此,MTR 提供了一个高层次的回顾,对所有测试进行了汇总。

12.8.1 简要提及主测试报告

由于 MTR 通常不是个别开发人员处理的文档,本节将简要展示 Std 829 建议的大纲,而不作进一步评论,随后集中讨论 LTRs。

1 引言

1.1 文档标识符

1.2 范围

1.3 参考文献

2 主测试报告的详细信息

2.1 所有汇总测试结果概述

2.2 决策依据

2.3 结论与建议

3 一般事项

3.1 术语表

3.2 文档变更程序和历史

关于 MTR 的更多信息,请参见 IEEE Std 829-2008。

12.8.2 级别 测试报告

尽管你可能有组件/单元测试报告和组件集成测试报告,但大多数组织将单元测试和集成测试交由开发部门处理,因为高层管理通常不关心这些低级细节。因此,你最常见的级别 测试报告 (LTRs) 将是系统(集成)测试报告和验收测试报告,通常是工厂验收测试报告和现场验收测试报告。Std 829 对 LTRs 的定义如下:

1 引言

1.1 文档标识符

1.2 范围

1.3 参考文献

2 详细信息

2.1 测试结果概述

2.2 详细测试结果

2.3 决策依据

2.4 结论与建议

3 一般事项

3.1 术语表

3.2 文档变更程序和历史

第一部分(“引言”)和第三部分(“一般事项”)与本章中大多数其他 Std 829 测试文档相同。测试报告的核心部分在第二部分(“详细信息”)。以下小节描述了其内容。

12.8.2.1 测试结果概述

本节是对测试活动的总结。它将简要描述测试覆盖的功能、测试环境、软件/硬件版本号以及其他与测试相关的常规信息。概述还应提到测试环境是否有任何特殊之处,如果测试在不同环境(如工厂)中进行,会得到不同的结果。

12.8.2.2 详细测试结果

在本节中总结所有的测试结果。列出所有发现的异常及其解决方案。如果某个缺陷的解决方案已被推迟,务必提供合理的解释,并讨论该缺陷对系统的影响。

如果有任何偏离测试程序的情况,需说明并为这些偏离提供理由。描述任何对测试程序的更改(红线)。

本节还应提供对测试过程的信心水平。例如,如果测试过程侧重于代码覆盖率,本节应描述测试过程中所实现的代码覆盖率的估算百分比。

12.8.2.3 决策依据

如果团队在测试过程中做出了任何决策,比如偏离测试程序或未能修正已知的异常情况,本节应提供这些决策的理由。本节还可以为得出的任何结论提供解释(在下一节中)。

12.8.2.4 结论与建议

本节应陈述测试过程得出的任何结论。本节应讨论产品是否适合发布/生产使用,并推荐可能的方案,例如禁用某些可能已知的异常功能,以便提前发布系统。此节也可以建议推迟发布,等待进一步的开发和可能的调试。

12.9 你真的需要所有这些吗?

IEEE Std 829-2008 描述了大量文档。你真的需要为你在家办公自己开发的下一个“杀手级应用”创建所有这些文档吗?当然不需要。除了最大的(政府资助的)应用程序外,Std 829 中描述的绝大多数文档都是过度的。对于普通项目,你可能只需要 STC、SRL 和 STP 文档。^25 测试日志将仅作为 STP 的附录。异常报告将是你缺陷跟踪系统中的条目(你可以从中生成纸质报告)。

你还可以通过使用自动化测试来减少 STC 和 STP 文档的大小。你可能无法消除所有手动测试,但可以去除其中的许多。

在较小的项目中,测试报告是完全可以省略的。STP 末尾的测试日志可能作为一个合理的替代方案,除非你有多个管理层要求完整的文档。

敏捷开发方法似乎是减少所有这些文档成本的一个不错选择。然而,请记住,开发、验证、验证和维护所有这些自动化测试程序也有相应的——而且往往是等同的——成本。

12.10 获取更多信息

Dingeldein, Tirena. “降低 IT 成本的 5 款最佳免费开源错误跟踪软件。”2019 年 9 月 6 日。blog.capterra.com/top-free-bug-tracking-software/

IEEE. “IEEE Std 829-2008:软件和系统测试文档的 IEEE 标准。”2008 年 7 月 18 日。standards.ieee.org/findstds/standard/829-2008.html。这很昂贵(我最后一次检查时是 160 美元),但这是黄金标准。它比 SDD 标准更易读,但仍然是阅读负担。

Peham, Thomas. “7 款用户推荐的优秀开源缺陷追踪工具。”2016 年 5 月 8 日。 usersnap.com/blog/open-source-bug-tracking/

Plantation Productions, Inc. “开源/开硬件:数字数据采集与控制系统。”无日期。 www.plantation-productions.com/Electronics/DAQ/DAQ.html。在这里你可以找到 DAQ 数据采集软件评审、软件测试用例、软件测试流程以及反向可追溯性矩阵。

Software Testing Help. “2019 年最佳缺陷追踪软件:顶级缺陷/问题追踪工具。”2019 年 11 月 14 日。 www.softwaretestinghelp.com/popular-bug-tracking-software/

Wikipedia. “缺陷追踪系统。”最后修改于 2020 年 4 月 4 日。 https://en.wikipedia.org/wiki/Bug_tracking_system

第十五章:后记:设计优秀的代码

Image

在介绍中,我解释了为什么这本书没有足够的篇幅来涵盖第二卷承诺会包含的许多主题。请期待在第四、五、六卷中看到这些内容:

  • 第四卷:设计优秀的代码

  • 第五卷:卓越编码

  • 第六卷:测试、调试与质量保证

假设我仍然活着并能完成这个系列,我可能会在书单中增加一本关于用户文档的书。唯一能承诺的事情是,第三卷和第四卷之间不会像第二卷和第三卷之间那样有那么长的间隔!

第四卷,设计优秀的代码,将接着本书后半部分的内容展开。在本卷中,你已经学会了如何记录软件开发过程;而在第四卷中,你将学习更多关于设计过程的知识,以及如何将你所学到的知识应用于设计优秀的代码。

posted @ 2025-12-01 09:44  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报