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;
}
这一部分将导致表示泄露,因此客户端可能会无意中更改返回数组中的值,并因此破坏不变量,即使遵守了所写的所有规范。
11. ADT不变量替换的前提条件
11.1. ADT不变量替换的前提条件
设计良好的抽象数据类型的一个巨大优势是,它封装并强制执行我们在前提条件中必须规定的属性。用ADT不变量取代复杂的precondition(前提条件),相当于将复杂的precondition封装到了ADT内部。
- 更安全地避免bug,因为所需的条件(没有重复的排序)可以在一个地方强制执行,即SortedSet类型,并且因为Java静态检查开始发挥作用,防止根本不满足该条件的值被使用,在编译时出错。
- 它更容易理解,因为它简单得多,SortedSet这个名字传达了程序员需要知道的东西。
- 它更容易改变,因为排序集的表示现在可以改变,而不需要改变独占或它的任何客户端。
例:
TweetList将能够表示tweets具有不同时间戳的要求
UserName能够代表对有效用户名的约束。
12 总结
- 抽象数据类型以其操作为特征。
- 操作可以分为构造器、生产器、观察器和变值器。
- ADT的规约是它的一套操作和它们的规约。
- 一个好的ADT是简单的、连贯的、充分的和独立于表现的。
- ADT通过为其每个操作生成测试来测试,但是在相同的测试中一起使用构造器、生产器、观察器和变值器。
- 远离bugs:一个好的ADT为数据类型提供了一个定义良好的契约,这样客户就知道从数据类型中期望什么,并且实现者有明确定义的自由去改变。
- 易于理解:一个好的ADT把它的实现隐藏在一组简单的操作后面,这样使用ADT的程序员只需要理解操作,而不需要理解实现的细节。
- 准备好改变:表示独立性允许抽象数据类型的实现改变,而不需要来自其客户端的改变
- 不变量是一个属性,在对象的生命周期内,对于ADT对象实例总是如此。
- 一个好的ADT保存自己的不变量。不变量必须由构造器和生产器建立,由观察器和变值器保存。
- rep不变量指定了表示的合法值,应该在运行时用checkRep()进行检查。
- 抽象函数将具体的表示映射到它所代表的抽象值。
- 表示泄露威胁到表示独立性和不变量的保持。
- 远离bugs:一个好的ADT保留了它自己的不变量,这样那些不变量就不太容易受到ADT客户端中的错误的影响,并且不变量的违反可以更容易地在ADT本身的实现中被隔离。显式声明rep不变量,并在运行时用checkRep()检查它,可以更早地捕捉误解和错误,而不是继续使用损坏的数据结构。
- 易于理解:表示不变量和抽象函数解释了数据类型表示的意义,以及它与抽象的关系。
- 准备好改变:抽象数据类型将抽象从具体的表示中分离出来,这使得无需更改客户端代码就可以更改表示。