软件构造学习笔记第六章——抽象数据类型

本文深入探讨了抽象数据类型(ADT)的概念,包括抽象的类型和操作划分,重点讲解了表示独立、不变性及其在设计和测试中的应用。通过实例和规则阐述了如何设计好的ADT,以及编程时如何确保不变性和正确性。

目录

1.一些和抽象有关的概念

2.对抽象类型的类型和操作的分类

2.1类型的分类

2.2操作的分类

2.3例子

​编辑 ​编辑 3.抽象数据类型是被操作定义的

4.设计一个好的抽象类型ADT(四个方面)

5.表示独立(representation independence)

5.1例子

6.测试抽象类型

7.不变性(invariant)

7.1表示暴露(representation exposure)

7.2表示不变性和抽象函数(Rep invariant and abstraction function)

7.2.1A空间(abstraction space)和R空间(representation space)

7.2.2抽象函数(abstraction function)和表示不变性(rep invariant)

7.2.3 例子

7.2.4检查不变性

 7.2.5属性一般非空

7.2.6有益的转变

7.3记录RI、AF、rep exposure safety argument 

7.4RI和AF在ADT设计中的作用

7.5如何建立不变性

 8.ADT的规约

9.ADT的不变性替代规约的前置条件

10.如何编程(由小到大)

10.1编写方法

10.2编写ADT

10.3编写程序

11.总结


1.一些和抽象有关的概念

①抽象(Abstraction):用更抽象的语言或者描述来代替更细节、底层的操作。(例如规约就是一种抽象)

②模块(Modularity):将一个整体系统拆分成若干模块,每个模块都可以单独被设计、实现、检验等。

③封装(Encapsulation):每个模块不受其它模块的BUG的影响。(如使用private属性,或者返回值时不返回类内的地址)

④信息隐藏(Information hiding):隐藏有关于模块实现的信息,可以用于未来在保持模块功能而修改模块内部细节。(规约)

⑤关注点分离(Separation of concerns):每个模块专注于实现一个特定的功能,也不用把这种功能分散实现在更复杂的模块中。(一个好的模块是内聚的,即功能小而精,独立)

2.对抽象类型的类型和操作的分类

2.1类型的分类

从实现角度来说,有用户自定义类型和系统给定的类型(如string)。

从实例出来的对象是否可变来说,有可变类型和不可变类型。

2.2操作的分类

①构造器(creator):产生该类型的对象。可传入其它类型的参数来进行产生。(如果某构造器被设置为静态的,也成为工厂方法factory method)

②生产器(producer):产生该类型的对象。必须至少传入一个该类型的对象,传入或不传入其它类型的参数。 

③观察器(observer):产生一个其它类型的数据。必须至少传入一个该类型的对象,传入或不传入其它类型的参数。

