栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

软件构造第5章设计规约总结

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

软件构造第5章设计规约总结

目录

前言

一、什么是规约?

二、为什么要用规约?

1、初入规约

 2、深入规约

i、行为等价性(Behavioral equivalence)

ii、规约的结构

iii、java中的规约

二、设计规约

1、注意规约的决定性(Deterministic)

2、使用声明性的规约(Declarative)

3、选择强的规约

4、尽可能使用ADT 

三、规约的图示化

总结


前言

规约设计这章结束没多久,趁热打铁总结一下。(9号下笔,本来想简洁一点,结果写了一大堆)

一、什么是规约?

我们先看一个java中的规约(specifications)的实例(也就是一个javadoc):

                  图1 BigInteger类中add方法的javadoc

这是BigInteger类中的add方法的规约(第一行是方法签名),可以看到方法签名的下一行写了add方法的功能:计算BigInteger对象(this,即调用方法的对象)和另一个BigInteger对象val的和并将其返回。

为了使用这个方法,使用者(client)要输入参数val,它的类型在签名里说明了是BigInteger,而Parameters部分告诉使用者val的意义:将要加到调用方法对象(也就是this)上的值。

最后的Returns部分告诉我们这个方法返回两个BigInteger对象的加和,由签名知道类型也是BigInteger。

可以看到,规约说白了就是对函数的功能、输入输出的自然语言说明,它其实也是使用者(client)和实现者(implementer)之间的“契约”(contract),虽然上面的例子不能很好的体现这一点。

二、为什么要用规约?

1、初入规约

规约这个东西在你的代码将会被长期使用、维护的时候会有很大的帮助,尽管现在我感觉我写的这些玩意并不值得设计规约,但是这种设计的经验是需要积累的。规约提供的帮助主要有两个方面。

1、对于使用者而言,可以通过规约直接知晓函数的功能以及输入输出的要求,不需要去看函数的源码,在lab1使用各种类方法时我们已经体会到了这种方便。

2、对于实现者而言,实现者可以自由更改函数的具体实现而不必告诉使用者(使用者只看方法的效果)。而且,实现者可以通过在规约说明中限定一些特殊的输入(如禁止某些输入)来减少函数内部的检查和处理,从而让代码运行得更快。

                                       图2 规约作为防火墙将使用者和实现者分开

这是对上面描述的一个概括:规约就像防火墙一样把实现者和使用者隔离开,使用者无需知道函数的具体实现,仅通过规约就可以知道该如何使用这个函数。在使用者遵守前置条件的前提下,实现者也无需去管自己实现的函数被如何使用。这种隔离造成了“解耦”(decoupling),这意味着使用者的代码(包含该函数调用的实现)和实现者的代码(函数的具体实现)可以独立地发生改动,只要双方遵守规约中对各自的制约(前置条件制约使用者,后置条件制约实现者)。

 2、深入规约

i、行为等价性(Behavioral equivalence)

来看下面两个类似的方法:

                                        图3 两种查找int数组指定值下标的方法

为了判定这两个方法的等价性,需要分析在将其中一个方法替换为另一个方法之后,程序的行为有没有发生变化,可以看到:

1、当数组arr里没有val时,findFirst返回arr的长度,而findLast返回-1,程序的行为变化了。

2、当数组arr里又不止一个val时,findFirst返回val的最小索引,findLast则返回val的最大索引,程序行为仍然不一致。

但是我们发现,当arr数组里只有一个val时,两个方法返回的索引一定相同。在这个情况下,两个方法”等价“。

上文所说的”行为等价“是相对于旁观者---即使用者而言的,为了能让这两个方法可以互换,我们可以通过修改前置条件来制约使用者,例如:

                                              图4 一般的规约写法

这个规约要求使用者输入的数组有且仅有一个val,当且仅当使用者满足这个前置条件时,他才能毫无悬念地得到val的下标,这是后置条件中实现者所保证的。如果后置条件不满足,实现者可以做随意的处理:抛出异常、返回任意值甚至是做任意修改,而且无需告诉使用者,因为使用者也没有遵守约定(前置条件),有点小无情。 

注意,这是一种通用的规约写法,在java中的规约写法会在下面讲。

ii、规约的结构

图4向我们呈现了规约的两大“条款”——前置条件和后置条件。

前置条件的关键词是requires,而后置条件的关键词是effects。

前置条件是使用者的义务,如果使用者遵守前置条件,就能确保调用方法的状态。就比如前面的find方法,如果使用者遵守前置条件,那么就能保证find方法接收的arr数组中只有一个val。

后置条件是实现者的承诺,它意味着当前置条件被满足时,方法的行为一定满足后置条件的要求,对于前面的find方法就是一定返回arr数组中val的下标。

iii、java中的规约

图4的规约在java中可以写成这样:

                                           图5 java中的规约

注意开头的这样的形式才能被解释成javadoc,IDE才能帮你生成对应的文档。

