acm-header
登录

ACM通信

BLOG@CACM

我的要求是否完整?


Bertrand Meyer

多年来建立的软件工程的一些重要概念在社区中并不广为人知。这个博客的一个用途是为这些被忽视的想法提供教程。我的上一篇文章讨论了一个与项目管理相关的:the最短可能时间表属性.这里是另一个,这一次是在需求工程领域,也是基于一个我认为是经典的出版物(它有40多年的历史),但从业者几乎不知道。

在我的大多数文章中,从业者确实是目标受众。我在一开始就强调了这一点,因为如果你扫一眼文本的其余部分,你会发现它包含了一些数学公式(最可怕的是),你可能会认为“这并不适合我”。它是!数学是非常简单的,我的目标是实际的:阐明任何编写需求的人都面临的一个永恒的问题(不管是什么风格,传统的还是敏捷的):我如何才能确保需求说明书是完整的?

在某种程度上,你不能。但有一个更好的答案,一个非常简单的答案,虽然是片面的,但有帮助。

定义完整性

更好的答案叫做“充分完备性”,它来自抽象数据类型的理论。Guttag和Horning在1978年的一篇文章中介绍了它。它还隐含在一个更实际的文档中,即1998年关于如何编写需求[2]的IEEE标准。

这篇文章没有什么真正的新内容;事实上我的书面向对象软件构造[3]包含了足够完整的广泛讨论(意味着比Guttag和Horning的学术文章更广泛地可访问)。但很少有人知道这些概念;特别是,很少有从业者听说过充分完整性(如果他们听说过抽象数据类型的话)。所以我希望现在的介绍对大家有所帮助。

确定需求完整性的问题一开始看起来没有希望的原因是自然的反应:完全于什么?要知道规范是完整的,我们需要对涉众想要的所有东西和所有环境约束的更一般的描述,但这只会把问题推得更远:我们如何知道这样的描述本身是完整的?

这种反对意见在原则上是正确的:我们永远无法确定我们没有忘记某人想要的东西,或环境强加给我们的某些属性。但是,也存在着更具体、更可评估的完整性概念。

IEEE标准给出了三个完整性标准。第一个声明“所有的需求”已经被包括在内,这是无用的,因为它遇到了上面提到的逻辑悖论,而且无论如何都是同义的(如果需求包括所有的需求,那么需求就是完整的,谢谢您提供的信息!)第二种是有意义的,但兴趣有限(完整性的“官僚主义”概念):需求文档中的每个元素都被编号,每个交叉引用都被定义,等等。最后一个标准很有趣:定义在所有可实现的情况下,软件对所有可实现的输入数据类别的响应”。这是有意义的。要理解这个子句,我们需要回到充分的完整性,甚至在此之前,回到抽象数据类型。

抽象数据类型将为我们提供数学上的小探索(我们的正式的野餐在我们对需求和完整性的研究中)。如果您不熟悉这个简单的数学理论(每个软件从业者都应该知道),我希望您能从介绍和示例中受益。它们将使我们能够在回到它在需求工程中的应用之前,正式地引入充分完整性的概念。

指定抽象数据类型

抽象数据类型是面向对象编程的数学基础。事实上,OO编程、OO分析和OO设计都只是在不同抽象级别上实现这个数学概念,即使很少有OO实践者意识到它。(如果您想了解更多,请在这里重新参考[3]。)

ADT(抽象数据类型)是一组对象,其特征不是通过它们的内部属性(它们是什么),而是通过适用于它们的操作(它们有什么)以及这些操作的属性。如果您熟悉OO编程,您就会认识到,在实现级别上,这简直是一个是多少。但是这里我们讨论的是数学对象,我们不需要考虑实现。

以这种方式定义的类型(作为ADT)的一个例子是一行上的POINT概念。我们不会说这个对象是如何表示的(这是一个在规范级别无关的概念),而是说它对世界的其他部分是如何显示的:我们可以在原点上创建一个新点,要求得到一个点的坐标,或者通过一定的位移移动这个点。这个例子可能是最简单的有意义的例子,但它给出了想法。

ADT规范包含三个部分:函数、前提条件和公理。让我们看看POINT抽象数据类型的定义(暂时跳过前置条件)。

函数是描述类型的操作。有三种类型的函数,由定义中的ADT的位置定义,这里POINT出现:

  • 创建者,其中类型只出现在结果中。
  • 查询,它只出现在参数中。
  • 命令,它出现在两边。

这里只有一个创造者:

:→点

是一个不接受参数并产生一个点(原点)的函数。我们把结果写成(而不是像在())。

