第3章:抽象数据类型(ADT)和面向对象编程(OOP) 3.2设计规约

大纲

1.编程语言中的功能/方法
2.规约:便于交流的编程,为什么需要规约
行为等同规约结构:前提条件和后条件测试和验证规约
3.设计规约分类规约图表规约质量规约
4.总结

编程语言的功能和方法

方法:构建模块
大型项目由小型方法构建
方法可以单独开发,测试和重复使用
方法的用户不需要知道它是如何工作的 - 这被称为“抽象”

注意:调用方法时参数类型不匹配 - 静态检查
返回值类型是否匹配,也在静态类型检查阶段完成

规约:便于交流的编程

(1)编程中的文档

Java API文档:一个例子
类层次结构和实现的接口列表。
直接子类,并为接口实现类。
类的描述
构建思想
方法摘要列出了我们可以调用的所有方法
每个方法和构造函数的详细描述

  • 方法签名:我们看到返回类型,方法名称和参数。 我们也看到例外。 目前,这些通常意味着该方法可能遇到的错误。
  • 完整的描述。
  • 参数:方法参数的描述。
  • 以及该方法返回的描述。

记录假设

向下写入变量的类型记录了一个关于它的假设:例如,此变量将始终引用一个整数。

  • Java实际上在编译时检查了这个假设,并保证在你的程序中没有地方违反了这个假设。

声明一个变量final也是一种形式的文档,声明该变量在初始赋值后永远不会改变。

  • Java也会静态地检查它。

如何函数/方法的假设?

便于交流的编程

为什么我们需要写下我们的假设?

  • 因为编程充满了它们,如果我们不写下它们,我们将不会记住它们,而其他需要阅读或更改我们程序的人将不会知道它们。 他们必须猜测。

程序必须记住两个目标:

  • 与电脑交流。 首先说服编译器你的程序是合理的 - 语法正确和类型正确。 然后让逻辑正确,以便在运行时提供正确的结果。
  • 与其他人沟通。 使程序易于理解,以便在有人修复,改进或未来适应时,他们可以这样做。

黑客与工程

黑客往往以肆无忌惮的乐观为标志

  • 不好:在测试任何代码之前先写很多代码
  • 不好:把所有的细节都留在脑海中,假设你会永远记住它们,而不是写在你的代码中
  • 不好:假设错误不存在或者很容易找到并修复

但软件工程不是黑客行为。 工程师是悲观主义者:

  • 好:一次写一点,随时测试(第7章中的测试优先编程)。
  • 好:记录你的代码依赖的假设
  • 好:捍卫你的代码免受愚蠢 - 尤其是你自己的!

静态检查有助于此

(2)规约和契约(方法)

规约(或称为契约)
规约是团队合作的关键。 没有规约就不可能委托实施方法的责任。
规约作为一种契约:实施者负责满足契约,而使用该方法的客户可以依赖契约。

  • 说明方法和调用者的责任
  • 定义实现的正确含义

规约对双方都有要求:当规约有先决条件时,客户也有责任。

  • 如果你在这个时间表上支付了这笔款项......
  • 我将用下面的详细规约来构建一个
  • 有些契约有不履行的补救措施

为什么规约?

现实:

  • 程序中最常见的错误是由于对两段代码之间的接口行为的误解而产生的。
  • 尽管每个程序员都有规约说明,但并不是所有的程序员都把它们写下来。 因此,团队中的不同程序员有不同的规约。
  • 程序失败时,很难确定错误的位置。

优点:

  • 代码中的精确规约让您分摊代码片段的责备,并且可以免除您在修复应该去的地方令人费解的痛苦。
  • 规约对于一个方法的客户来说是很好的,因为他们不需要阅读代码。

规约对于方法的实现者来说是很好的,因为他们给了实现者自由地改变实现而不告诉客户。
规约也可以使码代码更快。
契约充当客户和实施者之间的防火墙。

  • 它保护客户免受单位工作细节的影响。
  • 它将执行器从单元使用的细节中屏蔽掉。
  • 这种防火墙会导致解耦,允许单元的代码和客户端的代码独立更改,只要这些更改符合规约。
  • 解耦,不需要了解具体实现

对象与其用户之间的协议

  • 方法签名(型号规约)
  • 功能和正确性预期
  • 性能预期性能

该方法做了什么,而不是如何做

  • 接口(API),不是实现

(3)行为等价性

要确定行为等同性,问题是我们是否可以用另一个实现替代另一个实现
等价的概念在客户眼中。

为了使一个实现替代另一个实现成为可能,并且知道何时可以接受,我们需要一个规约来说明客户端依赖于什么
注意:规约不应该谈论方法类的局部变量或方法类的私有字段。

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

一个方法的规约由几个子句组成:

  • 先决条件,由关键字require表示
  • 后置条件,由关键字效果表示
  • 特殊行为:如果违反先决条件,会发生什么?

先决条件是客户(即方法的调用者)的义务。 这是调用方法的状态。
后置条件是该方法实施者的义务。
如果前提条件适用于调用状态,则该方法必须遵守后置条件,方法是返回适当的值,抛出指定的异常,修改或不修改对象等等。

