3-2设计规约

一.程序设计语言2中的函数/方法。
规范:通信编程
为什么需要规范
行为等价
规范结构:先决条件和后决条件
测试 验证规范
3.设计规范
分类规范
图表规范
质量规范
4.摘要总结

本次讲座的目的
▪ 了解方法规范中的先决条件和后决条件,并能写出正确的规范
▪ 什么是前置条件和后置条件,它们对方法的实现者和客户机意味着什么
▪ 了解不确定的规格,能够识别和评估不确定性
▪ 理解声明性规范和操作性规范,并能够编写声明性规范
▪ 了解先决条件、后决条件和规范的强度;能够比较规范强度▪ 能够写出连贯的、有用的、适当强度的规范

编程语言中的函数和方法
方法:构建基块
▪ 大程序是用小方法建立起来的
▪ 方法可以单独开发、测试和重用
▪ 方法的使用者不需要知道它是如何工作的——这叫做“抽象”
3-2设计规约
2规范:交流式编程
(1) 程序设计中的文档化
3-2设计规约
类层次结构和实现的接口列表。
▪ Directsubclasses,对于接口,实现类。
▪ 对class的描述
▪ 构造总结
▪ 方法摘要列出了我们可以调用的所有方法、
▪ 每个方法和构造函数的详细说明
-方法签名:我们看到返回类型、方法名和参数。我们也看到例外。目前,这些通常意味着该方法可能会遇到错误。
–完整描述。–Parameters:方法参数的描述,以及方法返回内容的描述。

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

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

(2) (方法的)规范和合同
规格(或称为合同)
▪ 规范是团队合作的关键。没有规范就不可能委派实现方法的责任。
▪ 规范充当一个契约,实现者负责满足契约,而使用该方法的客户机可以依赖该契约。
–说明方法和调用方的职责
–定义实现正确的含义
▪ 规范对双方都有要求:当规范有前提时,客户也有责任。

为什么需要规格?
▪ 现实:程序中许多最糟糕的错误都是由于对两段代码之间的接口行为的误解而产生的。
–虽然每个程序员都有自己的规范,但并不是所有的程序员都把它们写下来。因此,团队中的不同程序员有不同的规范。
–当程序失败时,很难确定错误在哪里。
▪ 优点:代码中的精确规范可以让您将责任分摊到代码片段上,并且可以让您免去为修复应该去哪里而费解的痛苦。
–规范对于方法的客户端来说很好,因为它们省去了读取代码的任务。

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

3-2设计规约
规范(合同)
▪ 方法与其用户之间的协议
-方法签名(类型规范)
-功能和正确性期望
-性能期望
▪ 关注方法做什么,而不是怎么做—
—依赖接口(API),而不是实现

(3) 行为等价性
▪ 为了确定行为等价性,问题是我们是否可以用一个实现代替另一个实现
3-2设计规约

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

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

(4) 规范结构:前置条件和后置条件

规格结构
▪ 方法的规范由几个子句组成:
–Precondition,由关键字requires指示
–Postcondition,由关键字effects指示
–Exceptional behavior:如果违反了Precondition,它会做什么
▪ 先决条件是客户的义务(即方法的调用方)。它是调用方法的状态的条件。
▪ 后置条件是方法实现者的义务。
▪ 如果调用状态的前提条件成立,则该方法有义务通过返回适当的值、抛出指定的异常、修改或不修改对象等方式遵守后条件。

规格结构
▪ 整体结构是一个逻辑含义:如果在调用方法时前置条件保满足了,则在方法完成时后置条件必须满足。▪ 如果在调用方法时前置条件不成立,则实现不受后置条件的约束。–它可以做任何事情,包括不终止、抛出异常、返回任意结果、进行任意修改等。

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

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

3-2设计规约
▪ 如果effect没有明确表示输入可以被改变,那么我们假设输入的改变是隐式不允许的。
▪ 几乎所有的程序员都会做同样的事情。突如其来的改变会导致可怕的bug。
▪ 惯例-除非另有说明,否则不允许改变。
–输入无不能是可变的
▪ 可变对象可以使简单的规范/契约变得非常复杂
▪ 可变对象减少了可变性

可变对象使简单契约变得复杂
▪ 对同一可变对象(对象的别名)的多次引用可能意味着程序中的多个位置(可能被广泛分隔)依赖于该对象来保持一致。
▪ 从规范的角度来说,不能再在一个地方强制执行契约,例如在类的客户机和类的实现者之间。
▪ 涉及可变对象的契约现在依赖于每个引用可变对象的人的良好行为。

