第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类同样,您应该抛出一个异常。

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

减小错误保证安全

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

准备好改变

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

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

减小错误保证安全

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

容易理解

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

准备好改变

  • 适当的规约赋予实现者自由,适当的强壮规约赋予客户自由。
  • 咱们甚至能够本身改变规约,而没必要从新审视每一个地方的使用状况,只要咱们只是增强它们:削弱先决条件并增强后置条件。
相关文章
相关标签/搜索