软件构造学习笔记(六)抽象数据类型
目录链接
- Part I Abstraction and User-Defined Types
- Part II Classifying Types and Operations
- Part III Abstract Data Type Examples
- Part IV Designing an Abstract Type
- Part V Representation Independence
- 5.1An example of violating RI
- 5.2An example of keeping RI
- Part VI Testing an Abstract Data Type
- Part VII Invariants
- 6.1Why are invariants required?
- 6.2Take actions!
- Part VII Rep Invariantand Abstraction Function
- 8.1Two spaces of values
- 8.2Mapping between R and A
- 8.4Abstraction Function
- 8.5Rep Invariant: another important ADT invariants
- 8.5What determine AF and RI?
- 8.6Some Example
- 8.7CheckRep
- Part IX Beneficent mutation
- Part X Documenting the AF, RI, and Safety from Rep Exposure
文章的内容是我在复习这门课程时候看PPT时自己进行的翻译和一些总结,如果有错误或者出入希望大家能和我讨论!同时也希望给懒得翻译PPT而刷到这篇博客的你一些帮助!
Part I Abstraction and User-Defined Types
每一种编程语言都有他自己内置的变量类型,例如大家再熟悉不过的int
、boolean
、strings
等等。当然用户可以根据自己的需要定义自己的数据类型。
数据抽象:
数据抽象是指由一组操作所刻画的数据类型,例如数字number就是一种能够进行加和乘的东西,而字符串string是一种可以进行连接和获得子串的东西。
- 传统的类型定义:
程序员在使用早先时候的编程语言的时候,定义变量时更关注于数据的具体表示。例如,他们会创建一个int整型变量来存储一天的年月日日期。 - 抽象类型:
抽象类型更加聚焦于操作。它的使用者无需担心它的数据实际上是怎么存储的,它的实现者也可以忽略编译器实际存储它的方式。所有所有需要关注的就是这个数据类型是怎么操作的。
需要记住,一个抽象树类型是由他所能进行的操作定义的。对于一种类型<T>,T的操作和规约刻画了T的特征。
为了更明了一点,举一个例子。当我们说到数据类型List的时候,我们说的既不是ArrayList
亦或是LinkedList
,我们说的只是List这一个数据类型和它能进行的操作,并不关注他具体是由链表实现还是由数组实现。
图1.1 抽象数据类型
Part II Classifying Types and Operations
数据类型可以分为可变数据类型和不可变数据类型。可变型的对象提供了可改变其内部数据的值的方法,而不可变数据类型的对象,他的操作其操作不可改变内部值,而是构造新的对象并且更改引用。关于这一部分的详细内容可以看一看我的笔记四。
接下来介绍非常重要的几个概念:
- 构造器Creators:
构造器的功能是实现一个抽象数据类型从无到有的过程。一个构造器所使用的参数可以是一个对象,但是不可以是正在构造的这个对象。 - 生产器Producer:
生产器的功能是从一个旧的对象中派生出一个新的对象,实现从有到新的过程。举一个例子吧,对象数据类型String
中的.concat()
方法的功能是连接两个字符串,它产生了一个新的对象,它实际上就是一个Producer。 - 观察器Observers:
观察器的功能是观察对象的某些性质,表现出来的结果就是返回一个跟对象不同类型的值。例如List中的.size()
方法,它的返回值是一个int。 - 变值器Mutators:
变值器是指那些能够改变对象属性的方法。List中的.add()
方法就是一个例子,它在原本的对象尾部添加了一个新的元素,使它本身的发生了改变。
对下表中的符号进行一些说明:
- 每一个T是一个抽象数据类型本身
- 每一个t是一些其他的数据类型
- + 号意味着前面的符号出现一次或者更多次
- * 号意味着前面的符号没出现过或者更多次
- | 号就是逻辑操作符or
OP | effect |
---|---|
Creators | t* → T |
Producer | T+, t* → T |
Observer | T+, t* → t |
Mutator | T+, t* → void | t | T |
Creator的标签
对于Creator来说的话,他在实现的过程中可能被弄成构造函数或者静态函数,而实现静态方法的Creator通常被称为工厂方法。
Mutator的标签
Mutator通常都是void方法,也就是说它多数情况下都会返回void。那么如果它返回了void,那么一定意味着他改变了对象的某些内部状态。当然啦,Mutator返回值也可以是非空的类型。例如Mutator方法Set.add()
返回值就是一个boolean类型,成功添加返回true
,失败添加返回false
。
Part III Abstract Data Type Examples
这一部分举一些例子,让大家更好的熟悉一下抽象数据类型
首先我们来看一看一个不可变数据类型String中的方法:
方法 | 类型 |
---|---|
Creators | String constructors |
Producers | .concat(),.substring(),.toUpperCase |
Observers | .length,.charAt() |
Mutators | NONE(It’s immutable) |
再来看一个可变数据类型List中的方法
方法 | 类型 |
---|---|
Creators | ArrayList and LinkedList constructors , Collections.singletonList |
Producers | Collections.unmodifiableList |
Observers | .size(),.get() |
Mutators | .remove(),.addAll(),Collections.sort |
做一个小练习吧~
图3.1 练习题
这里解释一下最后一个BufferedReader.readLine()
。在课后也有同学提问为什么这个方法是Mutator。老师给出了解答:当调用这个.readLine()
方法时,内部的指针指向的位置放生了改变,因此为Mutaor。
Part IV 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也不应该包含一些特化的方法。
Part V Representation Independence
这一部分的内容很重要,讲的是表示独立性的内容,在后面的学习中有很多使用
首先说一下表示独立性的概念:客户端在使用ADT的时候无需考虑其内部是如何实现的,ADT内部标识的变化不影响外部Spec和客户端。
这个概念很重要,它保证了通过前提条件和后置条件可以充分刻画了ADT的操作,Spec规定了客户端和实现这之间的契约,明确了客户端可以知道依赖哪些内容,实现者知道可以安全更改的内容。
5.1An example of violating RI
我们写一段代码,简单的实现一个家庭类,类中有一个方法可以调用方法返回家庭中的一个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。
5.2An example of keeping RI
相反,对于上面的代码,我们修改客户端的实现体为以下,就RI,在调用的时候就不会出现问题了。
图5.1 遵守RI
Part VI Testing an Abstract Data Type
针对ADT中不同类型的方法,有不同形式的测试手段。
- 测试creators、producers和mutators
我们可以通过使用observers来观察对象的运行结果是否符合我们的预期。 - 测试observers
同样的,我们通过mutator和其他两种类型对对象进行操作,在观察我们的对象,来看结果是否正确。
但同时也存在一定的风险:就是如果我们的检测所以依赖的其他方法有错误,可能导致被测试的方法测试结果失去参考价值。
Part VII Invariants
这一部分讲的是不变量相关的内容
我们常说,能够保持他自己的不变量是一个优秀的ADT最重要的属性。而不变量是程序的一个属性,不论在程序的哪个阶段都保持为真。例如,Imuutability就是一个典型的不变量。ADT要自己保护自己的不变量,也就是说保护不变量是ADT的责任,而与client的任何行为都无关。
6.1Why are invariants required?
当我们知道了一个ADT在保护它的不变量之后,我们对代码中错误地方的推断就会省去很多不必要的麻烦。
举一个例子吧。如果我们知道String
是一个不变量的话,当你在调试使用String
的代码的时候,或者当你尝试为另一个ADT创建使用String
的代码时,使用String
而造成的问题就不在考虑范围内了。如果没有这个不变量,那么所有使用String
的地方都需要进行相关的检查,检查它是否被改变了。因此,我们总是要假设client有恶意破坏ADT不变量的行为,我们称之为防御性编程。
6.2Take actions!
怎么样才能保护我们的不变量呢?我们观察下面一段代码:
图6.2.1 代码示例
对于这段代码,第一个可变性威胁就是client可以直接访问到变量所在的区域。这是一个表示泄漏的简单例子。表示泄露是一个很严肃的问题,它一方面影响了不变量,另一方面也影响了表示独立性。它使得代码无法在不影响client的情况下改变其内部表示。
解决方案:
方案很简单啦,就是通过使用修饰符private
和fianl
来限定变量的可见性。
Part VII Rep Invariantand Abstraction Function
8.1Two spaces of values
数据空间分为两类,分别为表示空间和抽象空间。表示空间为表示值构成的空间,放置的实现者看到和使用的值。在一般情况下的ADT的表示比较简单,但有些时候也需要较为复杂的表示。而抽象空间为抽象值构成的空间,放置的是client看到和使用的值。
ADT开发者关注表示空间R,client关注抽象空间A。举一个例子,我们选择使用字符串来代表一组字符,那么表示空间R包含的就是一个一个的字符串,表示空间A包含的就是字符集合。
图8.1.1 RA示例
8.2Mapping between R and A
RA之间使用一种映射关系来进行匹配,这种映射关系有如下的特点:
- 每一个抽象值都匹配于一个表示值,因此一定是满射。
- 每一个抽象值不一定只有一个表示值与其对应,也就是说可能出现多对一的可能。因此未必单射。
- 结合上两条,这种映射关系未必双射。
值得注意的是,A空间中不应该有值被遗漏,如果出现值被遗漏的情况说明方法在设计的过程中有缺陷,导致抽象空间有缺口。
8.4Abstraction Function
抽象函数:
用来表示R和A之间映射关系的函数,即如何将R中的每一个值解释为A中的每一个值。AF:R → A
同样的,如果使用映射的特性来描述的话,AF也同样是满射、未必单射、未必双射的。R中的部分值并非合法的,在A中无映射值。
8.5Rep Invariant: another important ADT invariants
一个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映射过去的集合是这个字符串中所有的字符构成的集合。
8.5What determine AF and RI?
抽象空间自己是不足以决定AF或者RI的,对于同一个ADT,可以有多种表示;不同的内部表示,需要设计不同的AF和RI。选择某种特定的表示方式R,进而指定某个子集是合法的(RI),并为该子集中的每个值做出(AF)解释即如何映射到抽象空间中的值。
总结来讲,同样的表示空间R,可以有不同的RI;即使是同样的R、同样的RI,也可能有不同的AF,即解释不同。
图8.5.1 不同的RI
图8.5.2 不同的AF
8.6Some Example
这里我有点犯懒了,就不打成代码了,而且屁屁踢中的图示也很清晰,这里截图给大伙儿看。
图8.6.1 例一
图8.6.2 例二
8.7CheckRep
图8.7.1 实例方法
我们可以看到在示例中的构造方法的结尾,有一个checkRep(),这个方法的作用就是检查RI是否被破坏了。好的编程习惯是在所有可能改变rep的方法内都要检查,其中Observer方法可以不用,但建议也要检查,以防止万一出现的错误。
Part IX Beneficent mutation
回想一下,一个类型是不可变的,当且仅当该类型的值在创建后永远不会改变。但是随着我们对抽象空间 A 和表示空间 R 的新理解,我们可以细化这个定义,就是抽象值永远不应该改变。但是,只要能够保证它继续映射到相同的抽象值,实现方面就可以自由地改变 rep 值,而且客户端看不到更改。这种变化就称为有益可变性。
下图的示例是之前我们看到过的计算分数的方法。当时的RI跟下面的RI相比比较紧,不包括那些分子分母有公因数的分述;而这个RI相对宽松一些,参与运算时,可以有公约数;显示输出时,需要简化。
也就是说这种mutation只是改变了R值,并未改变A值,对client来说是immutable的。但是!这也不意味着在immutable的类中可以随意出现mutator!
图9.1 示例
Part X Documenting the AF, RI, and Safety from Rep Exposure
这一部分的知识到此就结束了,感谢你能看到这里,说明你真的有好好学习。如果你觉得我写的还行,还请一键三连!
软件构造学习笔记(六)抽象数据类型相关推荐
- 软件构造学习笔记(九)面向复用的软件构造技术
目录链接 Part I What is Software Reuse? Part II How to measure "reusability"? Part III Levels ...
- 软件构造学习笔记-第八周
本周重点是Liskov可替换原则.它要求父类和子类的行为一致性,子类要有更强的不变量.更弱的前置条件.更强的后置条件.在该原则的要求下,每个子类都可以对父类进行替换.这在开发过程中会带来极大的便利,在 ...
- 软件构造学习笔记ATD
在面向对象的编程中,ADT的编写十分重要,与传统的c语言不同,面向对象的编程更加商业化一点,所以保密需要做好,有点商业机密的感觉.如何设计良好的抽象数据结构,通过封装来避免客户端获取数据的内部表示,避 ...
- 哈工大软件构造学习笔记1 Views and Quality Objectives of Software Construction
先要搞清楚软件构造的对象是什么,如何刻画,在关注如何构造. 1,Five key quality objectives of software construction 软件构造的五个关键质量目标 容 ...
- 软件构造学习笔记-第九周、第十周
因为本周五开始五一假期,所以只有一节软件构造课.因为内容还属于创建模式.结构模式.行为模式.将该堂课的内容整合到本博客中.本周的重点是程序开发模式,在写代码之前首先充分考虑采用哪种模式更有利于开发.维 ...
- 哈工大2022春软件构造学习笔记1
课程概述 第一部分:软件构造基础 第二部分:ADT+OOP 第三部分:面向可复用性和可维护性的软件构造 第四部分:面向健壮性与正确性的软件构造 第一章 软件构造的多维度视图和质量目标 软件构造的多维度 ...
- 软件构造学习笔记-第六周
这周的重点是重载和重写.重载要求两方法的签名必须不同,而重写则要求两方法的签名必须相同.重载可以发生在同一个类中,也可以发生在父类和子类中:重写必须发生在父类和子类中.接口/抽象类不具有构造方法,只有 ...
- 软件构造学习笔记-实验3
本次实验要求从五项要求(航班管理.高铁车次管理.操作系统进程管理.大学课表管理.学习日程管理)里完成三项要求,并且尽量实现复用. 面向可复用性和可维护性的设计:PlanningEntry 1.首先设计 ...
- 软件构造学习笔记-第五周
本周讲了AF.RI.Safety from rep exposure.spec等概念.这些是辅助程序设计的重要部分,需要在代码中以注释的形式体现,可以显著提高代码可读性,明确设计的目的.必须要养成写的 ...
最新文章
- HLS-Demo IOS 视屏直播样例
- 做支付遇到的HttpClient大坑(一)
- sql server 2008学习12 事务和锁
- android 通知_Android 全局消息通知框架实现(类似EventBus)
- 算法工程师面试题【集锦cv/ml/dl】
- ZOJ-3704 I am Nexus Master! 模拟
- Matlab--二次多项式曲面拟合
- 《网页设计技巧》系列之一 浅谈文本排版
- springboot配置手动提交_Spring Boot 入门教程 | 图文讲解
- 实施成功的ITIL变更管理的6个步骤
- 电子书阅读器背景颜色修改方法
- 管理员登陆页面php,Dedecms管理员登录输入账号密码点击登陆又回到登陆界面
- TiDB | TiDB在5A级物流企业核心系统的应用与实践
- Xshell和Xftp官方家庭免费版
- Pytorch系列笔记(二)
- sprintf用法(很强大啊)
- 虚拟化IBM HMC
- 笔记本电脑显示rpc服务器不可用,Win7电脑RPC服务器不可用怎么办 RPC服务器不可用解...
- AI 2021 条形码插件
- notepad++使用方法