共享可变对象会使契约复杂化
▪ 谁该受责备?
—客户是否有义务不修改其取回的对象?
–实现者是否有义务不保留它返回的对象?
▪ 澄清规范的一种可能方法:
3-2设计规约
完全建立在客户端开发者的“良心”之上,是不可靠的!

这个怎么样?
3-2设计规约
▪ 这个规格至少说明阵列必须是新的。
▪ 但是,它是否阻止实现者将别名保存到新数组中?
▪ 它是否会阻止实现者更改该数组或在将来将其用于其他用途?
这回责任又到了开发者这一边的“良心”…也是靠不住的

可变对象减少了可变性
▪ 这个怎么样?
3-2设计规约
▪ 不可变的字符串返回值在客户机上提供了一个保证,实现者永远不会像使用char数组那样互相干涉。
▪ 它并不依赖于程序员仔细阅读规范注释。
▪ 字符串是不可变的。不仅如此,这种方法(与前一种方法不同)让实现者可以*地引入缓存,这是一种性能改进。

(5) *测试和验证规范
黑盒测试
▪ 黑盒测试:检查被测试程序是否以独立于实现的方式遵循指定的规范。
▪ 测试用例不应该依赖于任何具体的实现行为。测试用例必须遵守合同,就像其他客户一样。

3-2设计规约
这个测试用例假设一个特定的实现find总是返回最低的索引。

3设计规范
(1) 分类规范
比较规格
▪ 有多决定性。规范是否只为给定的输入定义一个可能的输出,或者允许实现者从一组合法的输出中进行选择?
▪ 如何声明。规范只是描述输出应该是什么,还是明确说明如何计算输出?
▪ 有多强。规范是有一个小的合法实现集,还是有一个大的实现集?
▪ “是什么使某些规范比其他规范更好?”
强规格与弱规格
▪ 如何比较两个规范的行为来决定用新规范替换旧规范是否安全?S2>=S1
如果-S2的前提条件小于或等于S1,则规范S2大于或等于规范S1;
对于满足S1的前提条件的状态,S2的后条件大于或等于S1。然后,满足S2的实现也可以用来满足S1,并且在程序中用S2替换S1是安全的。
▪ 规则:削弱先决条件:对客户提出更少的要求永远不会让他们感到不安。——强化后置条件,就是提供更多承诺。

3-2设计规约
3-2设计规约
3-2设计规约3-2设计规约
更强的规范可能有更弱的先决条件和/或更强的后决条件。 在这两种情况下,实现者必须更加小心;但是具有更多不同输入或更具体需求的客户机现在可能能够使用更强大的规范。

确定性规格与不确定性规格
▪ 确定性:当状态满足前提条件时,结果是完全确定的。
–只有一个返回值和一个最终状态是可能的。
–没有多个有效输出的有效输入。
在deterministic下:规范允许同一输入有多个有效输出。 ▪ 不确定性:有时表现为一种方式,有时表现为另一种方式,即使在同一个程序中调用了相同的输入(例如,取决于随机或定时)
-为了避免混淆,我们将引用不确定的规范作为欠确定。,not d… == under d…
▪ 欠确定性在规范中提供了一个由实现者在实现时做出的选择。–未充分确定的规范通常由完全确定的实现实现来实现。

声明性规范与操作性规范
▪ 操作式范给出了方法执行的一系列步骤;伪代码描述是可操作的。
▪ 声明性指定不提供中间步骤的详细信息。相反,它们只是给出最终结果的属性,以及它与初始状态的关系。
▪ 声明性规范更可取。–它们通常更短,更容易理解,最重要的是,不要无意中暴露客户可能依赖的实现细节。
▪ 为什么存在操作规范?
–程序员使用规范来解释维护人员的实现。
–必要时,在方法主体内使用注释,而不是在规范注释中使用注释。

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

(2) 图解说明
▪ 此空间中的每个点表示一个方法实现。
▪ 规范在所有可能实现的空间中定义一个区域。
▪ 给定的实现要么按照规范执行,满足先决条件意味着后条件契约(它在区域内),要么不满足(在区域外)。
3-2设计规约
▪ 实现者可以*地在规范中移动,更改代码而不必担心扰乱客户机。
–这对于实现者能够提高算法的性能、代码的清晰性,或者在发现错误时改变方法等至关重要。
▪ 客户不知道他们会得到什么样的实现。
–他们必须尊重规范,但也可以*地更改如何使用实现,而不必担心它会突然中断。

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

