设计规约

1. Java中的函数和方法

参数:参数类型是否匹配,在静态类型检查阶段完成

返回值:返回值是否匹配,也在静态类型检查阶段完成
一个完整的方法,包括方法的规约spec和方法的实现体implementation

 

 

方法:构建基块

▪ 大程序是用小方法建立起来的
▪ 方法可以单独开发、测试和重用
▪ 方法的使用者不需要知道它是如何工作的——这叫做“抽象”

2. 规范

 2.1 程序设计中的文档化

 

 

 

类层次结构和实现的接口列表包括:
▪ Directsubclasses,对于接口,实现类。
▪ 对class的描述
▪ 构造总结
▪ 方法摘要列出了我们可以调用的所有方法、
▪ 每个方法和构造函数的详细说明

方法签名:我们看到返回类型、方法名和参数。我们也看到例外。目前,这些通常意味着该方法可能会遇到错误。

完整描述。Parameters:方法参数的描述,以及方法返回内容的描述。

记录假设
▪ 写下变量的类型记录了一个关于它的假设:例如,这个变量总是引用一个整数。
Java实际上在编译时检查这个假设,并保证程序中没有违反这个假设的地方。
▪ 声明变量final也是一种文档形式,声明变量在初始赋值之后永远不会更改。
Java也会静态地检查它。

交流式编程
▪ 为什么我们需要写下我们的假设?
因为编程中充满了假设,如果我们不把假设写下来,我们就不会记住它们,其他需要阅读或更改我们的程序的人也不会知道它们,他们只能猜测。
▪ 编写程序时必须牢记两个目标:与计算机交流。首先说服编译器你的程序是合理的
语法正确和类型正确。然后将逻辑正确化,以便在运行时给出正确的结果。
与他人沟通。使程序易于理解,以便在将来有人必须修复、改进或修改程序时,他们可以这样做。

2.2 方法的规范与合同

规约给“供需双方”都确定了责任,在调用的时候双方都要遵守,方便程序与客户端之间达成一致

精确的规约,有助于区分责任;客户端无需阅读调用函数的代码,只需理解spec即可

规约可以隔离“变化”,无需通知客户端;规约也可以提高代码效率;同时,规约扮演“防火墙”角色,方便解耦,用户与开发人员不需要了解对方的具体实现

用户与类在输入/输出的数据类型、功能和正确性、性能方面达成一致

规约只讲“能做什么”,不讲“怎么实现”

规范(合同)
该契约充当客户端和实现者之间的防火墙。
它保护客户不受该单元工作细节的影响。
它保护实现者不受单元使用细节的影响。
此防火墙令客户端和程序不耦合,只要更改符合规范,允许独立更改单元代码和客户端代码。

 

 

 2.3 行为等价性

为了确定行为等价性,问题是我们是否可以用一个实现代替另一个实现。这两个函数是否可以相互转换。

 

 

 

两个实现是否相互等价?当val不存在时,findFirst返回arr的长度,findLast返回-1; 当val出现两次时,findFirst返回较低的索引,findLast返回较高的索引。

让我们从客户端的角度来观察等价性:
当val恰好出现在数组的一个索引处时,这两个方法的行为相同, 每当调用该方法时,它们都将传入一个arr,其中正好有一个元素val, 对于这样的客户机,这两种方法是相同的。
为了能够用一个实现替换另一个实现,并且知道什么时候可以接受,我们需要一个规范来准确地说明客户机依赖于什么。

