软件构造ADT总结
软件构造ADT总结
前面讲了方法的操作与其规约以及数据类型及其特性,现在将数据和操作复合起来,构成ADT
抽象与用户定义的类型
除了编程语言所提供的基本数据类型和对象数据类型,程序员可定义自己的数据类型。
数据抽象
指的是由一组操作刻画的数据类型。
传统的类型定义关注于变量的具体表示,比如说设计一个日期Date的数据类型,回先考虑用Integer表示年月日
而抽象类型强调“作用于数据上的操作”,程序员和 client无需关心数据如何具体存储的,只需设计/使用操作即可。

比如bool类型的数据,我们可以用一个bit表示也可以用字符串来表示,但是只要实现了上述操作与spec即可。通过抽象实现了数据类型与具体的数据结构内存存储以及实现相分离
例如List类型,当我们讨论List类型时我们不会关心到底是用ArrayList还是LinkedList实现的具体的List的数据类型,我们讨论的是一个由不透明的值(可能的具有List类型的对象)构成的集合,其中的元素符合了规约中对操作的描述如:List.get(),List.size()。
总结下来ADT是由操作定义的与内部实现无关

对ADT类型与操作的分类
可变与不可变类型:与第四节讲的相似,可变类型的对象:提供了可改变其 内部数据的值的操作如:StringBuilder不可变数据类型: 其操作不可 改变内部值,而是构造新的对象如String
ADT操作的分类
- Creator创造器:指的是从无到有创造一个ADT
- Producer生产器:从已有的ADT(可能不止一个)创造一个同类型的ADT如String的concat方法
- Ob'server观察器:利用已有的ADT类型返回一个其他类型的值
- Mutator变值器:改变某个对象的属性的方法
用映射表示如下图:

creator的分类
-
实现为构造函数(与类重名的那个)
List array = new ArrayList();
-
实现为静态函数:这类构造器也通常称为工厂方法
Array.asList() List.of() String a = String.valueof(Object Obj);//其会返回参数的字符串形式
mutator方法的签名
方法签名:方法名和形参列表共同组成方法签名
变值器通常返回void类型此时该数据类型一定已经改变,但其也可以返回非空类型。例如Set.add返回boolean类型表示增加操作是否成功。
ATD的例子
首先对于不可变数据类型如int与String来说,其没有mutator的方法,具体如下
对于可变类型List来说其四种类型的方法都有如右图 | ![]() |
---|
解析几道题:

首先对于第一行其根据输入的数字或String构造一个Integer类,其次需要注意的是倒数第二个与前面的第四章的包装类的联系其根据输入的List包装出一个会在运行时检测是否出现改变操作的新类所以为Producer其次为最后一行每次调用这个方法对应得BufferReader中的内容均会发生改变所以为mutator
设计ADT
没有具体的公式但是由以下几个经验法则
- 设计简洁、一致的操作。通过简单操作的组合实现复杂的操作。操作的行为应该是内聚的(简单单一不要实现过多的功能)。
- 要足以支持client对数据所做的所有操作需要,且用操作满足client需要的难度要低, 判断方法:对象每个需要被访问到的属性是否都能够被访问到。例如没有get操作就无法获得list的内部数据,或者是用遍历获取list的size太复杂,将其封装为size方法
- 要么抽象、要么具体,不要混合 --- 要么针对抽象设计,要么针对具体应用的设计。例如面向具体的应用的类型中不应该有通用的方法,面向通用类型不应该包含具体的方法。
表示独立性
representation (the actual data structure or data fields used to implement it)在类中具体表示为数据结构或者数据域
表示独立性:client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端。
具体举个例子,我们用两种方法实现一个叫MyString的类
-
第一种数据结构用一个不可变的String类表示
方法如下:
当我们调用其中的方法时得到的diagram如下图:
-
第二种实现方法的表示通过以下的数据结构来实现:
private char[] a; private int start; private int end
其实现为
这样当进行substring操作时不必再初始化一个新的String对象只需要把指针指向对应得String即可
当运行时其diagram如下图:
区分规约,表示,实现三个名词
用一个具体的例子来解释
- 规约(Specification)除了注释的部分(及/加两个*的部分)还有签名(及类名方法名)的部分,如果内部的方法也有规约那么也包括那一部分。(its set of operations and their specs.)
- 表示(Representation)数据结构或者数据域
- 实现(Implementation):具体的方法中的部分如return
如何测试一个ADT
我在测试ADT时需要把其中的方法都测试一遍。及测试creators, producers, and mutators时:调用observers来观察这些 operations的结果是否满足spec;测试observers时:调用creators, producers, and mutators等方法产生或改变对象,来看结果是否正确。
这样会有风险:如果被依赖的其他方法有错误,可能导致被测试方法的测试结果失效
因此我们需要在保证一个方法正确的前提下再去测试其他方法,我们同时需要为其写详细的测试策略来表明我们的测试顺序
不变性
不变性:程序在任何时候总是true的性质,。如Immutability就是一个典型的"不变量",一个不可变类信息的对象在其一生中的值均相同。
ADT需要始终保持其不变量,与client端的任何行为无关。
为什么需要不变量:保持程序的正确性,容易发现错误。例如我们使用String的时候总是假设其不变性来推测程序是否正确,如果没有这个不变量在所有使用String的地方,都要检查其中的String是否改变了。我们总是要假设client有破坏不变性的行为,使用防御性拷贝等方法来防止。
为保证不变性
- 首先需要注意让client无法直接 访问ADT的field。及通过private关键字保证数据或方法只能在内部访问,或者通过final关键字保证数据域在对象被创造后不会被重新分配。
举个例子:

我们在client端运行以上代码时会发现我们不仅在客户端可以影响在Spec中声明的表示不变性,也会影响表示独立性:一旦我们在ADT内部改变其实现其影响会传递到客户端因为其引用了对应ADT中的数据。
- 我们最好通过Immutable的类型构造Rep,这样来彻底防止表示泄露。我们不能将希望都寄托于client端
总结一下,首先我们尽量不要把可变参数包括到项目中,当我们返回参数是我们可以采用new一个新对象的方法或者是返回可变对象的不可变包装。我们要尽可能使用不可变表示来代替最终使用的防御拷贝。
表示不变性与抽象函数
两类空间(R,A)
R:rep Values实现者看到与使用的值
A:abstract values:client看到与使用的值
开发者关注R空间,client关注A空间
R空间与A空间之间的映射
满足以下条件:
-
必须是满射,及用户的所有值在R空间中都有值与之对应
-
未必单射,R空间有些值对应到A空间中的同一个值
-
未必双射,因为可能有的R空间中的值无法对应到A空间中
抽象函数
AF:R - > A ,就是我们上面描述的R与A之间的映射
R与A之间映射关系的函数,及如何将R中的每个值解释为A中的每个值
AF具有上面说的满射非单射非双射的特征,并且我们会发现R中的部分值由于在A空间内无对应值因此是非法的
表示不变性
表示不变性是将R空间中的值映射到boolean值的一个映射。
对于R空间中的任意一个值r,RI(r)的值为true当且仅当r被AF映射,及这个值是合法的
或者我们也可以把RI看成是一个集合,其是R的一个子集并且其中的元素是AF的定义域上的元素
如何在程序中注释AF与RI
我们应该在rep之后书写RI与AF。如图中其AF为满射不为单设与双射
什么决定了RI与AF
光有内部的数据类型及表示无法决定RI与AF。我们应该选择某种特定的表示方式R,进而指定某个子集是“合法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射到抽象空间中的值。
举几个例子证明光有其中任何一个均无法决定另外几个
首先对于同样的R值可以有不同的RI:
其次同样的RI与R也可能AF不同,及解释不同:
如何利用RI与AF设计ADT
设计ADT:
- 选择R和A
- RI --- 合法的表示值
- 如何解释合法的表示值 ---映射AF,做出具体的解释:每个rep value如何映射到abstract value,而且要把这种选择和解释明确写到代码当中
练习题:
- 首先A空间是client直接打交道的空间因此是必须知道的;AF是ADT内部的不变量是实现细节与用户无关;Creator与Observer是用户直接使用的因此必须知道;Rep与RI是对用户封闭的不变性的内容,这里需要知道的是RI与用户的正确输入无关,其是在已经确定用户正确输入空间的基础上对R空间的筛选因此用户威知道
- 对于developer而言这些值全部需要知道
对于这个题首先要明确RI映射的值域为boolean,通过其一定能判断R空间中的某个值是不是合法的的AF的输入第三个第四个与最后一个均不是判断性的语句因此不正确
使用checkrep随时检查rep
举个例子有表示有理数的类:
我们要随时检查其RI是否改变,在所有可能改变rep的方法内部都要检查,即使obsever方法也需要检查以防万一。
有益的变化
之前我们提到过一个类型是不可改变的当且仅当其在被创造后不会改变。我们在R与A的理解上我们可以完善该定义:抽象值应当永远不变,但是我们的R空间的值可以改变只要它对应的A空间值不变这样其变化对客户端就不会可见。这种改变叫做有益的变化。
举个例子:
我们之前讨论过的表示有理数的类,其参与运算时,可以有公约数;显示输出时,需要简化(没有公约数),此时我们发现在一个Immutable的类里对R值做了改变。
虽然这种mutation只是改变了R值,并未改变A值,对client来说是 immutable的 →“注意AF并非单射我们改变后的R值对应的A值仍然相同”。这种变化不是有害的,甚至可能为有益的。
但这并不代表在immutable的类中就可以随意出现mutator。
撰写RI,AF,以及Safety from Rep Exposure
要精确的记录RI:rep中的所有fields何为有效,AF:如何解释每一个R值,给出理由,证明代码并未对外泄露其内部表示——自证清白。
例子:
ADT的规约主要包括什么
ADT的规约里只能使用client可见的内容来撰写,包括参数、返回值、异常等,如果规约 需要提及“值”,只能使用A空间 中的“值”。ADT的规约里也不应谈及任何内部表示的细节,以及R空间中的任何值。ADT的内部表示(私有属性)对外部都应严格不可见。故在代码中以注释的形式写出AF和RI而不 能在Javadoc文档中,防止被外部看到而破坏表示独立性/信息隐藏
使用ADT的不变性代替前置条件
前面我们讲述规约时提到了前置条件,它可以视作函数的输入值。我们用ADT的不变性代替前置条件的意思是我们可以把对client端的输入不变性转化为某个ADT,在这个ADT中该不变性为其内部属性。
例如这个例子我们把对客户端的排序要求换为了要求输入一个SortedSet而在SortedSet的不变性及其A空间满足了这个要求。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】