HIT软件构造学习笔记5- Abstract Data Type (ADT)——1

Abstraction and User-Defined Types

每一种编程语言都有他自己内置的变量类型,例如大家再熟悉不过的intbooleanstrings等等。当然用户可以根据自己的需要定义自己的数据类型

数据抽象:
数据抽象是指由一组操作所刻画的数据类型,例如数字number就是一种能够进行加和乘的东西,而字符串string是一种可以进行连接和获得子串的东西

传统的类型定义:
程序员在使用早先时候的编程语言的时候,定义变量时更关注于数据的具体表示。例如,他们会创建一个int整型变量来存储一天的年月日日期

抽象类型:
抽象类型更加聚焦于操作。它的使用者无需担心它的数据实际上是怎么存储的,它的实现者也可以忽略编译器实际存储它的方式。所有所有需要关注的就是这个数据类型是怎么操作的。

需要记住,一个抽象数据类型是由他所能进行的操作定义的。对于一种类型<T>,T的操作和规约刻画了T的特征

为了更明了一点,举一个例子。当我们说到数据类型List的时候,我们说的既不是ArrayList亦或是LinkedList,我们说的只是List这一个数据类型和它能进行的操作,并不关注他具体是由链表实现还是由数组实现

ADT是由操作定义的,与其内部如何实现无关

 Classifying Types and Operations 

数据类型可以分为可变数据类型和不可变数据类型。

可变型的对象提供了可改变其内部数据的值的方法,而不可变数据类型的对象,他的操作其操作不可改变内部值,而是构造新的对象并且更改引用。

  • 构造器Creators:
    构造器的功能是实现一个抽象数据类型从无到有的过程。一个构造器所使用的参数可以是一个对象,但是不可以是正在构造的这个对象。
  • 生产器Producer:
    生产器的功能是从一个旧的对象中派生出一个新的对象,实现从有到新的过程。举一个例子吧,对象数据类型String中的.concat()方法的功能是连接两个字符串,它产生了一个新的对象,它实际上就是一个Producer。
  • 观察器Observers:
    观察器的功能是观察对象的某些性质,表现出来的结果就是返回一个跟对象不同类型的值。例如List中的.size()方法,它的返回值是一个int。
  • 变值器Mutators:
    变值器是指那些能够改变对象属性的方法。List中的.add()方法就是一个例子,它在原本的对象尾部添加了一个新的元素,使它本身的发生了改变。

对下表中的符号进行一些说明:

  • 每一个T是一个抽象数据类型本身
  • 每一个t是一些其他的数据类型
  • + 号意味着前面的符号出现一次或者更多次
  • * 号意味着前面的符号没出现过或者更多次
  • | 号就是逻辑操作符or
OPeffect
Creatorst* → T
ProducerT+, t* → T
ObserverT+, t* → t
MutatorT+, t* → void | t | T

Creator的标签
对于Creator来说的话,他在实现的过程中可能被弄成构造函数或者静态函数,而实现静态方法的Creator通常被称为工厂方法

Mutator的标签
Mutator通常都是void方法,也就是说它多数情况下都会返回void。那么如果它返回了void,那么一定意味着他改变了对象的某些内部状态。当然啦,Mutator返回值也可以是非空的类型。例如Mutator方法Set.add()返回值就是一个boolean类型,成功添加返回true,失败添加返回false
 

Abstract Data Type Examples

不可变数据类型String中的方法:

方法类型
CreatorsString constructors
Producers.concat(),.substring(),.toUpperCase
Observers.length,.charAt()
MutatorsNONE(It’s immutable)

可变数据类型List中的方法

方法类型
CreatorsArrayList and LinkedList constructors , Collections.singletonList
ProducersCollections.unmodifiableList
Observers.size(),.get()
Mutators.remove(),.addAll(),Collections.sort

解释一下最后一个BufferedReader.readLine()。在课后也有同学提问为什么这个方法是Mutator。老师给出了解答:当调用这个.readLine()方法时,内部的指针指向的位置放生了改变,因此为Mutaor 

Designing an Abstract Type

设计一个ADT包括了选择好的操作以及要决定它们怎么运作。良好的ADT设计要考经验法则,提供一组操作,设计其行为规约Spec

Rules of thumb 1 设计简洁、一致的操作

使用简单但是数量上多一些的小操作往往是更好的。通过简单操作的组合来实现更加复杂的操作,要远远比弄一个巨大巨复杂的方法好。而且操作应该是有着被精心设计过的目的,也就是说,方法的行为应该是内聚的,而不是包含一个非单一的全套动作

Rules of thumb 2 要足以支持client对数据所做的所有操作需要,且用操作满足client需要的难度要低

也就是说操作集的规模应该是足够大的,可以满足客户端想要进行的所有计算和访问。这个经验规则的判断方法也很简单,检测一下对象每一个需要被访问到的属性是否都能被访问到就可以了。

举一个例子来说明吧。对于ADTList来说,如果我们没有.get()方法,我们就无从访问List中的值;如果没有.size()方法就需要使用比那里的方法来获取数组的长度,比较麻烦