④变值器(mutator):将传入该类型的对象的值进行改变。必须至少传入一个该类型的对象。(不可变类型和可变类型的区别在此,不可变类型没有变值器

(其中T为该类型的对象,t为其他类型的对象,左边是传入的参数,右边是返回值,一个+表示至少有一个对象,*是至少有零个对象)         

2.3例子

  3.抽象数据类型是被操作定义的

        抽象数据类型是被操作定义的,而不是具体的内部实现。举个例子,当我们在讨论Array这个类型时,我们并不是指的是内存块、hash table或者其它可能代表序列的结构体,相反,Array是一组不知道内部如何存储或实现的值,我们只知道它有一些和数组相关的操作如查找长度等。

        抽象数据类型的操作对用户是可见的,而抽象数据类型内部是属性字段、方法实现是对用户不可见的,只对实现者可见。

4.设计一个好的抽象类型ADT(四个方面)

①抽象类型应该由较简单的操作组成,这些操作可以组合拥有强大的功能,而不是由复杂而用处较少的操作组成。

②每个操作对应的是较为普遍的用法,而不是特殊情况,各个操作的应该处理相近类型的事情。比如一个运算类型,那么需要的操作就是加减乘除,而不要此时引入一个计算数组长度的操作。

③如果一个操作是常用的,那么就应该实现这个操作,哪怕它可以由其它操作交给用户来完成。比如求数组长度,用户可以根据知道数组索引来求得数组长度,但是由于求数组长度这个操作常用,我们就可以帮它实现,而不用交给用户再实现。

④类型可以是泛型如集合、数组、图等,也可以是特定类型如街道地图、电话簿等。但请不要将二者进行融合,不要在泛型里放入特定类型的操作,也不要在特定类型里放入泛型的操作。

5.表示独立(representation independence)

representation :the actual data structure or data fields used to implement the type.

注意:通常representation代表的就是类里面的字段如private int a = 1;等,只有此处的representation我觉得应该是类里面的所有实现代码。

表示独立意味着外部代码不依赖于其调用的类的内部实现方式,即如果外部代码调用了这个类的方法,同时类的该方法发生改变,但只要该改变仍然符合规约,那么对于外部代码调用无影响。

作为一个实现者,只有当ADT的操作完全指定了先决条件和后决条件时,您才能安全地更改其表示形式,这样客户才知道依赖什么,并且您知道可以安全地更改什么。

5.1例子

一个valueOf方法的两种实现方式。

方法一的属性

  

 方法二的属性:

 

 

 这个抽象类型有这两种不同的实现方式,但是对外部的结果都是相同的,体现了表示独立。

6.测试抽象类型

        测试构造器、生产器和变值器只能用观察器来测试;测试观察器则需要新建一个对象对观察。

7.不变性(invariant)

注:老师翻译成的是不变量,我觉得使用“不变量”容易给人带来混淆,这里的“变量”二字并非代码里的变量,因此我翻译成不变性。

         一个良好的抽象数据类型最重要的是它能够保持它的不变性。

        不变性就是一个类内的所定义的属性(也就是之前提到的representation,如private int a = 1;)始终符合某种条件。

        不变性是一个程序的特性,即无论在任何时候,这个特性都是成立的。例如不可变类型可以说是一个不变性,在遍历数组时,i<array.length恒成立是一个不变性。

        不变性由ADT负责,与客户端无关。

        保持ADT的不变性可以通过给类内部的属性设置为private类型避免外部代码调用修改和只允许通过类提供的方法来修改内部的属性等方式。

7.1表示暴露(representation exposure)

注:此处的表示的意思是一个类内的所定义的属性。

        表示暴露即类内部的属性能够直接被外部访问到,如属性设置为public类型,或者返回值是一个类内部的可变类型的属性就会导致表示暴露。表示暴露会破坏表示独立,即当修改类内部实现代码时,可能会影响到外部使用到该内部属性的地方。

        因此,在编写一个类时,应该仔细检查每个属性的类型(也不是说public不对,如果public的是不可变类型,那么也依旧不会导致错误,不会破坏表示独立)以及每个方法返回值的类型。

        表示暴露对表示独立和保持不变性都有影响。

7.2表示不变性和抽象函数(Rep invariant and abstraction function)

7.2.1A空间(abstraction space)和R空间(representation space)

        A空间指的是从客户角度来看,是客户想要的值组成的空间。例如,客户想要的是一个大整数,那么所有的整数值组成的就是A空间。

        R空间指的是由若干个对象(可能不止一个,通常是多个)组成的实现A空间的值的空间。如果A空间是所有整数值组成的,那么R空间可以是数值类型的数组,用来表示数字。

例子:一个客户想要的是一个字符集合,那么A空间就是字符集合。R空间可以是用字符数组来存储字符,表示一个字符集合。

        每个A空间的值都应该被映射到。

        每个A空间的值可以被一个或多个映射(因为R空间的实现方式有多种,比如假设A空间是真和假,那么R空间可以是用1代表真,0代表假,同样也可以用T代表真,用F代表假)。

        R空间的值不一定有对应的A空间的值。 

7.2.2抽象函数(abstraction function)和表示不变性(rep invariant)

抽象函数:从R空间到A空间的映射。AF:R--->A.

通俗地讲,抽象函数代表的就是怎么用代码来实现客户的需求。

表示不变性:从R空间到布尔值的映射。RI:R--->T|F。如果一个R空间的值i存在到A空间的映射,那么RI(i)=T,否则为F。

通俗地讲,表示不变性代表的就是哪些代码(数据)可以用来实现客户的需求。

举个例子,RI是不接受重复的字符的字符数组,AF是字符数组一一映射到集合元素,则有:

 抽象函数和表示不变性应该被记录在代码中,在声明的属性附近,如下:

        大多数人总有个疑问,如果给定了A空间和R空间,那么AF和RI不就确定下来了吗?这是不对的。实现一个抽象数据类型,不仅仅是选择A空间和R空间,而且还要判断哪些属性是合法的,选择如何将这些属性映射到相应的A空间(因为总有许多种映射方式)。 

例题:

①以下哪些是对用户可见的?

②以下哪些是对实现者可见的?

③表示不变性

7.2.3 例子

A空间:用最简分数表示的有理数。

R空间:使用成对的大整数。

RI:分母必须为正数且分子分母最简。

AF:对应相应的有理数。

7.2.4检查不变性

        可以在构造器、变值器和产生器后对不变性进行检查,如上图在调用构造器的末尾插入了this.checkRep(),而且这个检查函数应该设置为private,因为检查是实现者的事情,不要给用户看到。

 

 7.2.5属性一般非空

7.2.6有益的转变

        即保持抽象空间不变,但是可以改变表示空间和RI和AF且仍然拥有着同样的映射,这种转变是允许的。

7.3记录RI、AF、rep exposure safety argument 

        记录的时候必须写得明确。RI仅仅写成“所有字段都有效”这样的通用语句是不够的,AF仅仅写成“表示一组字符”是不够的。最好写成函数形式。

7.4RI和AF在ADT设计中的作用

设计ADT:①选择R和A;②确定RI,合法的表示值;③确定AF,如何表示;④将以上的设计和选择明确详细地写到代码中。

7.5如何建立不变性

①构造器和生产器在创建对象时要确保不变性为真。

②观察器和变值器在执行时不能改变对象的不变性。

③在每个方法返回之前,用checkrep()来检查不变性是否保持。

判断是否保持不变性:

①拥有不变性的属性是由构造器和产生器生成的。

②在调用观察器和变值器时不改变不变性。

③没有表示暴露。

 8.ADT的规约

(一图胜千言)

        ADT的规约只能写参数、返回值、异常、操作的规约和A空间的值,不能有内部表示的细节、变量、RI、AF等等。

9.ADT的不变性替代规约的前置条件

替换成:

相当于将复杂的前置条件封装到了ADT内部。

10.如何编程(由小到大)

10.1编写方法

10.2编写ADT

10.3编写程序

11.总结

(以上图片均来源于网络Reading 11: Abstraction Functions & Rep Invariants

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值