整体结构是一个合乎逻辑的含义:如果在调用方法时前提条件成立,则在方法完成时必须保持后置条件。
如果在调用方法时前提条件不成立,则实现不受后置条件的限制。

  • 可以自由地做任何事情,包括不终止,抛出异常,返回任意结果,进行任意修改等。

Java中的规约

Java的静态类型声明实际上是方法的前提条件和后置条件的一部分,该方法是编译器自动检查和执行的一部分。
静态检查
契约的其余部分必须在该方法之前的评论中进行描述,并且通常取决于人类对其进行检查并予以保证。
参数由@param子句描述,结果由@return和@throws子句描述。
将前提条件放在@param中,并将后置条件放入@return和@throws。

可变方法的规约

如果效应没有明确说明输入可以被突变,那么我们假设输入的突变是隐式地被禁止的。
几乎所有的程序员都会承担同样的事情。 惊喜突变导致可怕的错误。
惯例:

  • 除非另有说明,否则不允许突变。
  • 没有突变的投入

可变对象可以使简单的规约/合约非常复杂
可变对象降低了可变性

可变对象使简单的合约变得复杂

对同一个可变对象(对象的别名)的多次引用可能意味着程序中的多个地方 - 可能相当分散 - 依靠该对象保持一致。
按照规约说明,契约不能再在一个地方执行,例如, 一个类的客户和一个类的实施者之间。
涉及可变对象的契约现在取决于每个引用可变对象的每个人的良好行为。

作为这种非本地契约现象的一个症状,考虑Java集合类,这些类通常记录在客户端和实现者之间的非常明确的契约中。

  • 尝试找到它在客户端记录关键要求的位置,以便在迭代时无法修改集合。

对这样的全局属性进行推理的需要使得理解难度更大,并且对可变数据结构的程序的正确性有信心。
我们仍然必须这样做 - 为了性能和便利性 - 但是为了这样做,我们在bug安全方面付出了巨大的代价。

可变对象降低了可变性

可变对象使得客户端和实现者之间的契约更加复杂,并且减少了客户端和实现者改变的自由。
换句话说,使用允许更改的对象会使代码难以改变。

(5)*测试和验证规约

正式契约规约

Java建模语言(JML)
这是一个有优势的理论方法

  • 运行时检查自动生成
  • 正式验证的依据
  • 自动分析工具

缺点

  • 需要很多工作
  • 在大的不切实际
  • 行为的某些方面不符合正式规约

文本说明 - Javadoc

实用方法
记录每个参数,返回值,每个异常(选中和未选中),该方法执行的操作,包括目的,副作用,任何线程安全问题,任何性能问题。
不要记录实施细节

语义正确性遵守契约

编译器确保类型正确(静态类型检查)

  • 防止许多运行时错误,例如“未找到方法”和“无法将布尔值添加到int”

静态分析工具(如FindBugs)可以识别许多常见问题(错误模式)

  • 例如:覆盖equals而不覆盖hashCode

但是,如何确保语义的正确性?

正式验证

使用数学方法证明正式规约的正确性
正式证明一个实现的所有可能的执行符合规约
手动努力; 部分自动化; 不能自动确定

测试

使用受控环境中的选定输入执行程序
目标

  • 显示错误,因此可以修复(主要目标)
  • 评估质量
  • 明确说明书,文件

黑盒测试:以独立于实现的方式检查测试的程序是否遵循指定的规约。

设计规约

(1)按规约分类

比较规约

它是如何确定性的。 该规约是否仅为给定输入定义了单个可能的输出,或允许实现者从一组合法输出中进行选择?
它是如何声明的。 规约是否只是表征输出的结果,还是明确说明如何计算输出?
它有多强大。 规约是否只有一小部分法律实施或一大套?
“什么使一些规约比其他规约更好?”

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

规约S2强于或等于规约S1如果

  • S2的先决条件弱于或等于S1
  • 对于满足S1的先决条件的状态,S2的后置条件强于或等于S1。

那么满足S2的实现也可以用来满足S1,在程序中用S2代替S1是安全的。

规则:

  • 削弱先决条件:减少对客户的要求永远不会让他们感到不安。
  • 加强后续条件,这意味着做出更多的承诺。

如果S3既不强于也不弱于S1,则规约可能会重叠(因此存在仅满足S1,仅S3,以及S1和S3的实现)或者可能不相交。
在这两种情况下,S1和S3都是无法比较的。

(2)图表规约

这个空间中的每个点代表一个方法实现。
规约在所有可能的实现的空间中定义了一个区域。
一个给定的实现要么按照规约行事,要满足前置条件 - 隐含 - 后置契约(它在区域内),或者不(在区域外)。
实现者可以自由地在规约中移动,更改代码而不用担心会破坏客户端。
这对于实现者能够提高其算法的性能,代码的清晰度或者在发现错误时改变他们的方法等而言是至关重要的。
客户不知道他们会得到哪些实现。

  • 他们必须尊重规约,但也有自由改变他们如何使用实现而不用担心会突然中断。