Rules of thumb 3 要么抽象、要么具体,不要混合 --- 要么针对抽象 设计,要么针对具体应用的设计
面向具体应用的ADT不应该包含通用的方法,同样的,面向通用类型的ADT也不应该包含一些特化的方法。

Representation Independence(表示独立性)

我们写一段代码,简单的实现一个家庭类,类中有一个方法可以调用方法返回家庭中的一个Person类:

/**
* Represents a family that lives in a household together.
* A family always has at least one person in it.
* Families are mutable.
*/
class Family {
		public List<Person> people;
		public List<Person> getMembers() {
				return people;
		} 
}
void client1(Family f) {
		Person baby = f.people.get(f.people.size()-1);
		...... 
}

我们看到客户端通过直接访问Family类中的List,使用List的方法作为客户端的实现体。这种做法乍一看是没问题的,但是如果我们更改了Family类的实现方式,将List改为Set,这个客户端将报废。究其原因就是因为客户端没有遵守RI

 An example of keeping RI

相反,对于上面的代码,我们修改客户端的实现体为以下,就RI,在调用的时候就不会出现问题了

 Testing an Abstract Data Type

针对ADT中不同类型的方法,有不同形式的测试手段。

  • 测试creators、producers和mutators
    我们可以通过使用observers来观察对象的运行结果是否符合我们的预期。
  • 测试observers
    同样的,我们通过mutator和其他两种类型对对象进行操作,在观察我们的对象,来看结果是否正确。

但同时也存在一定的风险:就是如果我们的检测所以依赖的其他方法有错误,可能导致被测试的方法测试结果失去参考价值。

Invariants(不变量)

能够保持他自己的不变量是一个优秀的ADT最重要的属性。而不变量是程序的一个属性,不论在程序的哪个阶段都保持为真。例如,Imuutability就是一个典型的不变量。ADT要自己保护自己的不变量,也就是说保护不变量是ADT的责任,而与client的任何行为都无关

Why are invariants required?

当我们知道了一个ADT在保护它的不变量之后,我们对代码中错误地方的推断就会省去很多不必要的麻烦。

举一个例子吧。如果我们知道String是一个不变量的话,当你在调试使用String的代码的时候,或者当你尝试为另一个ADT创建使用String的代码时,使用String而造成的问题就不在考虑范围内了。如果没有这个不变量,那么所有使用String的地方都需要进行相关的检查,检查它是否被改变了。因此,我们总是要假设client有恶意破坏ADT不变量的行为,我们称之为防御性编程
 

怎么样才能保护我们的不变量呢?我们观察下面一段代码:

 对于这段代码,第一个可变性威胁就是client可以直接访问到变量所在的区域。这是一个表示泄漏的简单例子。表示泄露是一个很严肃的问题,它一方面影响了不变量,另一方面也影响了表示独立性。它使得代码无法在不影响client的情况下改变其内部表示。

解决方案:
通过使用修饰符privatefianl来限定变量的可见性。

Rep Invariantand Abstraction Function

Two spaces of values

数据空间分为两类,分别为表示空间和抽象空间。表示空间为表示值构成的空间,放置的实现者看到和使用的值。在一般情况下的ADT的表示比较简单,但有些时候也需要较为复杂的表示。而抽象空间为抽象值构成的空间,放置的是client看到和使用的值

ADT开发者关注表示空间R,client关注抽象空间A。举一个例子,我们选择使用字符串来代表一组字符,那么表示空间R包含的就是一个一个的字符串,表示空间A包含的就是字符集合

 Mapping between R and A

RA之间使用一种映射关系来进行匹配,这种映射关系有如下的特点:

  1. 每一个抽象值都匹配于一个表示值,因此一定是满射
  2. 每一个抽象值不一定只有一个表示值与其对应,也就是说可能出现多对一的可能。因此未必单射
  3. 结合上两条,这种映射关系未必双射

Abstraction Function(AF)

抽象函数:

用来表示R和A之间映射关系的函数,即如何将R中的每一个值解释为A中的每一个值。AF:R → A

同样的,如果使用映射的特性来描述的话,AF也同样是满射、未必单射、未必双射的。R中的部分值并非合法的,在A中无映射值。

Rep Invariant: another important ADT invariants(RI)

RI(表示不变性)是将表示值对应给boolean的对应关系。对于一个表示值r,RI(r)为真当且仅当r通过AF有对应值。也就是说,可以将RI看做表示值集合的一个自己,它能够告诉我们一个表示值是否是合法的。例如下面的例子:

public class CharSet{
		private Strings;
		// Rep invariant:
		// 	s contains no repeated characters
		// Abstraction function:
		// 	AF(S) = {s[i] | 0 <= i < s.length()}
		......

可以看到上面的方法的RI接收所有不含重复字符的字符串,AF映射过去的集合是这个字符串中所有的字符构成的集合。

What determine AF and RI?

抽象空间自己是不足以决定AF或者RI的,对于同一个ADT,可以有多种表示;不同的内部表示,需要设计不同的AF和RI。选择某种特定的表示方式R,进而指定某个子集是合法的(RI),并为该子集中的每个值做出(AF)解释即如何映射到抽象空间中的值。

总结来讲,同样的表示空间R,可以有不同的RI;即使是同样的R、同样的RI,也可能有不同的AF,即解释不同

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值