软件构造学习笔记(五)

第六讲 抽象数据类型(ADT)
本讲主要介绍了抽象数据类型与表示独立性:如何设计良好的抽象数据结构,通过封装来避免客户端获取数据的内部表示(表示泄露),避免潜在的bug。

1 抽象类型
数据抽象是由一组操作所刻画的数据类型,

传统类型定义更关注数据的具体表示,并非操作,而抽象类型强调数据上的操作,无需关心数据的存储,是需要完成设计、使用操作即可。

2 类型和操作的分类
类型可以分为可变类型(提供了可改变内部数据值的操作)和不可变类型(操作不可改变内部值,而是构造新对象)。

操作可以分为构造器(Creator)、生产器(Producer)、观察器(Observer)、变值器(Mutator)。具体的划分规则如下图所示。

 

1.构造器Creators,创造该类型新的对象,可能实现为构造函数或静态函数;
2.生产器Producer,从该类型旧对象中创建新对象,如String的concat()方法;
3.观察器Observers,获取抽象类型对象并返回不同类型的对象,如List类的size()方法;
4.变值器Mutators,改变对象属性的方法,如List类的add()方法。变值器通常返回void;

工厂方法:构造器实现成为了静态方法。

3 设计一个抽象类

良好ADT的设计靠“经验法则”,提供一组操作,设计其行为规约spec。
1.设计简洁、一致的操作。最好使用简单的操作实现,而非复杂的操作;每个操作都应该有明确的目的,且有连贯的行为,而不是一大堆特殊情况,操作行为应该是内聚的。
2.足以支持client对数据所做的所有操作需要,操作满足client需要的难度要低。对象的每个需要被访问的属性应该都能被访问到;基本信息应该易于获取。
3.要么抽象,要么具体,不能混合。面向具体应用的类型不应该包含通用方法,而面向通用的类型不应包含面向具体应用的方法。

4 表示独立性
表示独立性:客户端使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部规约和客户端。

通过前置条件和后置条件充分刻画了ADT的操作,规约规定了客户端和实现者之间的契约,客户端由此可知可以依赖哪些内容,实现者可知可以安全更改的内容。

5 测试ADT
产生器、制造器、变值器的测试:使用观察器观察这些操作的结果是否满足规约。

观察器的测试:调用产生器、制造器、变值器等方法产生或改变对象,看结果是否正确。

风险:如果被依赖的其他方法有错误,可能导致被测试方法的测试结果失效。

具体方法是划分等价类。

6 不变量,RI和AF
不变量:程序在任何时候总是真的性质。维持不变量可以更容易发现程序中的错误。

ADT应始终保持其不变量,不变量应由ADT来负责,与客户端的任何行为无关。

表示空间R:实现者看到和使用的值(表示值)构成的空间。

抽象空间A:客户端看到和使用的值(抽象值)构成的空间。

表示不变量RI:R到boolean的一个映射。RI(r)为真,当且仅当r是合法的。

抽象函数AF:R和A之间的一个对应关系,如何将R中合法的值解释成为A中的值。

 

R中非合法的值在A中无对应值。

应当紧接着表示值精确地注释RI和AF。还应给出表示泄露的安全声明。

AF是满射,不是单射(R中部分值可能不合法,缺少A中对应的映射值),因此不一定是双射。
RI是values值到booleans的映射,对于每一个值r,RI是true表示r可以被AF映射,换言之RI说明哪些值是合法的,它是所有表示值的一个子集,包含了所有合法的表示值。

同一个ADT可以有多种表示。不同的内部表示需要设计不同的AF和RI.同样的R,可以有不同的RI.即使是同样的R、RI,也可能有不同的AF,即“解释不同”。

在所有可能改变表示值的方法内都要检查RI是否满足。一般通过调用checkRep()(专门检查RI是否满足的方法)来实现。

这里区分一下规约,规约里只能使用client的内容撰写,如参数、返回值、异常等,值只能为A空间的值。