当S2比S1强时,它在此图中定义了一个较小的区域。
较弱的规约定义了一个更大的区域。
强化实施者的后置条件意味着他们自由度较低,对产出的要求更强。
弱化前提意味着:实现必须处理先前被规约排除的新输入。

(3)设计好的规约
规约的质量

什么是一个好方法? 设计方法意味着主要编写一个规约。
关于规约的形式:它显然应该简洁,清晰,结构良好,以便阅读。
然而,规约的内容很难规定。 没有一个可靠的规则,但有一些有用的指导方针。

规约应该是连贯的(内聚的)

该规约不应该有很多不同的情况。 冗长的参数列表,深层嵌套的if语句和布尔型标志都是麻烦的迹象。
除了可怕地使用全局变量和打印而不是返回之外,规约不是一致的 - 它执行两个不同的事情,计算单词并找出最长的单词。
调用的结果应该是信息丰富的
如果返回null,则无法确定密钥是否先前未绑定,或者实际上是否绑定为null。这不是一个很好的设计,因为返回值是无用的,除非您确定没有插入null。

规约应该足够强大

规约应给予客户在一般情况下足够强大的保证 - 它需要满足其基本要求。 - 在规定特殊情况时,我们必须格外小心,确保它们不会破坏本来是有用的方法。例如,对于一个不合理的论证抛出异常,但允许任意的突变是没有意义的,因为客户端将无法确定实际发生了什么样的突变。

规约也应该足够薄弱

这是一个不好的规约。

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

规约应该使用抽象类型

用抽象类型编写我们的规约为客户和实现者提供了更多的自由。
在Java中,这通常意味着使用接口类型,如Map或Reader,而不是像HashMap或FileReader这样的特定实现类型。

  • 像列表或集合这样的抽象概念
  • 特定的实现像ArrayList或HashSet。

这强制客户端传入一个ArrayList,并强制实现返回一个ArrayList,即使可能存在他们希望使用的替代List实现。

先决条件还是后置条件?

是否使用前提条件,如果是,则在继续之前,方法代码是否应该尝试确保先决条件已满足?
对于程序员:

  • 前提条件最常见的用法是要求提供一个属性,因为该方法检查该属性会很困难或昂贵。

如果检查一个条件会使方法变得难以接受,那么通常需要一个先决条件。

对用户而言:

  • 一个不平凡的先决条件会给客户带来不便,因为他们必须确保他们不会以不良状态调用该方法(违反前提条件); 如果他们这样做,没有可预测的方法来从错误中恢复。

所以方法的用户不喜欢先决条件。

  • 因此,Java API类倾向于指定(作为后置条件),当参数不合适时,它们会抛出未经检查的异常。
  • 这使得在调用者代码中找到导致传递错误参数的错误或不正确的假设更容易。
  • 通常情况下,尽可能靠近错误的地点快速失败,而不是让糟糕的价值观通过远离其原始原因的程序传播。

关键因素是检查的费用(编写和执行代码)以及方法的范围。

如果只在类本地调用,则可以通过仔细检查调用该方法的所有类来解决前提条件。
如果该方法是公开的,并且被其他开发人员使用,那么使用前提条件将不太明智。 相反,像Java API类一样,您应该抛出一个异常。

总结
规约作为程序实现者与其客户之间的关键防火墙。
它使得单独的开发成为可能:客户端可以自由地编写使用该过程的代码,而无需查看其源代码,并且实现者可以自由地编写实现该过程的代码而不知道它将如何使用。

减少错误保证安全

  • 一个好的规约清楚地记录了客户和实施者依赖的相互假设。错误通常来自界面上的分歧,并且规约的存在会降低这一点。
  • 在你的规约中使用机器检查的语言特性,比如静态类型和异常,而不仅仅是一个人类可读的评论,可以更多地减少错误。容易理解
  • 一个简短的规约比实现本身更容易理解,并且使其他人不必阅读代码。

准备好改变

  • 规约在代码的不同部分之间建立契约,允许这些部分独立更改,只要它们继续满足合同的要求。

声明性规约在实践中是最有用的。
先决条件(削弱了规约)使客户的生活更加艰难,但明智地应用它们是软件设计师的重要工具,允许实施者做出必要的假设。

减少错误保证安全

  • 没有规约,即使是我们程序中任何部分的细微变化,都可能成为敲打整个事情的尖端多米诺骨牌。
  • 良好的结构,一致的规约最大限度地减少了误解,并最大限度地提高了我们在静态检查,谨慎推理,测试和代码审查的帮助下编写正确代码的能力。

容易理解

  • 写得很好的声明性规约意味着客户端不必阅读或理解代码。

准备好改变

  • 适当的规约赋予实现者自由,适当的强壮规约赋予客户自由。
  • 我们甚至可以自己改变规约,而不必重新审视每个地方的使用情况,只要我们只是加强它们:削弱先决条件并加强后置条件。

相关推荐