单纯看实现代码,并不足以判定不同的implmentation是否是“行为等价的”,需要根据代码的spec(开发者与client之间形成的contract判定行为等价性。在编写代码之前,需要弄清楚spec如何协商形成、如何撰写。

2.3 规范结构

方法的规范由几个子句组成:
Precondition,由关键字requires指示
Postcondition,由关键字effects指示
Exceptional behavior:如果违反了Precondition,它会做什么

前置条件是对客户端的约束,是使用方法时必须满足的条件

后置条件是对开发者的约束,是方法结束时必须满足的条件

如果前置条件满足,则后置条件也必须满足;前置条件不满足,则方法可以做任何事情。

 

 

 

Java规范
▪ Java的静态类型声明实际上是方法的前置条件和后置条件的一部分,编译器会自动检查和执行该部分。
▪ 合同的其余部分必须在方法之前的注释中加以说明,并且通常依赖于人来检查和保证它。
▪ 参数由@param子句描述,结果由@return和@throws子句描述。
▪ 在可能的情况下,将前置条件放入@param,后置条件放入@return和@throws。

规格说明可以谈些什么?
方法的规范可以讨论方法的参数和返回值,但不应该讨论方法的局部变量或方法类的私有字段。
您应该考虑规范读者看不见的实现
在Java中,方法的源代码通常对规范读者不可用,因为Javadoc工具从代码中提取规范注释并将其呈现为HTML。

对于mutator方法的规约,规约的后置条件中需要说明是否对参数做出了改变。 除非在后置条件里声明过,否则方法内部不应该改变输入参数。

应尽量遵循此规则,尽量不设计mutating的spec,否则就容易引发bugs;

程序员之间应达成的默契:除非spec必须如此,否则不应修改输入参数。

尽量避免使用mutable的对象,返回值与参数尽量用不可变对象,在规约里进行限定。

3. 设计规约

3.1 规约分类

规范的比较:规范的强度,规范的确定性,规范的陈述性

强规范与弱规范

如何比较两个规范的行为来决定用新规范替换旧规范是否安全?

S2>=S1
如果-S2的前提条件小于或等于S1,则规范S2大于或等于规范S1;
对于满足S1的前提条件的状态,S2的后条件大于或等于S1。然后,满足S2的实现也可以用来满足S1,并且在程序中用S2替换S1是安全的。

规则:削弱先决条件:对客户提出更少的要求永远不会让他们感到不安。——强化后置条件,就是提供更多承诺。

 

 

确定性规范与不确定性规范

确定性:当状态满足前提条件时,结果是完全确定的。
只有一个返回值和一个最终状态是可能的。
没有多个有效输出的有效输入。
在deterministic下:规范允许同一输入有多个有效输出。

不确定性:有时表现为一种方式,有时表现为另一种方式,即使在同一个程序中调用了相同的输入,多次执行时得到的输出可能不同(例如,取决于随机或定时)

欠确定性在规范中提供了一个由实现者在实现时做出的选择。欠定的规约通常有确定的实现。

声明性规范与操作性规范

操作式范给出了方法执行的一系列步骤;伪代码描述是可操作的。
声明性不提供中间步骤的详细信息。相反,它们只是给出最终结果的属性,以及它与初始状态的关系。

声明性规范更可取。它们通常更短,更容易理解,最重要的是,不要无意中暴露客户可能依赖的实现细节。

为什么存在操作规范?
程序员使用规范来解释维护人员的实现。
必要时,在方法主体内使用注释,而不是在规范注释中使用注释。

一个规约有多种阐述方式,选择对客户端和维护者最清晰的。

3.2 图解说明

此空间中的每个点表示一个方法实现。
规范在所有可能实现的空间中定义一个区域。
给定的实现要么按照规范执行,满足先决条件意味着后条件契约(它在区域内),要么不满足(在区域外)。

 

 

 

实现者可以自由地在规范中移动,更改代码而不必担心扰乱客户机。
这对于实现者能够提高算法的性能、代码的清晰性,或者在发现错误时改变方法等至关重要。
客户不知道他们会得到什么样的实现。
他们必须尊重规范,但也可以自由地更改如何使用实现,而不必担心它会突然中断。

当S2强于S1时,它在这个图中定义了一个较小的区域。、
较弱的规范定义了较大的区域。
更强的后置 条件意味着实现的自由度更低了➔在图中的面积更小
更弱的前置条件意味着 实现时要处理更多的可能输入, 实现的自由度低了➔面积更小

 

 

 

 

 3.3 设计良好的规约

规范应该是内聚的

Spec描述的功能应单一、简单、易理解。

 

 

除了糟糕地使用全局变量和打印而不是返回之外,规范也不连贯——它做了两件不同的事情,计算单词和查找最长的单词。
将这两个职责分成两个不同的方法将使它们更简单,并且在其他上下文中更有用(随时可以更改)。

结果应该是丰富的

 

 

如果返回null,则无法判断键以前是否未绑定,或者实际上是否已绑定到null。
这不是一个很好的设计,因为除非您确定没有插入null,否则返回值是无用的。

规范应足够牢靠

规范应该在一般情况下给客户一个足够有力的保证——它需要满足他们的基本要求。
在指定特殊情况时,我们必须格外小心,以确保它们不会破坏原本有用的方法。

 

 

例如,没有必要为一个错误的论点抛出异常,而是允许任意的突变,因为客户将无法确定到底发生了什么样的突变。如果抛出了一个NullPointerException,客户机就要自己找出list2的哪些元素真正进入了list1。

规范应该足够弱

 

 

 

这是一个不好的规格。
它缺少重要的细节:文件是为读写而打开的吗?它已经存在还是已经创建?
它太强了,因为它无法保证打开文件。它运行的进程可能没有打开文件的权限,或者文件系统可能有一些超出程序控制的问题。

相反,规范应该说一些更弱的东西:它试图打开一个文件,如果它成功了,该文件就具有某些属性。

规范应该使用抽象类型
用抽象类型编写我们的规范给了客户机和实现者更多的自由。
在Java中,这通常意味着使用接口类型(如Map或Reader),而不是特定的实现类型(如HashMap或FileReader)。

抽象概念,如列表或集合–特定实现,如ArrayList或HashSet。
这将强制客户端传入一个ArrayList,并强制实现者返回一个ArrayList,即使它们可能希望使用其他列表实现。

先决条件还是后决条件?
是否使用前置条件,如果使用,则方法代码是否应在继续之前尝试确保满足前置条件?
对于程序员来说:最常用的前提条件是要求一个属性,因为它对于方法来说很难或者很昂贵来检查它。

不写Precondition,就要在代码内部check;若代价太大, 在规约里加入precondition, 把责任交给client

对用户来说:一个非常重要的先决条件会给客户带来不便,因为他们必须确保他们不会在错误状态下调用方法(这违反了先决条件);如果他们这样做了,就没有可预测的方法可以从错误中恢复。

所以客户端不喜欢太强的前置条件。对此惯用做法是:不限定太强的前置条件,而是在后置条件中抛出异常:输入不合法!

是否使用前置条件的关键因素是检查的成本(编写和执行代码)和方法的范围。
如果只在类中本地调用它,则可以通过仔细检查调用该方法的所有站点来释放前置条件--责任交给内部chlient。
如果该方法是公共的,并且被其他开发人员使用,那么使用一个先决条件就不那么明智了。相反,像Java API类一样,应该抛出一个异常。

 

posted @ 2022-06-09 22:33  Ha何  阅读(68)  评论(0)    收藏  举报