More Examples of ADTs
更多的ADT示例
假设你开发了一套软件来控制一个核反应堆的冷却系统。你可以为这个冷却系统规定如下一些操作,从而将其视作一个抽象数据类型:
coolingSystem.GetTemperature()
coolingSystem.SetCirculationRate(rate)
coolingSystem.OpenValve(valveNumber)
coolingSystem.CloseValve(valveNumber)
实现上述各操作的代码由具体环境决定。程序的其余部分可以用这些函数来操纵冷却系统,无须为数据结构的实现、限制及变化等内部细节而操心。
下面再举一些抽象数据类型以及它们可能提供的操作:
|
巡航控制 设置速度 获取当前设置 恢复之前的速度 解散 |
搅拌机 开启 关闭 设置速度 启动“即时粉碎器” 停止“即时粉碎器” |
油罐 填充油罐 排空油罐 获取油罐容积 获取油罐状态
|
|
列表 初始化列表 向列表中插入条目 从列表中删除条目 读取列表中的下一个条目 |
灯光 开启 关闭 |
堆栈 初始化堆栈 向堆栈中推入条目 从堆栈中弹出条目 读取栈顶条目 |
帮助屏幕 添加帮助项 删除帮助项 设置当前帮助项 显示帮助屏幕 关闭帮助显示 显示帮助索引 返回前一屏幕 |
菜单 开始新的菜单 删除菜单 添加菜单项 删除菜单项 激活菜单项 禁用菜单项 显示菜单 隐藏菜单 获取菜单选项 |
文件 打开文件 读取文件 写入文件 设置当前文件位置 关闭文件
电梯 |
|
指针 获取新分配内存的指针 用现有指针释放内存 更改已分配内存的大小 |
到上一层 到下一层 到指定层 报告当前楼层 回到底层 |
通过研究这些例子,你可以得出一些指导建议,下面就来说明这些指导建议:
把常见的底层数据类型创建为ADT并使用这些ADT,而不再使用底层数据类型 大多数关于ADT的论述中都会关注于把常见的底层数据类型表示为ADT。从前面的例子中可以看到,堆栈、列表、队列以及几乎所有常见的底层数据类型都可以用ADT来表示。
你可能会问:“这个堆栈、列表或队列又是代表什么呢?”如果堆栈代表的是一组员工,就该把它看做是一些员工而不是堆栈;如果列表代表的是一个出场演员名单,就该把它看做是出场演员名单而不是列表;如果队列代表的是电子表格中的一组单元格,就该把它看做是一组单元格而不是一个一般的队列。也就是说,要尽可能选择最高的抽象层次。
把像文件这样的常用对象当成ADT 大部分编程语言中都包含有一些抽象数据类型,你可能对它们已经比较熟悉了,而只是可能并未将其视作ADT。文件操作是个很好的例子。在向磁盘写入内容时,操作系统负责把读/写磁头定位到磁盘上的特定物理位置,如果扇区的空间用完了,还要重新分配新扇区,并负责解释那些神秘的错误代码。操作系统提供了第一层次的抽象以及在该层次上的ADT。高层语言则提供了第二层次的抽象以及在这一更高层次上的ADT。高级语言可以让你无须纠缠于调用操作系统API以及管理数据缓冲区等繁琐细节,从而让你可以把一块磁盘空间视做一个“文件”。
你可以采用类似的做法对ADT进行分层。如果你想在某一层次用ADT来提供数据结构的操作(比如说在堆栈中压入和弹出数据),没问题。而你也可以在这一抽象层次之上再创建一个针对现实世界中的问题的抽象层次。
简单的事物也可当做ADT 为了证明抽象数据类型的实用价值,你不一定非要使用庞杂的数据类型。在前面的一组例子中,有一盏只支持两种操作(开启、关闭)的灯。你可能会觉得把简单的“开”、“关”操作放到单独的子程序中有些浪费功夫,不过即使这样简单的操作也可以通过使用ADT而获益。把灯和与之相关的操作放到一个ADT里,可以提高代码的自我说明能力,让代码更易于修改,还能把改动可能引起的后果封闭在TurnLightOn()和TurnLightOff()两个子程序内,并减少了需要到处传递的数据的项数。
不要让ADT依赖于其存储介质 假设你有一张保险费率表,它太大了,因此只能保存到磁盘上。你可能想把它称做一个“费率文件”然后编出类似RateFile.Read()这样的访问器子程序(access routine)。然而当你把它称做一个“文件”时,已经暴露了过多的数据信息。一旦对程序进行修改,把这张表存到内存中而不是磁盘上,把它当做文件的那些代码将变成不正确的,而且产生误导并使人迷惑。因此,请尽量让类和访问器子程序的名字与存储数据的方式无关,并只提及抽象数据类型本身,比如说“保险费率表”。这样一来,前面这个类和访问器子程序的名字就可能是rateTable.Read(),或更简单的rates.Read()。
Handling Multiple Instances of Data with ADTs in Non-Object-
Oriented Environments
在非面向对象环境中用ADT处理多份数据实例
面向对象的编程语言能自动支持对同一ADT的多份实例的处理。如果你只是在面向对象的环境中工作,那你根本就不用自己操心处理多个实例的实现细节了,恭喜你!(你可以直接去读下一节“ADT和类”。)
如果你是在像C语言这样的非面向对象的环境中工作,你就必须自己手工实现支持处理多个实例的技术。一般来说,这就意味着你要为ADT添加一些用来创建和删除实例的服务操作,同时需要重新设计ADT的其他服务操作,使其能够支持多个实例。
前面字体那个ADT原来只是提供这些操作:
currentFont.SetSize(sizeInPoints)
currentFont.SetBoldOn()
currentFont.SetBoldOff()
currentFont.SetItalicOn()
currentFont.SetItalicOff()
currentFont.SetTypeFace(faceName)
在非面向对象的环境里,这些操作不能附着在某个类上,此很可能要写成:
SetCurrentFontSize( sizeInPoints )
SetCurrentFontBoldOn()
SetCurrentFontBoldOff()
SetCurrentFontItalicOn()
SetCurrentFontItalicOff()
SetCurrentFontTypeFace( faceName )
如果你想一次使用更多的字体,那么就需要增加一些服务操作来创建和删除字体的实例了,比如说这样:
CreateFont(fontId)
DeleteFont(fontId)
SetCurrentFont(fontId)
这里引入了一个fontId变量,这是用来在创建和使用多个字体实例时分别控制每个实例的一种处理方法。对于其他操作,你可以采用下列三种方法之一对ADT的接口进行处理:
■ 做法1:每次使用ADT服务子程序时都明确地指明实例。在这种情况下没有“当前字体”的概念。你把fontId传给每个用来操作字体的子程序。Font ADT的服务子程序负责跟踪所有底层的数据,而调用方代码只需使用不同的fontId即可区分多份实例。这种方法需要为每个Font子程序都加上一个fontId参数。
■ 做法2:明确地向ADT服务子程序提供所要用到的数据。采用这种方法时,你要在调用ADT服务的子程序里声明一个该ADT所要用到的数据。换句话说,你要声明一个Font数据类型,并把它传给ADT中的每一个服务子程序。你在设计时必须要让ADT的每个服务子程序在被调用时都使用这个传入的Font数据类型。用这种方法时,调用方代码无须使用fontId,因为它总是自己跟踪字体数据。(虽然从Font数据类型即可直接取得所有数据,但你仍然应该仅通过ADT的服务子程序来访问它。这称为保持结构体“封闭”。)
这种方法的优点是,ADT中的服务子程序不需要根据fontId来查询字体的信息。而它的缺点则是向程序的其余部分暴露了字体内部的数据,从而增加了调用方代码可能利用ADT内部实现细节的可能性,而这些细节本应该隐藏在ADT的内部。
■ 做法3:使用隐含实例(需要倍加小心)。设计一个新的服务子程序,通过调用它来让某一个特定的字体实例成为当前实例——比方说SetCurrentFont(fontId)。一旦设置了当前字体,其他所有服务子程序在被调用时都会使用这个当前字体。用这种方法也无须为其他服务子程序添加fontId参数。对于简单的应用程序而言,这么做可以让使用多个实例更为顺畅。然而对于复杂的应用程序来说,这种在系统范围内对状态的依赖性就意味着,你必须在用到字体操作的所有代码中跟踪当前的字体实例。这样一来,复杂度有可能会急剧增长,对于任何规模的应用程序来说,还有一些更好的替代方案。
在抽象数据类型的内部,你还可以选择更多处理多个实例的方法;但在抽象数据类型的外部,如果你使用非面向对象的编程语言的话,能选择的方法也就是这些了。
ADTs and Classes
ADT和类
抽象数据类型构成了“类/class”这一概念的基础。在支持类的编程语言里,你可以把每个抽象数据类型用它自己的类实现。类还涉及到继承和多态这两个额外的概念。因此,考虑类的一种方式,就是把它看做是抽象数据类型再加上继承和多态两个概念。