3-2设计规约
(3) 设计良好的规越
规格的质量
▪ 什么是好方法?设计一个方法意味着首先编写一个规范。 ▪ 关于规范的形式:它显然应该简洁、清晰、结构良好,以便易于阅读。
▪ 然而,规范的内容很难规定。没有绝对正确的规则,但有一些有用的指南

(1) 规范应该是一致的
▪ 规格不应该有很多不同的情况。长参数列表、深度嵌套的if语句和布尔标志都是问题的征兆。
3-2设计规约
▪ 除了糟糕地使用全局变量和打印而不是返回之外,规范也不连贯——它做了两件不同的事情,计算单词和查找最长的单词。
将这两个职责分成两个不同的方法将使它们更简单(易于理解)并且在其他上下文中更有用(随时可以更改)。

(2) 结果应该是信息丰富的
3-2设计规约
▪ 如果返回null,则无法判断键以前是否未绑定,或者实际上是否已绑定到null。
▪ 这不是一个很好的设计,因为除非您确定没有插入null,否则返回值是无用的。

(3) 规格要足够坚固
▪ 规范应该在一般情况下给客户一个足够有力的保证——它需要满足他们的基本要求。
在指定特殊情况时,我们必须格外小心,以确保它们不会破坏原本有用的方法。
3-2设计规约
–例如,没有必要为一个错误的论点抛出异常,而是允许任意的突变,因为客户将无法确定到底发生了什么样的突变。 –如果抛出了一个NullPointerException,客户机就要自己找出list2的哪些元素真正进入了list1。

(4) 规范也应该足够弱
3-2设计规约
▪ 这是一个不好的规格。
–它缺少重要的细节:文件是为读写而打开的吗?它已经存在还是已经创建?
–它太强了,因为它无法保证打开文件。它运行的进程可能没有打开文件的权限,或者文件系统可能有一些超出程序控制的问题。
▪ 相反,规范应该说一些更弱的东西:它试图打开一个文件,如果它成功了,该文件就具有某些属性。

(5) 规范应该使用抽象类型
▪ 用抽象类型编写我们的规范给了客户机和实现者更多的*。
▪ 在Java中,这通常意味着使用接口类型(如Map或Reader),而不是特定的实现类型(如HashMap或FileReader)。
–抽象概念,如列表或集合–特定实现,如ArrayList或HashSet。
▪ 这将强制客户端传入一个ArrayList,并强制实现者返回一个ArrayList,即使它们可能希望使用其他列表实现。

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

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

▪ 对用户来说:一个非常重要的先决条件会给客户带来不便,因为他们必须确保他们不会在错误状态下调用方法(这违反了先决条件);如果他们这样做了,就没有可预测的方法可以从错误中恢复。
▪ 所以方法的使用者不喜欢前置条件
更容易找到导致传递错误参数的错误或错误假设。
–一般来说,最好尽可能接近错误的位置,而不是让错误的值通过远离其原始原因的程序传播。

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

摘要
▪ 规范充当过程实现者与其客户端之间的关键防火墙。
它使单独的开发成为可能:客户端可以*地编写使用过程的代码,而不必看到其源代码;实现者可以*地编写实现过程的代码,而不必知道如何使用它。

安全无bug
一个好的规范清楚地记录了客户机和实现者所依赖的相互假设。错误通常来自于接口上的分歧,而规范的出现减少了这种分歧。
–在您的规范中使用机器检查的语言特性,比如静态类型和异常,而不仅仅是人类可读的注释,可以更有效地减少bug。
▪ 易于理解——一个简短的、简单的规范比实现本身更容易理解,并且节省了其他人阅读代码的时间。
▪ 准备好改变—规范在代码的不同部分之间建立契约,允许这些部分独立更改,只要它们继续满足契约的要求。

声明性规范在实践中最有用。
▪ 先决条件(削弱了规范)使客户端难以满足条件,明智地应用它们是软件设计师的一个重要工具,应该允许实现者作出必要的假设。

–如果没有规范,即使是对程序任何部分的最小更改,也可能是翻倒了整个程序的domino。
–结构良好、一致的规范最大限度地减少了误解,并通过静态检查、仔细的推理、测试和代码评审,最大限度地提高了编写正确代码的能力。
——编写良好的声明性规范意味着客户端不必阅读或理解代码。
—适当的弱规范为实现者提供了*,适当的强规范为客户端提供了*。
–我们甚至可以自己更改规范,而不必重新访问它们使用的每个地方,只要我们只是加强它们:削弱先决条件和加强后决条件。