软件构造之设计Java规约

在这篇文章中,我们将看看描述相同行为的不同规约,并讨论它们之间的权衡。我们将看三个维度来比较规约:            
它有多大的确定性。这个规范是否只定义了一个对于给定输入的一个可能的输出?或是否允许实现者从一系列合法的输出中选择一个?            
它是如何声明的。规范仅仅描述了输出应该是什么,还是明确地说明了输出是如何得来的?            
它有多。规约是有一小部分合法的实现方式,还是有很多实现方式?    

确定的规约与欠确定的规约         

我们考虑下边两个方法:
下边是一个关于find方法的一个可能的规约:
这个规约是确定的:当给出满足前提条件的输入时,结果是完全确定的。只有一个返回值和一个最终状态是可能的,没有存在一个以上的有效输出的有效输入。
让我们来看另外一个规约:
该规约不是确定性的。它没有说明如果某个元素在数组中出现超过一次,该返回哪个索引。它只是说,可以根据该函数额度返回值在数组中找到某个元素。该规范允许对于同一个有效输入,存在多个有效的输出。
注意,这与一般意义上的不确定性不同。即使在同一程序中调用相同的输入,非确定性代码有时表现出一种方式,有的时候表现为另一种。例如,当代码的行为依赖于一个随机数,或者取决于并发进程的时间时,这种情况可能发生。但是一个不确定的规约不一定要有一个不确定的实现。它可以通过完全确定的实现来满足。为了避免这种混乱,我们将不确定的规约称为欠确定的规约。

声明性的规约和操作性的规约

简单的说,操作性的给出该方法运行时的一系列步骤,一般为伪代码描述的操作。声明性额度规约没有给出中间步骤的详细信息。相反,它只给出最终结果的性质以及它与初始状态的关系。            
几乎所有的情况下,声明性的规范更可取。它们通常更短,更容易理解,最重要的是,它们不会在不经意之间将我们方法实现的细节暴露给客户端。例如,当我们想查找数组中额度某一个元素时,我们不会在规约中写下”遍历数组直到他找到这个值“,因为该规约说明搜索过程从数组索引低的位置到数组索引高的位置,然后将最低的索引返回。 
对于一个给定的规约,我们可以将它描述成多种形式,具体使用哪一种取决于程序员自己
       

强的规约和弱的规约

假设程序员想改变一种方法,不管是它的实现方式如何,还是它的规约本身。在已经存在依赖于方法当前规约的客户端的情况下,我们如何比较两个规范的行为,以确定用新规约替换旧规约是否安全?
通常,一个规约S2比另一个规约S1强或者相等需满足下边两个条件:
    S2的前置条件比S1的前置条件弱,这意味着客户端可以有更大范围的输入
    在和S1前置条件的相同的情况下,S2的后置条件比S1的后置条件强或者和S1的后置条件强度一样,这意味着他们有更少的自   由,因为他们对输出的要求更强。
在这种情况下,满足规约S2的实现方法也能满足规约S1,这时候用S2替换S1是安全的。
例如,下边关于find方法的规约
可以被替换为
它的前置条件变弱了

也可以被替换为

他的后置条件变强了

用图形表示规约

规约在所有可能的实现的空间中定义了一个区域。一个给定的实现要么按照规范行事:满足前提条件,隐含条件和后置条件,则它在该规约的区域内,否则它在该规约的区域外。

我们可以想象客户在这个空间中寻找可以作为防火墙的规约:
实现者可以自由地在规约确定的区域内移动,可以在不用担心会破坏客户端的情况下更改代码。这对实现者能够改进算法的性能,代码的清晰度或者在发现错误时改变他们的方法等而言至关重要。
客户不知道他们会得到哪些实现。他们必须尊重规范,但可以在不用担心程序突然中断的情况下,自由地改变他们使用某个实现的方式。
当S2比S1强时,它在此图中定义了一个较小的区域,说明比S1更少的实现满足S2。但是,满足S2的每个实现也满足S1,所以较小的区域S2嵌套在S1内。如果另一个规约S3既不比S1强也不比S1弱,则他们所确定的区域要么会出现小部分重叠,要么不相交,这两种情况下,S1和S3是无法比较的。一般体现为S3的前置条件比S1弱,以及在相同的前置条件的情况下,S3的后置条件也弱于S1的后置条件。


如何设计一个好的规约

规约应该是紧密结合的

他不应该有很多种不同的情况。冗长的参数列表,深层嵌套的if语句或者布尔类型阿标记都会带来一定的麻烦。考虑下边的规约:
这是一个设计良好的程序吗?可能不是:它是不连贯的,因为它做了几件事情(找到两个数组并且总结索引),这些事情并没有真正相关。 最好使用两个独立的程序,一个找到索引,另一个索引它们。
再看下边的例子:

除了使用了全局变量和打印而不是返回之外,规范是不连贯的,它执行两个不同的事情,计算单词并找出最长的单词。将这两种任务分离为两种不同的方法将使它们变得更简单(易于理解)并且在其他情况下更有用(准备改变)。


调用的结果应该是信息性的

考虑下边一个方法的规约,该方法将一个值放入一个映射中,其中键的类型为K,值的类型为V:

注意,该规约的前提条件并不排除空值,因此在该地图可以存储空值。 但后置条件将null用作缺少key时的特殊返回值。这意味着如果返回null,则不能分辨键是否先前未被绑定,或者它是否实际上被绑定为null。这不是一个很好的设计,因为返回值是无用的,除非你确实知道你没有插入空值。


规约应该足够强大

当然,规约应该为客户在一般情况下提供足够强的保证,即它需要满足他们的基本要求。在规定特殊情况时,我们必须格外小心,确保它们不会破坏本来是有用的方法。
例如,对于一个不合理的论证抛出异常,但允许任意的突变是没有意义的,因为客户端将无法确定实际发生了什么样的突变。这里有一个说明这个缺陷的规约:
如果抛出NullPointerException,客户端需要自己弄清楚list2中哪些元素实际上被放到了list1中。

规约也应该足够薄弱

考虑下边一个需要打开文件的方法的规约

这是一个不好的规约。它缺乏重要的细节:文件是为阅读或写作而打开的吗? 它是否已经存在或被创建? 它太强大了,因为无法保证打开文件。它运行的过程可能缺少打开文件的权限,或者文件系统可能存在一些超出程序控制范围的问题。相反,规范应该说更弱一些:它试图打开一个文件,如果成功,该文件应该具有某些属性。

规约应尽可能使用抽象类型

在Java中,我们可以区分更多抽象概念,如List或Set,以及特定的实现,如ArrayList或HashSet。
用抽象类型编写我们的规范为客户和实现者提供了更多的自由。在Java中,这通常意味着使用接口类型,如Map或Reader,而不是像HashMap或FileReader这样的特定实现类型。 考虑这个规范:
这迫使客户端传入一个ArrayList,并强制实现者返回一个ArrayList,即使可能存在他们希望使用的替代List实现。由于规范的行为不依赖于任何关于ArrayList的特定内容,因此最好使用更抽象的List来编写此规范。


总结

声明性规范在实践中是最有用的。先决条件变强使客户端的选择更加艰难,但明智地应用它们是软件设计师的重要工具,并且允许实现者做出必要的假设。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值