在OO编程中,创建者对应于类的构造函数(Eiffel中的创建过程)。和构造函数一样,创建函数也可能有参数:例如,与其总是在原点处创建一个点,我们可以这样决定创建一个具有给定坐标的点,指定它为INTEGER→point,并使用它作为(i)对于某个整数i(我们的点的坐标是整数)为了简单起见,我们选择一个没有参数的创建者。在任何情况下,这里的新类型POINT只出现在结果的一侧。

每个有用的ADT规范都至少需要一个创建器,没有它,我们将永远无法获得任何该类型的对象(这里有一些点)。

也只有一个查询:

x:点→整数

它给出了一个点的位置,写着x(p)。更一般地说,查询使我们能够获得新类型对象的属性。这些属性必须用我们已经定义的类型表示,比如这里的INTEGER。同样,必须至少有一个查询,否则我们永远无法获得关于新类型对象的可用信息(根据我们已经知道的内容表示的信息)。在OO编程中,查询对应于类和函数的字段(属性),没有副作用。

我们也只有一个命令:

移动:点×整数→点

对于任意点p和整数I,得到一个新的点,移动(p, i)。同样,除非ADT规范至少有一个命令,表示修改对象的方法,否则它是没有意义的。(在数学中,我们实际上并不修改对象,而是得到新的对象。在命令式编程中,我们实际上会更新现有的对象。)在面向对象编程的类中,命令对应于过程(可能更改对象的方法)。

您可以看到这个想法:通过可应用的操作定义POINT的概念。

列出它们的名称和它们的参数types results的类型(如POINT × INTEGER→POINT)还不足以指定这些操作:我们必须指定它们的基本属性,当然不必诉诸于编程实现。这就是ADT规范的第二个组成部分——公理的作用。

比如我在上面写的得到原点,这个点x= 0,但你只有我的保证。我的话很好,但还不够好。一个公理会明确地告诉你这个性质:

x= 0—a0

第二个公理,也是最后一个公理,告诉我们什么移动确实。它适用于任意点p和任意整数m:

x移动(p、m)) =x(p) + m - A1

换句话说,移动p × m得到的点的坐标就是p + m的坐标。

就是这样!(除了前提条件的概念,这需要稍等片刻。)这个示例很简单,但是这种方法可以应用于任意数量的数据类型,具有任意数量的适用操作和任意级别的复杂性。这就是我们在OO编程中编写类时在设计和实现层所做的。

我的ADT是否足够完整?

充分的完整性是我们可以在这样的规范上评估的一个属性。如果公理足够强大,能够以不涉及T的形式生成任何格式良好的查询表达式的值,那么针对T类型的ADT规范(这里是POINT)就足够完整。这个定义包含了一些新术语,但概念非常简单;我将通过一个例子解释它的含义。

通过ADT规范,我们可以形成各种表达式,表示任意复杂的规范。例如:

x移动移动移动3),x移动移动, -2), 4), -6)

这个表达式将产生一个整数(since函数)x它的结果类型为INTEGER),用于描述带点的计算结果。我们可以将这个计算可视化;注意,它涉及到创建两个点(因为出现了两次)并移动它们,在一种情况下,使用其中一个的当前坐标作为另一个的位移。下图说明了这个过程。

通过画这张图得到的结果是,x (P5)也就是-1。我们将在下面用数学方法推导它。

或者,如果像大多数程序员(以及其他许多人)一样,您发现操作推理比数学推理更直观,那么您可以认为前面的表达式描述了以下OO程序的结果(变量类型为POINT):

创建p——在c++ /Java语法中:p = new POINT();
创建
p.move (3)
q.move (2)
q.move (4)
p.move (q.x)
p.move (6)

结果: = p.x

您可以使用POINT with的类,在您最喜欢的OO编程语言中运行这个程序x而且移动,的值结果也就是-1。

然而,在这里,我们将停留在数学层面,并使用ADT的公理简化表达式,与我们计算任何其他数学公式的方法相同,应用规则而不需要依赖直觉或操作推理。下面是表达式(我们称它为i,类型为INTEGER):

x移动移动移动3),x移动移动, -2), 4), -6)

一个查询表达式是一个应用最外层函数的表达式,这里的x是一个查询函数。请记住,查询函数的新类型POINT只出现在左侧。这是关于x,所以上面的表达式I确实是一个查询表达式。

为了充分的完整性,查询表达式是我们感兴趣的,因为它们的值是用我们已经知道的东西表示的,比如整数,所以它们是我们可以具体地获得ADT(可以这么说,去抽象它)直接可用信息的唯一方法。

但我们只能通过应用公理得到这样的值。因此,如果公理总是给我们答案:任何这样的查询表达式的值,那么公理就是“足够完整”的。

让我们看看上面的表达式i是否满足这个充分完备的条件。为了使它更容易处理,让我们把它写成更简单的表达式(所有类型都是POINT),如下图所示:

p1 =移动3)
p2 =移动, 2)
p3 =移动(p2, 4)
p4 =移动(p1,x(p3))
p5 =移动(p4 6)
我=x(p5)

(您可能会注意到,中间表达式大致对应于上述将计算解释为程序的步骤。它们也出现在下面重复的说明性图中。)

现在我们开始应用公理来求表达式的值。记住,我们有两个公理:A0告诉我们x) = 0和A1x移动(p、m)) =x(p) + m。将A1应用于表达式i的定义

我=xp4) - 6
= i4 - 6

如果我们定义

当=xp4)——类型为INTEGER

我们只需要计算i4。将A1应用到p4的定义上,我们得到

当=x(p1) +x(p3)

计算这两项:

  • 再次应用A1,我们看到第一项x(p1)x(新+ 3,但是A0告诉我们x)为零,所以x(p1) 3。
  • 至于x(p3),再次从A1,x(p2) + 4x(p2)等于(从A1到A0)就是-2x(p3) 2。

最后,i4 = 5,整个表达式i = i4 - 6的值为-1。好工作!

证明足够的完整性

i的成功计算只是一个例子的推导,表明在这种特殊情况下,公理产生了以INTEGER表示的答案。我们如何从一个例子到一个完整的规范?

首先是坏消息:就像编程中所有有趣的问题一样,ADT规范的充分完整性在理论上是无法确定的。没有通用的自动过程来处理ADT规范并打印出“足够完整”或“不够完整”。

现在你已经从震惊中恢复过来,你可以分享计算机科学家对这样的公告的自然反应:那又怎样。(事实上,我们可以把计算机科学家这个概念定义为这样的人:在早晨刷牙之前——如果他刷牙的话——就已经为一个无法确定的问题构建了一个实用的解决方案的轮廓。)我们能找到一种方法来确定a是否鉴于规范足够完整。事实上,这样的证明是计算机科学家版的牙科卫生:除非ADT足够完整,否则它就不会在黄金时间出现。

证明通常不会太难,并且将遵循我们简单示例所演示的一般风格。

我们注意到充分完备性的定义说:“公理强大到足以产生任何的价值格式良好的查询表达式的形式不涉及类型"。我还没有定义“形式良好”。它仅仅意味着表达式的结构正确,语法正确(基本上括号的匹配正确),参数的数量和类型正确。例如,以下表达式不是格式良好的(如果p是POINT类型的表达式):

移动(p, 55)——括号使用不当。
移动(p)—参数数错误。
移动(p, p)——类型错误:第二个参数应该是一个整数。

这样的表达是无意义的,所以我们只关心形式良好的表达。注意,除了x而且移动,表达式可以像示例中那样使用整数常量(尽管我们可以推广到任意整数表达式)。我们将整数常量视为查询表达式。

我们必须证明,通过两个公理A0和A1,我们可以确定任何查询表达式i的值。注意,因为唯一的查询函数是x, i唯一可能的形式,除了一个整数常数,是x(p)用于POINT类型的表达式p。

证明通过对数字的归纳法进行n查询表达式i中的括号对。

有两个基本步骤:

  • n= 0:在这种情况下I只能是一个整数常数。(ADT函数中惟一没有圆括号的表达式是,它不是一个查询表达式。)所以这个值是已知的。在所有其他情况下,我都是这样的xp)指出。
  • n= 1:在这种情况下p只能是,也就是说I = x (),因为除了,是移动,任何使用它都会加上括号。在这种情况下,公理A0给出了i: 0的值。

对于归纳步骤,我们考虑i withn+ 1个括号对,n > 1。如前所述,i是x (p)的形式,所以p正好有n对括号。p不能(它会给出0对括号并且在第二步中被处理掉了)所以p必须是这样的形式

p =移动(p', i')——对于类型为POINT的表达式p'和类型为INTEGER的表达式i'。

暗示(因为I =x(p)),根据公理A1, i的值是

x(p) +我

