抽象数据类型ADT(3)

9.有益的可变性

9.1.有益的可变性

回想一下,当且仅当类型的值在创建后从不改变时,该类型才是不可变的。
随着我们对抽象空间A和表示空间R的新理解,我们可以完善这一定义:抽象值永远不应改变。
但是只要它继续映射到同一个抽象值,实现(implementation)就可以自由地改变表示值,这样客户就看不到变化。
这种变化被称为有益的可变性.

  • 对不变的的ADT来说,它在A空间的抽象值应是不变的。
  • 但其内部表示的R空间中的取值则可以是变化的

9.2.有益的可变性的例子

这种较弱的RI允许一系列RatNum算术运算来简单地忽略将结果减少到最低项。但是,当要向人显示结果时,我们首先要简化它,也就是说:参与运算时,可以有公约数;显示输出时,需要简化(没有公约数)
在这里插入图片描述
请注意,这个toString实现重新分配了private字段分子和分母,改变了表示——即使它是一个不可变类型的观察器方法!
但是,至关重要的是,变值不会改变抽象值。

  • 将分子和分母除以相同的公因数,或将二者乘以-1,对抽象函数AF(分子,分母)=分子/分母的结果没有影响。

另一种思考方式:AF是一个多对一的功能,表示值已更改为另一个仍然映射到相同的抽象值。所以变值是无害的,或者说是有益的。

  • 这种变值只是改变了R值,并未改变A值,对客户来说是不变的的→“AF并非单射",从一个R值变成了另一个R值
  • 此种可变是无害的,甚至是有益的
  • 但这并不代表在不变的的类中就可以随意出现变值器。

10.从表示泄露中记录AF,RI和安全性

10.1.记录AF和RI

最好在类中记录抽象函数和表示不变量, 在代码中用注释形式记录AF和RI
对于RI来说,仅仅是一个“所有字段都有效”这样的一般性声明是不够的,要精确的记录RI:rep的所有fields何为有效

  • 表示不变量的工作是准确解释是什么使字段值有效或无效。

对于AF来说,提供一个通用的解释是不够的,比如"表示一组字符。", 要精确记录AF:如何解释每一个R值

  • 抽象函数的工作是精确定义如何解释具体的字段值。
  • 作为一项功能,如果我们采用记录的AF并替换为实际(合法)字段值,我们应该获得它们所代表的单个抽象值的完整描述.

10.2.记录表示泄露的安全声明

另一个需要记录的是表示泄漏的安全声明
这是一个注释,检查rep的每个部分,查看处理rep的那个部分的代码(特别是关于来自客户的参数和返回值,因为这是表示泄露发生的地方),并给出代码不暴露rep的原因。也就是:给出理由,证明代码并未对外泄露其内部表示——自证清白

10.3.总结:ADT的规约可能会谈到什么

ADT的规约里只能使用客户可见的内容来撰写,包括参数、返回值、异常等。

  • 这包括参数、返回值和由其操作引发的异常。

如果规约里需要提及"值",只能使用A空间中的"值":每当规约需要引用类型T的值时,它应该将该值描述为抽象值,即抽象空间A中的数学值。

规约里也不应谈及任何内部表示的细节,以及R空间中的任何值:规约不应该谈论表示的细节,或者表示空间的元素。

ADT的内部表示(私有属性)对外部都应严格不可见:它应该将rep本身(私有字段)视为对客户不可见,就像方法体及其局部变量被视为不可见一样。

这就是为什么我们把表示不变量和抽象函数写成类体中的普通注释,而不是类上面的Javadoc注释。故在代码中以注释的形式写出AF和RI而不能在Javadoc文档中,防止被外部看到而破坏表示独立性/信息隐藏

  • 将它们写成Javadoc注释将使它们成为类型规范的公共部分,这将有助于代表独立性和信息隐藏。

10.4.如何建立不变量

不变量是一个对整个程序都成立的属性——在关于一个对象的不变量的情况下,它减少到对象的整个生命周期。
为了保持不变,我们需要:

  • 使不变量在对象的初始状态下为真;
  • 确保对对象的所有更改保持不变。
  • 在对象的初始状态不变量为true,在对象发生变化时,不变量也要为true

