在软件构造课程学习的第三章中,我们学习了抽象数据类型(Abstract Date Type),其中有几个初学时较难理解的概念,在此做一总结。
ADT中的方法分类
在一个ADT里设计的所有方法,大都可以分为以下四类:
- 构造器(Creators):用于创建一个新的对象,主要表现为构造函数。典型的有Integer.valueOf()。
- 生产器(Producers):通过接受同类型的对象以创建新的对象。典型的有String.contact()。
- 观察器(Observers):获取ADT内部的某个信息,而不改变ADT的现有状态。典型的有List.size()。
- 变值器(Mutators):改变对象内部的信息。典型的有List.add()。
以上四类方法接受的参数类型和产生的返回值类型关系如下:
其中,T表示ADT本身的类型,t表示除了ADT之外的其他类型。“ * ” 表示出现0次或多次,“ + ” 表示出现1次或多次,“ | ” 表示或。
表示独立性(Representation Independence, RI)
ADT的表示独立性,要求客户在使用ADT时无需考虑ADT的内部实现,只需要根据ADT的规约进行使用即可。即ADT的内部实现变化不应该影响用户在客户端的使用。表示独立性的关键在于将数据结构的使用和数据结构自身的形式分离。防止因为用户在使用过程中假设ADT内部的实现,在假设的基础上形成依赖。
不变量(Invariants)和表示泄露(Representation Exposure)
不变量是指在ADT创建之后就不会发生改变的量,在ADT存在的时间范围内恒为True,不受外部使用者的操作所影响。若ADT的不变量发生改变,则说明这个ADT已经发生变异,程序运行过程中的某个地方存在bug。
而表示泄露形容了一种类外部的代码可以直接修改类内部存储的数据的现象。通常是类外代码直接对类内的可变类型对象进行直接修改,导致了之前所提到过的引用别名现象,这种引用方式会导致我们对同一块内存进行多次引用,导致程序运行过程中的不确定性,恶意代码也可以十分简单的对ADT进行攻击,严重影响ADT的表示独立性和不变量。
为了防止表示泄露,我们通过可以采用以下几种方法:
- 使用private和final关键词对域进行修饰。
- 使用防御性拷贝,需要注意的是,防御性拷贝可以发生在传入和传出数据时。
- 通过规约对用户的行为进行限制。
- 使用不可变类型的数据构建ADT。
表示不变量(Representation Invariant)和抽象函数(Abstract Function)
表示空间R和抽象空间A
在介绍RI和AF之前,我们首先引入表示空间R和抽象空间A的概念。如下图:
表示空间R指的是开发人员实际实现时内部的值,而抽象空间A表示的是用户看到的和使用的值。开发人员更关注R,而用户更关注A。
抽象函数AF
有了以上的两个概念,我们就可以给出抽象函数的概念。抽象函数即是描述从R到A的映射关系的函数,即如何将表示空间中的一个值解释为抽象空间中的一个值。AF通常是满足以下关系的一个映射:
- AF是一个满射,即用户所看到或使用的任意一个值都是由一个表示值映射而来的。
- AF未必是一个单射,即用户户所看到或使用的任意一个值可能由不止一个表示值映射而来。
- AF未必是一个双射,即开发人员所面对的表示值中,会存在不满足前置条件的表示值,对于这类表示值不存在对于的抽象值。
表示不变量
由上面的讲述,我们知道了存在一定的表示值,其不满足规约中的前置条件,故无法进行映射。这就引出了一个问题:怎样的表示值无法被映射呢? 表示不变量RI就定义了这个问题,RI是由表示值空间R到boolean值的一个映射,其中映射为true的值可以被映射到抽象空间中,否则无法被映射。
我们可以这样理解,RI告诉了我们表示空间R中的值是否能被映射到抽象空间A中,也可以认为RI构成了表示空间R的一个子集,该子集中的值能够被映射到抽象空间A中。
以上概念的联系
在构建ADT时,我们需要有以下步骤:选定表示空间(R),进而找出其中满足条件的子集(RI),并为子集中的每个元素做出对应的解释(AF),最终将其映射到抽象空间(A)中。