ADT不变量可以取代复杂的前置条件(相当于把其封装到ADT内部)。
所需条件可在一个位置强制执行,而Java静态检查发挥作用,如果值不符合条件则编译时报错。
这更容易理解,传达了程序要需要知道的内容;这更容易改变,无需更改client。

 

第七讲 面向对象的编程(OOP)

主要内容为OOP的基本概念、Java中的一些重要的对象方法、设计好的类。

1 对象,类,方法,接口
对象是一些状态和行为的组合。

而在Java中,状态对应为field,而行为对应method。

任何对象都以一个类,类定义了方法和字段(统称为成员)。类定义了类型和实现。

粗略来说,类的方法是它的API.

类成员变量为与类本身相关的变量(而不是它的实例)。相应的还有类方法。

除此以外的变量和方法称为实例方法、实例成员变量。

接口中只有方法的定义,没有实现。接口之间可以继承与扩展。一个类可以实现多个接口。

接口用来确定ADT规约,而类是通过接口来实现ADT。

类也可以不需要接口直接作为ADT,既有ADT定义又有ADT实现。

接口中用default修饰的方法,在子类中无需重复实现。接口中default修饰的方法可以实现接口中某些统一的功能,在各个类中重复实现。default以增量式的为接口额外增加的功能而不破坏已实现的类。

2 封装与信息隐藏
好的模块应尽可能地隐藏内部数据和不同于其他模块的实现细节。

使用接口可以进行信息隐藏,通过成员修饰语,给client提供他们需要的方法,其他成员以private修饰,不进行提供,这样客户端代码无法直接访问内部属性(但仍可访问其他的非接口成员)。

使用修饰符来进行信息隐藏。

 

3 继承和重写
严格继承:子类只能添加新方法,无法重写超类中的方法。添加final修饰符来实现该操作。

重写的方法应保持函数签名相同。实际执行时调用哪个方法,运行时决定。

抽象方法只有定义没有实现,而抽象类只需要存在一个方法只有定义没有实现,但是抽象类不能够实例化(无法new生成对象),需要将未实现的方法都实现才能new。

其中,Interface是只有抽象方法的抽象类。

4 多态、子类型、重载
多态:

1.特殊多态:一个方法可以有多个同名的实现(重载)
2.参数化多态:一个类型名字可以代表多个类型(泛型编程)
3.子类型多态:一个变量名字可以代表多个类的实例(子类型)

重载:多个方法具有同样的名字,但有不同的参数列表或返回值类型。可以方便客户端调用。

具体使用哪个实现,是在编译阶段决定的。

重载的规则:
1.必须有不同的参数列表;
2.可以有不同的返回值类型;
3.可以有不同的访问权限;
4.可以有不同的异常;
5.一个方法可以在同一个类内重载,也可以在子类中重载。

泛型编程是一种编程风格,其中数据类型和函数是根据待指定的类型编写的,随后在需要时根据参数提供的特定类型进行实例化。泛型编程围绕“从具体进行抽象”的思想,将采用不同数据表示的算法进行抽象,得到泛型化的算法,可以得到复用性、通用性更强的软件。

使用泛型变量的三种形式:泛型类、泛型接口和泛型方法。

泛型信息只存在于编译阶段,运行时会被“擦除”。

子类型:B是A的子类型,当且仅当B的规约不弱于A的规约。

子类型多态:不同类型的对象可以统一的处理而无需区分,从而隔离了“变化”。

5 一些Java对象方法
Object是所有Java的基类,有三个主要的方法,即equals()、hashCode()和toString()。
equals()方法不重写默认比较内存地址是否相同;toString()方法不重写默认显示内存地址。

设计不可变的类时,不应提供任何变值器,确保任何方法都不能被重写(使用private和final),避免表示泄露,实现toString(),hashCode(),clone(),equals()等方法。

一般来说尽可能要使用不可变的类,尤其是规模比较小的类。

对于规模比较大的类(例如BankAccount, Collection),可以考虑用可变类实现,但要最小化其可变性。构造方法应尽可能完全地初始化实例,避免使用再初始化方法。

posted @   TongL_Roy  阅读(43)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示