根据ADT操作的类型对此进行翻译:

  • 构造器和生产器必须为新的对象实例建立不变量,也就是构造器和生产器在创建对象时要确保不变量为true
  • 变值器和观察器在执行时必须保持不变性。
  • 在每个方法返回之前,用checkRep()检查不变量是否得以保持

表示泄露的风险使情况更加复杂。
如果rep被泄露,那么对象可能在程序中的任何地方被改变,而不仅仅是在ADT的操作中,并且我们不能保证在那些任意改变之后不变量仍然成立。
▪ 表示泄漏的风险:一旦泄露,ADT内部表示可能会在程序的任何位置发生改变(而不是限制在ADT内部),从而无法确保ADT的不变量是否能够始终保持为真的.
所以证明不变量的完整规则——如果一个抽象数据类型的不变量是

  • 由构造器和生产器建立;
  • 由变值器和观察器保存;
  • 不出现表示泄露

则不变量适用于抽象数据类型的所有实例。
例:
在这里插入图片描述
图中

public double[] getAllSides(){
	return sides;
}
  • 1
  • 2
  • 3

这一部分将导致表示泄露,因此客户端可能会无意中更改返回数组中的值,并因此破坏不变量,即使遵守了所写的所有规范。

11. ADT不变量替换的前提条件

11.1. ADT不变量替换的前提条件

设计良好的抽象数据类型的一个巨大优势是,它封装并强制执行我们在前提条件中必须规定的属性。用ADT不变量取代复杂的precondition(前提条件),相当于将复杂的precondition封装到了ADT内部。

  1. 更安全地避免bug,因为所需的条件(没有重复的排序)可以在一个地方强制执行,即SortedSet类型,并且因为Java静态检查开始发挥作用,防止根本不满足该条件的值被使用,在编译时出错。
  2. 它更容易理解,因为它简单得多,SortedSet这个名字传达了程序员需要知道的东西。
  3. 它更容易改变,因为排序集的表示现在可以改变,而不需要改变独占或它的任何客户端。

例:
在这里插入图片描述

TweetList将能够表示tweets具有不同时间戳的要求
UserName能够代表对有效用户名的约束。

12 总结

  • 抽象数据类型以其操作为特征。
  • 操作可以分为构造器、生产器、观察器和变值器。
  • ADT的规约是它的一套操作和它们的规约。
  • 一个好的ADT是简单的、连贯的、充分的和独立于表现的。
  • ADT通过为其每个操作生成测试来测试,但是在相同的测试中一起使用构造器、生产器、观察器和变值器。
  • 远离bugs:一个好的ADT为数据类型提供了一个定义良好的契约,这样客户就知道从数据类型中期望什么,并且实现者有明确定义的自由去改变。
  • 易于理解:一个好的ADT把它的实现隐藏在一组简单的操作后面,这样使用ADT的程序员只需要理解操作,而不需要理解实现的细节。
  • 准备好改变:表示独立性允许抽象数据类型的实现改变,而不需要来自其客户端的改变
  • 不变量是一个属性,在对象的生命周期内,对于ADT对象实例总是如此。
  • 一个好的ADT保存自己的不变量。不变量必须由构造器和生产器建立,由观察器和变值器保存。
  • rep不变量指定了表示的合法值,应该在运行时用checkRep()进行检查。
  • 抽象函数将具体的表示映射到它所代表的抽象值。
  • 表示泄露威胁到表示独立性和不变量的保持。
  • 远离bugs:一个好的ADT保留了它自己的不变量,这样那些不变量就不太容易受到ADT客户端中的错误的影响,并且不变量的违反可以更容易地在ADT本身的实现中被隔离。显式声明rep不变量,并在运行时用checkRep()检查它,可以更早地捕捉误解和错误,而不是继续使用损坏的数据结构。
  • 易于理解:表示不变量和抽象函数解释了数据类型表示的意义,以及它与抽象的关系。
  • 准备好改变:抽象数据类型将抽象从具体的表示中分离出来,这使得无需更改客户端代码就可以更改表示。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值