如果我们能确定两者的值我们就能确定i的值x(p)和我。p以来n括号对和p =移动(p', i'), p'和i'都是最多的n- 1个括号对。(这个用n- 1是合法的,因为我们有两个基本步骤,使我们能够假设n> 1)。结果,两者都是x(p')和i'最多n括号对,使我们能够通过归纳假设推导出它们的值,以及i的值。

根据我的经验,大多数充分完备性的证明都遵循这种风格:对括号对的数量(或最大嵌套级别)进行归纳。

先决条件

到目前为止,我一直在讨论通用ADT规范的第三个组成部分:先决条件。需要先决条件是因为大多数实际规范都需要它们的某些功能部分.从X到Y的偏函数是对X的某些元素可能不产生值的函数。例如,对实数的反函数对X产生1 / A,它是偏函数,因为它对A = 0没有定义(或者,在计算机上,对非零但非常小的A没有定义)。

假设在我们的例子中,我们只想接受区间[-4,+4]中的点:

我们可以简单地通过将move转化为偏导函数来模拟这个性质。上面具体说明为

移动:点×整数→点

普通的箭头→引入一个总(总是定义的)函数。对于部分函数,我们将使用交叉箭头-|->,将函数[4]指定为

移动:点×整数-|->点

其他职能保持不变。偏函数会带来麻烦:对于f in X -|-> Y,如果f是偏函数,我们不能再高兴地使用f (X),即使对于X的适当类型X,我们必须确保X属于f的,意为f被定义的一组值。这是没有办法的:如果您希望您的规范是有意义的,并且它使用部分函数,那么您必须显式地指定每个函数的域。下面是如何做,在这种情况下移动

移动(p:点;d:整数)需要|x(p) + d | < 5——其中|…|是绝对值

使充分完备性的定义(和证明)适应可能存在的部分函数:

  • 我们只需要考虑(对于公理必须产生查询表达式值的规则)满足相关先决条件的格式良好的表达式。
  • 但是,定义必须包含这样一个属性:公理总是使我们能够确定表达式是否满足相关的先决条件(通常是证明的简单部分,因为先决条件本身就是查询表达式)。

相应地更新前面的证明并不难。

回要求

充分完整性的定义对评估需求文档的完整性有很大的帮助。我们必须首先遗憾地注意到,对于今天的许多团队来说,需求止步于“用例”(场景)或“用户故事”。当然,这些都不是要求;它们只描述个别案例,对于需求就像测试对于程序一样。它们可以用于检查需求,但不足以作为需求。我假设的是真实的需求,其中包括行为的描述(以及其他元素,如环境属性和项目属性)。为了描述行为,您将定义操作及其效果。现在我们知道老的IEEE标准通过说明完整的需求应该包括什么来告诉我们什么

定义在所有可实现的情况下,软件对所有可实现的输入数据类别的响应

无论我们是否费心指定adt,它们都在后台;我们的系统操作反映了命令,而我们可以观察到的效果反映了查询。为了使我们的规范完整,我们应该尽可能多地(心理的或显式的)绘制所有命令对所有查询可能产生的影响的矩阵。“尽可能多”,因为软件工程是工程,我们很少能够达到完美。但是矩阵的完满程度告诉我们很多(这里可能是软件度量?)关于我们的需求离完整性有多近。

我应该注意到需求的完整性还有其他方面。例如,Michael Jackson、Pamela Zave和Axel van Lamsweerde(在以后的文章中有详细的参考资料)的工作区分了业务目标、环境约束和系统属性,导致了完整性的概念,即系统属性在多大程度上满足目标并服从约束[5]。充分的完备性在系统级别上运行,以及它的理论基础,是每个实践软件工程师或项目经理都应该掌握的重要概念之一。

Bertrand Meyer首席技术官是谁埃菲尔铁塔的软件(Goleta, CA),沙夫豪森理工学院(瑞士)教授和教务长,俄诺波利斯大学(俄罗斯)软件工程实验室负责人。

引用和笔记

[1]约翰V.古塔格,吉姆J.霍宁:抽象数据类型的代数规范年代,在Acta Informatica第10卷第1期。1,第27-52页,1978年,现有在这里从施普林格网站。这是一篇经典论文,但我注意到,如今很少有人知道它;在谷歌Scholar中,我看到了700多篇引用,但其中不到100篇是在过去8年里。

[2] IEEE:软件需求规范的推荐实践, IEEE标准830-1998,1998。这个标准应该是过时的,被更详细、更冗长的新标准所取代,但它仍然是更好的参考:简单、适度,并被行业广泛应用。它确实需要更新,但要更新得好。

[3] Bertrand Meyer,面向对象软件构造,第二版,普伦蒂斯大厅,1997年。事实上,在1988年的第一版中就已经有了关于充分完整性的讨论。

有一个很好的HTML交叉箭头字符⇸,但是CACM的博客写作软件不能正确处理它,所以我们将就着用-|->

感谢米兰理工大学的Elisabetta Di Nitto提出了需求完整性的概念。


评论


surbhi nahta

感谢您提供的信息和更新。我偶然看到你的博客,想说我真的很喜欢浏览你的博客文章。我正在搜索这个话题,我得到确切的帖子。我也在搜索关于塔利课程在浦那,试图写一些关于这个话题。


显示1评论

登录为完全访问
»忘记密码? »创建ACM Web帐号
Baidu
map