软件构造:对于设计规约的理解

目录

前言

一、什么是规约/p>

二、为什么要用规约/p>

1、初入规约

 2、深入规约

i、行为等价性(Behavioral equivalence)

         ii、规约的结构

         iii、java中的规约

二、设计规约

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

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

3、选择强的规约

4、尽可能使用ADT 

三、规约的图示化

总结


前言

设计规约这部分内容今天正好复习到了,就浅浅总结一下!

一、什么是规约/h2>

我们先看一个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),虽然上面的例子不能很好的体现这一点。

二、为什么要用规约/h2>

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(强化),后者真包含于前者。

总结

        其实,我本来想做简要的章节总结,但写起来就有点停不下来的感觉,写了一堆东西,多多少少也讲了一些规约的内容。实际上,要想真正掌握规约的设计使用,最终还是得落实到实践中。 

文章知识点与官方知识档案匹配,可进一步学习相关知识Java技能树首页概览91528 人正在系统学习中

来源:m0_53533796

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年5月11日
下一篇 2022年5月11日

相关推荐