可以看到@param的内容一般都是前置条件,而@return的内容一般都是后置条件,但是我们也要注意那些没有用@标注的地方,其中可能也隐含了前置条件或后置条件,或者二者皆有。

值得注意的是方法签名也是规约的一部分,而且它既是前置条件(参数的类型要求)也是后置条件(返回值的类型要求),并且它的满足性可以被编译器静态检查。所以其实我们都已经写过一些残缺的“规约”了。

关于规约的细节还有很多。

二、设计规约

说了半天,我们到底该如何自己设计规约呢?(下面的用户即使用者client)

1、注意规约的决定性(Deterministic)

规约的决定性(deterministic)指的是:当用户的输入满足前置条件时,输出能完全确定,即只有一种可能的返回结果。

                                       图6 具有完全决定性的find方法规约

如图的规约具有完全决定性,由于val在arr数组中只出现一次,所以对arr数组查询val的下标将会有唯一的返回结果。

与此相对的,一些规约则是待决定性的(unerdetermined),如下:

                                   图7 具有待决定性的find方法规约

对于val在arr数组中多次出现的情况,这个规约的后置条件仅仅承诺会返回val的某个下标,也就是对于一个输入,可以有多种输出的可能性。注意,这指的是在用户还不知道当前输入的输出结果时,输出的可能性是多种的,但是一旦这个输入的结果出来了,下一次再以这个输入调用方法时仍会得到相同的输出。这里要与不确定性区分开,不确定性往往意味着即使是相同的输入,每一次的输出都有可能是不同的。这可能是因为在实现中使用了依赖随机数或时间的函数。

从上文可以看到,决定性的规约可以让用户用的更舒服、更安心,因为他们知道自己的输入将会得到什么结果。而待决定性的规约可以给予实现者更多实现的自由,而且实现起来会更加容易。

2、使用声明性的规约(Declarative)

使用声明性(declarative)的规约意味着:仅仅给出最后输出的属性和意义,以及它们和输入之间的关系,这区别于操作性(operational)的规约,这种规约会暴露实现的细节给用户,导致他们不必要地去依赖实现的内容进行编程,而且方法的实现更改时这种规约也要跟着改,非常不便。

因此在大多数情况下,声明式的规约会更加合适,而且这种规约也更加简洁、易懂,如果非要写实现的细节,可以注释在实现里,只对实现的维护者(maintainer)可见。

3、选择强的规约

规约的强度可以这么简单地理解:规约的强度越强,意味着它的前置条件就越弱,后置条件就越强。

这种强度的设定体现了这么一种思想:更少的要求(前置条件),更多的承诺(后置条件),用户的使用限制更少(弱的前置条件),方法的返回更清晰、更有保证、无需处理多种可能情况(决定性)。

                                         图8 规约1

                                      图9 规约2

                                     图10 规约3

                                   图11 规约4

对比上面4个针对find方法的规约,我们可以看到:

规约2在承诺相同的情况下比规约1的要求更少,因此规约2>规约1。

规约3在要求与规约2相同的情况下承诺了更多(返回最小下标),因此规约3>规约2。

但是当我们试图比较规约3和规约4时,发现它们各有长短:规约3承诺更多,规约4要求更少,而规约的强度并没有一个量化的标准,因此两个规约无法比较。

在选择规约强度时也要把握好度,如果强度太高,对于实现者而言可能是无法实现的,而且规约只是蓝图,描绘了方法的理想行为,方法的实际效果还需要通过测试来检验。

4、尽可能使用ADT 

 在规约中使用ADT能同时给予使用者和实现者更多的自由。在java中,这意味着多使用接口(interface)类,对于使用者而言,他能向方法传递接口的不同实现。对于实现者而言,他也能摆脱参数是具体实现类的对象带来的干扰,以更大的灵活性进行实现。

三、规约的图示化

                                                              图12 java方法构成的“宇宙”

 我们可以把java中所有的方法想象成宇宙,而每一个方法就是宇宙中的一颗星星,如图12,findFirst和findLast是两个具体的方法,在图中就是2个点(因为他们的算法/行为固定了(fixed)),也就是两颗“星星”。

规约则是宇宙中的一个范围,在这个范围内的实现方法都满足规约的要求(前置条件和后置条件),而在范围之外的则不满足规约的要求。

因此,规约就像宇宙中星云的边界一般,实现者可以自由地在星云内“漫步”(实现方法),例如提升方法的性能或者修复BUG,而无需担心会影响用户。

用户只选取某一片星云,而不必去在意其中的某一颗星星。

比原始规约更强的规约,意味着更小的“星云”,也就是更少的星星(实现方法)可以被选中,就好比图12中的findOneOrMore,AnyIndex(原始)和findOneOrMore,FirstIndex(强化),后者真包含于前者。

总结

如前言,本来想做简要的章节总结,却不能很好把握重点,写了一堆东西,但是也多多少少讲了一些规约的内容,要想真正掌握规约的设计使用,最终还是得落实到实践中。 

                                     

 

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/879980.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号