软件构造回顾(一)

在过去的几天复习软件构造的这一门课的时候,我发现了许多在第一次学习的过程中没有注意到的地方,并且,由于是复习,所以之前老师在课堂教学中的教学思路我能够厘清,因此,我准备按照某种思路对过去学过的知识做出一个整理。

三、Abstract Data Type (ADT) and Object-Oriented Programming (OOP)

首先,因为最初在这一章的学习过程中我仅仅对于ADT有一些模糊的认识,而对于OOP是完全不能理解的,但是,经过几轮实验的洗礼,我逐渐摸清其中的知识:这一章的大致内容就是教我们如何设计出一个合格的ADT并且能够理解面向对象编程的思想。

3.1 Data Type and Type Checking数据类型与类型检验

如何设计一个ADT, 当然要从最基本的数据开始说起,数据是构成ADT很关键的部分,由于我们的编程语言采用的是Java,所以一切规则都要按照Java来制定。

Data type in programming languages数据类型

数据类型浅显的说就是指一组特定的值和以及其相应的操作,比如boolean类型、int类型等,那么在Java中数据类型大致分为两种:

基本数据类型对象数据类型
int, long, byte, short, char,float, double, booleanClasses, interfaces, arrays, enums,annotations
不可变有的不可变有的可变
在栈中分配内存在堆中分配内存
无法做到表达的统一可以做到表达的统一
用的时候花费的代价低用的时候花费的代价高

对于某些基础数据类型他们是可以进行自动转换的,并且对于它们也有特定的操作方法,比如int+int;

Static vs. dynamic data type checking

理解类数据类型之后,就要进行他们之间的区分了:
所以区分的过程分为两种:
静态类型检查和动态类型检查。
静态类型检查:顾名思义,在程序还没有运行的时候就可以静态检查数据类型是否正确,它包括检查:语法错误,类名/函数名错误,参数数目错误,参数类型错误,返回值类型错误等。观察这些错误可以看出静态类型检查的特点:它注重的是“类型”这个问题。
动态类型检查:这可以对照着静态的进行理解,它在从程序运行之后发生,并且他关注的是:非法的参数值,非法的返回值,越界,空指针等问题。它的特点:关注点在于类型中的值是否合法。
一个明显的例子:

int sum = 0;
int n = 0;
int average = sum/n;

这是个除0的问题,其中的各个类型对应的值本身没有违法,但是操作违法,所以属于动态类型检查的范围。

Mutability and Immutability

在Java中,数据对象还分为可变对象和不可变对象,这大致可以在他们的存储方式上进行区分:不可变:改变一个变量,将该变量指向另一个值的存储空间;可变:改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值。
所以不可变的数据可以理解为:一旦被创建,其值不能改变。而可变的数据:拥有方法可以修改自己的值。

那么对于一些需要不可变的性质的地方该如何处理呢?
答案是用final。

在被final修饰之后,变量就变得“不可变”了:
final修饰的类无法派生子类
final变量无法改变值/引用
final方法无法被子类重写

Complex data types: Arrays and Collections

在认识了给种类型的分法之后,我们就可以认识一下在Java中几个特别的类型:Array,List,Set,Map等。
对于这些类型,它们都是可变的数据类型。
并且,他们也有这各自的一些改变自身值的方法。
但是在某些情况下,我们需要用到他们的不可变的形态,那么该如何做到呢?
这时我们需要一个包装纸,对他们进行包装,例如List,有:

public static <T> List<T> unmodifiableList(List<? extends T> list);

这一方法,它将List进行包装,之后就返回一个只能读不能改的List了。其他的类型均有这样的方法可以进行包装。

这样对于各种数据类型的介绍就完成了。

3.2Designing Specification设计规约

在完成了数据类型的介绍之后,我们就可以用一些函数(或称方法)来对这些数据进行处理了。那么紧接着我们就要面对一个问题:怎么能让使用者明白你写的这个方法使用来干什么的呢?
这就需要我们用spec来和客户达成一致。

什么是spec?

可以这么理解:spec是开发者和使用者之间的一份契约,它规定了方法的使用方式,就像防火墙一样,它告诉使用者:这个方法该怎么用,它的作用是什么;它又告诉开发者:这个方法该做什么,应该输出什么样的结果。使用者不知道方法的具体实现方式,开发者也不知道使用者会怎么用,他只知道按照约定的方式用可以输出对应合法的结果。
所以spec是双向的,那么它也是由两要素组成:输入和输出的数据。

前置条件和后置条件

输入可以理解为前置条件:它对应着客户端,约束着客户端。
输出可以理解为前置条件:它对应着开发者,约束着开发者。
当前置条件满足,后置条件必须满足;若前置条件不满足,后置条件不一定满足(随意,但尽量委婉一点提示为好。)

spec的特性

spec的写法:

/**
*@param这对应着前置条件
*@return这对应着后置条件
*@throws这对应着后置条件

spec的强弱:
spec变强的条件:前置条件要求变低+后置条件要求变高
强弱的体现是在开发者和使用者之间进行的博弈。

如何设计一个好的spec呢?
思路:让使用者和开发者都满意。
内聚的:Spec描述的功能应单一、简单、易理解
信息丰富的:不能让客户端产生理解的歧义
足够强:太弱的spec,client不放心、不敢用 。
开发者应尽可能考虑各种特殊情况,在post-condition给出处理措施。
足够弱:太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难
度。
使用抽象类型:可以给方法的实现体与客户端更大的自由度
安全:必要时检测前置条件是否满足

这样之后,就完成了spec的相关介绍。之后就可以按照spec对方法进行设计了。

3.3 Abstract Data Type (ADT)抽象数据类型 (ADT)

在完成方法之后,我们就应该考虑,会有什么东西可以将这些方法进行汇总,使得这个东西具有这些方法带来的功能呢?
这时就引入了ADT这个概念。
这个概念关注的是操作的用法和作用,而并不关注数据如何储存,这就是抽象的含义。

4种ADT的操作

将ADT含有的各种操作方法进行分类,可以分为四类:
creator:构造器:构造函数或者使用静态方法构造出一个对象,从0到1。
producer:生产器:有一个对象生成另一个对象,从A到B。
observer:观察器:顾名思义,查看这个对象的某些特性
mutator:变值器:改变对象的默写属性,一般为返回值为void。

设计ADT的规约

1.设计简洁、一致的操作
2.要足以支持client对数据所做的所有操作需要,且用操作满足client需要的难度要低
3.要么抽象、要么具体,不要混合 — 要么针对抽象设计,要么针对具体应用的设计

表示独立性

client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端。换句话说,用户只需要知道这个ADT是干什么的,并且知道这个ADT需要哪些数据就可以了,他不需要知道这个ADT的功能是怎么实现的。

AF和RI

Invariants
保持不变量:在ADT工作的时候,有些特性是不能够改变的,这也就是不变的。
但是在某些情况下,ADT会将自己的某些属性不经意之间暴露出去,最好的方法就是使用不可变数据类型,但是,有些时候必须使用可变的数据类型,那么这个时候就需要防御式编程了。

AF和RI
在这里插入图片描述
R :表示空间,程序内部的值。
A :抽象空间,客户端看到的值和使用的值。
中间的映射是满射关系。

表示不变性RI:
某个具体的“表示”是否是“合法的”。
或者所有表示值的一个子集,包含了所有合法的表示值。
或者一个条件,描述了什么是“合法”的表示值。

抽象函数AF:
R和A之间映射关系的函数,即如何去解释R中的每一个值为A中的每一个值。

对于RI,在使用方法的时候应该对其进行检验,查看是否满足条件,这时我们就引入了checkrep()方法,用断言的方式进行检查。

描述AF、RI、Safety from Rep Exposure

在设计ADT时,在代码中用注释形式记录AF和RI和Safety from Rep Exposure,要做到精确,不放过任何一个说明。

并且在测试ADT的时候,也要根据那些注释来设计测试用例,用observer测试creator、producer、mutator,用另三种测试observer。

3.4 Object-Oriented Programming (OOP)面向对象的编程

在完成ADT的设计后,我们就可以开始真正的面向对象编程了,首先,我们要分清对象的含义。

对象

对象指的是什么?
我的理解是对象就是一个实例,它拥有一些属性和方法。他可以用一个类来实现,也可以用接口再用类来创造。

接口、抽象类、具体类

接口:用来定义ADT
类:用来实现ADT
接口之间可以继承与扩展
一个类可以实现多个接口(从而具备了多个接口中的方法)
一个接口可以有多种实现类,例如:ArrayList、LinkList实现List。
类也可以直接定义并实现ADT(不推荐)
类中可以有静态方法和default修饰的方法。
抽象类:
至少有一种抽象方法,abstract method
也可以有一些共性方法。
具体类:
实现所有父类中没有实现的方法。

为什么要这么分层次呢?
这是为了封装和隐藏,不将具体的实现暴露给客户端,客户端自然而然无法破坏程序。

Inheritance、Overriding and Overloading

继承关系存在于类和类之间,用extend来表示。
当子类和父类中对于某个方法存在不同的需求的时候,就需要用到重写Overriding:
重写的函数:完全同样的signature,在运行的时候才能够决定用哪个方法,即为动态检查。
父类型中的被重写函数体不为空
对其大多数子类型来说,该方法是可以被直接复用的。
但对某些子类型来说,有特殊性,故重写父类型中的函数,实现自己的特殊要求。
如果父类型中的某个函数实现体为空
其所有子类型都需要这个功能,但各有差异,没有共性,在每个子类中均需要重写

重写之后,利用super()复用了父类型中函数的功能,然后可以对其进行扩展,所以善用super可以减小工作量。

重载:
这个概念在最开始学的时候,非常容易和重写弄混,因为他们都是修改方法,但是掌握之后就能够很好的分出二者的区别:
在这里插入图片描述
重载是多个方法有着相同的名字,但是他们的参数列表或者返回值类型有所不同,而重写是参数相同,返回值也相同,内部实现有所不同。并且,重载的类型判断是基于传递的参数的引用类型:
在这里插入图片描述
参数化多态:使用泛型变成。
子类型多态:不同类型的对象可以统一的处理而无需区分

3.5 Equality in ADT and OOP ADT和OOP中的“等价性”

完成了ADT的设计和面向对象编程后,我们就要考虑一下等价性问题:

等价性

不可变的类型:
引用等价性:==,对应的内存地址,基本数据类型

对象等价性:.equal(),内存地址指向的内容,对象数据类型。
一般情况下,在自定义的ADT中,我们都要override .equal()方法。这个时候需要引入一个instanceof进行类型的判断,在判断内容,这样就可以很好的避免类型不同的情况。
二者还是很好区分的。可以用int和string进行理解。

可变的类型:
情况简单得多,也分为两种:
观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致
行为等价性:调用对象的任何方法都展示出一致的结果

对可变类型来说,往往倾向于实现严格的观察等价性;在有些时候,观察等价性可能导致bug,甚至可能破坏RI,所以追求行为等价性即可,并且由于Object的equal()、hashcode()已经实现行为等价性了,所以对可变类型来说,无需重写这两个函数,直接继承Object的两个方法即可。

综上,就是软件构造学科中第三章的思路。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值