。
2021年春季学期
计算学部《软件构造》课程
Lab 3实验报告
姓名 | 李浩 |
学号 | 1190202415 |
班号 | 1903005 |
电子邮件 | 3122253137@qq.com |
手机号码 | 18246121682 |
3.2 面向可复用性和可维护性的设计:IntervalSet<L>·· 1
3.2.1 IntervalSet<L>的共性操作···· 1
3.2.3 面向各应用的IntervalSet子类型设计(个性化特征的设计方案)···· 2
3.3 面向可复用性和可维护性的设计:MultiIntervalSet<L>·· 2
3.3.1 MultiIntervalSet<L>的共性操作···· 2
本次实验覆盖课程第 2、3 章的内容,目标是编写具有可复用性和可维护性
的软件,主要使用以下软件构造技术:
子类型、泛型、多态、重写、重载
继承、代理、组合
语法驱动的编程、正则表达式
API 设计、API 复用
本次实验给定了三个具体应用(值班表管理、操作系统进程调度管理、大学
课表管理),学生不是直接针对每个应用分别编程实现,而是通过 ADT 和泛型等抽象技术,开发一套可复用的 ADT 及其实现,充分考虑这些应用之间的相似性和差异性,使 ADT 有更大程度的复用(可复用性)和更容易面向各种变化(可维护性)。
实验环境配置
Eclipse和Git在实验一与实验二中已经配置过。本次实验无需配置额外的实验环境。
在这里给出你的GitHub Lab3仓库的URL地址(Lab3-学号)。
https://github.com/ComputerScienceHIT/HIT-LAB3-1190202415
请仔细对照实验手册,针对每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
简要介绍三个应用。
值班表管理,一个单位有 n 个员工,在某个时间段内安排值班。每天只能安排一个员工且不能出现无人值班的情况;每个员工需要安排在连续的几天内。值班表内需要记录员工的名字、职位、手机号码,以便于外界联系值班员。
操作系统进程调度管理,进程被调度在 CPU 上执行,操作系统决定在各个时段内执行哪个进程。操作系统可挂起某个正在执行的进程,在后续时刻可以恢复执行被挂起的进程。每个时间只能有一个进程在执行,其他进程处于休眠状态;一个进程的执行被分为多个时间段;在特定时刻,CPU 可以“闲置”;调度无规律,可看作是随机调度。
大学课表管理:课程需要特定的教室和特定的教师。假设各周的课表都是完全一样的,同样的课程安排将以“周”为单位进行周期性的重复,直到学期结束;一门课程每周可以出现 1 次,也可以安排多次,且由同一位教师承担并在同样的教室进行;允许课表中有空白时间段;同一个时间段内可以安排不同的课程;一位教师也可以承担课表中的多门课程。
三者的共性:
三者中的时间段都可以被抽象为开始,结束,标签(可以是多种类型)的抽象数据结构,因此可以设计相应的ADT,并在子类中分别对不同的应用进行具体操作。
三者的差异:
- 是否允许相同的label对应不同的时间段。排班表中,唯一的label只可以被绑定到唯一一个时间段里,而像操作系统进程调度和课程表管理这种复杂应用,同一个标签对象也可以被绑定到多个时间段上。
- 是否允许时间轴上有空白。在排班表中,不允许有空白;在操作系统进程调度和课程表管理中,允许有空白排班表和操作系统不允许不同的interval出现重叠。而课程表可以出现时间段重叠,但是教师和教室不能是同一个
- 是否包含周期性的时间段。排班表和操作系统没有周期性的时间段,而课程表可以有周期性的时间段。
-
- 面向可复用性和可维护性的设计:IntervalSet<L>
该节是本实验的核心部分。
-
-
- IntervalSet<L>的共性操作
-
这个ADT描述了一组在时间轴上分布的“时间段”,每个时间段对应有着一个特定的标签,同时标签不可以重复。所以共性操作包括:
empty():创建一个空对象。
void insert(long start, long end, L label):在当前对象中插入新的时间段和标签。
Set<L> labels():获得当前对象中的标签集合。
boolean remove(L label):从当前对象中移除某个标签所关联的时间段。
long start (L label):返回某个标签对应的时间段的开始时间。
long end (L label):返回某个标签对应的时间段的结束时间。
如图所示:
接下来在CommonIntervalSet<L>类中实现2.2.2 局部共性特征的设计方案。
首先利用一个Map来建立标签与时间的联系,同时时间用一个long型数组表示
这里对于insert方法,直接对long型数组的第一与第二个元素操作即可
获取label集合:
重写的toString方法,用于返回对应的IntervalSet的输出结果的输出格式
还有一些上述提到的共性方法,实现都很简单,这里就不全部列举了。
-
-
- 面向各应用的IntervalSet子类型设计(个性化特征的设计方案)
-
这里只有DutyIntervalSet继承了CommonIntervalSet实现类,同时传入的泛型参数为Employee。在这里,DutyIntervalSet作为排版表的一种数据结构,在后续的app设计中作为成员变量使用。这里代表的是整个值班表。
这里维护了一个时间段的集合:
接下来重写toString方法是为了让此数据结构以合适的形式进行输出:
这里有几个注意点:首先,此处是一个标签对应着一个时间段,所以比较便利。
其次,还需要对label列表对应的时间段进行排序在输出,需要重写一下compare方法,
-
- 面向可复用性和可维护性的设计:MultiIntervalSet<L>
- MultiIntervalSet<L>的共性操作
- 面向可复用性和可维护性的设计:MultiIntervalSet<L>
这是一个接口描述了一组在时间轴上分布的“时间段”(interval),每个时间段附着一个特定的标签,且标签不重复。共性操作包括:
empty():创建一个空对象。
void insert(long start, long end, L label):在当前对象中插入新的时间段和标签。
Set<L> labels():获得当前对象中的标签集合。
boolean remove(L label):从当前对象中移除某个标签所关联的时间段。
IntervalSet<Integer> intervals(L label); 从当前对象中获取与某个标签所关联的所有时间段
接下来在CommonIntervalSet<L>类中实现2.2.2 局部共性特征的设计方案。
首先利用一个Map来建立标签与时间的联系,同时时间用一个long型数组表示,由于这里不止一个时间对,所以用List来存储数组
这里的insert方法比较特殊:在插入前需查看此label是否已经存在,若已经存在,则需要新建一个long型数组存储新加入的时间段。
这里的remove方法需要删除与当前label相关的所有时间段:
Intervals方法:从当前对象中获取与某个标签所关联的所有时间段
这里获取到很容易,难点在于需要对获取到的时间段进行排序使其按照从小到大的数据进行 排序,而且这里需要重写compare方法:
-
-
- 面向各应用的MultiIntervalSet子类型设计(个性化特征的设计方案)
-
这里有两个类设计继承了CommonMultiIntervalSet实现类,首先是ProcessIntervalSet,这里作为后面app设计中的一种数据结构出现,用于存放进程执行表的一种数据结构,因此,在该数据结构中,我们一样,重新进行toString方法的重写来实现,这里同样是进行循环,对列表中的每一个label,都需要调用intervals方法来获得其全部的时间段,接下来通过适当的形式进行输出。
如图所示:
接下来是CourseIntervalSet,也作为后面app设计中的一种数据结构出现,用于存放课程表的一种数据结构,我们一样,重新进行toString方法的重写来实现,这里同样是进行循环,对列表中的每一个label,都需要调用intervals方法来获得其全部的时间段,接下来通过适当的形式进行输出。由于course中属性众多,且都需要体现给使用者,所以这里需要对所有属性都进行输出,所以输出的格式会稍微复杂一些,
首先是employee,包含三个属性:
相应的get方法,以及重写的equals方法,重写的hashCode(),重写的toString()
2.process
包含四个属性:如图:
相应的get方法,以及重写的equals方法,重写的comapreTo(),重写的toString()
最后是course:
包含六个属性:如图:
相应的get方法,以及重写的equals方法,重写的hashCode(),重写的toString()
如图
-
- 可复用API设计
在这一部分中,我讲这三部分需求分别写到三个java类中,每一个java类中完成一种需求。在designAPI包下的。
在类similarityAPI中:
在这里我们的需求是计算两个MultiIntervalSet对象的相似度,本方法会返回两个MultiIntervalSet间的相似度,即看label等价所对应的长度在时间总长度上的占比。
* 按照时间轴从早到晚的次序,针对同一个时间段内两个对象
* 里的 interval,若它们标注的 label 等价,则二者相似度为 1,否则为 0;
* 若同一时间段内只有一个对象有 interval 或二者都没有,则相似度为 0。将各
* interval 的相似度与 interval 的长度相乘后求和,除以总长度,即得到二者
* 的整体相似度
主要是通过在遍历过程中逐步更新,获得时间轴的总长度以及重叠的长度,并进行求解。
循环过程:‘
在类conflictAPI,求解MultiIntervalSet<L>对象中的时间冲突比例,所谓的“冲突”,是指同一个时间段内安排了两个不同的interval对象。用发生冲突的时间段总长度除于总长度,得到冲突比例,是一个[0,1]之间的值。我们在这里记录两个变量,并通过label列表的循环,逐个比较是否发生冲突同时记下冲突的长度,并在循环过程中记下时间刻度的范围,最后二者相除来求解。
如图:
通过循环累加,即可得到空闲时间比例
利用上述设计和实现的ADT,实现手册里要求的各项功能。
首先是包含如下属性:员工列表,之前设计好的DutyIntervalSet作为排班系统,以及起始时间。
如图首先对数组成员进行初始化:
添加工作成员:
这里是随机安排的方法的部分方法体
:
最后是值班信息与员工信息的输出,直接循环即可
功能列表:
这里使用processIntervalSet作为数据结构,同时维护一个的Map存储进程和已执行时间的映射关系,同时进入app会有如下菜单功能可供选择:
其中,操作选项1,2涉及对Map的增删;5,6对应processIntervalSet的intervals操作。
对于操作3,对所有的已存在的进程进行遍历,且执行的时间为最短到最长之间的时间:
操作四与之不同之处即为每次都是用最短的是行时间来实现最短进程有限调度。
使用CourseIntervalSet作为数据结构,同时是用一个List来维护一个课程列表,同时进入app会有如下菜单功能可供选择:
其中,操作1,2包含的是对list列表的操作,操作三是对数据结构CourseIntervalSet的操作,利用其中的insert即可,4.对应的是CourseIntervalSet中的调用course中的一个属性,是否被安排的属性,在这里进行 遍历课程列表并输出尚未被安排的,5是对当前课程列表中所有的课程进行输出,包括已安排的,未安排的。6是查看空闲比冲突比,这里就是调用上一届设计好的API来直接使用即可。
同时,APP拥有很好的健壮性,能面对用户各种非法的、不符合格式的输入
分别以employee,roster为数据类型,存入list数组中,如图为定义的两个内部类:
由于传入文件的特殊性,我们需要一个方法对其进行解析:
接下来就是一行一行读取文件,同时维护一个Map在读取文件时一边进行解析,一边实现读取,一边进行相应维度的识别与增加
如图为八个文件的输出:
。
因为现在可以出现一个员工被安排多段值班的情况,例如张三的值班日期为(2021-01-01, 2021-01-10), (2021-02-01, 2021-02-06);之前的情况都是一个员工只可以值班一段时间就可以了,在之前的方法里我们用的是只满足一对一的所以在之前的方法里,我们只需要将DutyIntervalSet继承的类修改为CommonMultiIntervalSet,同时时间段的存储也要换成MultiIntervalSet。并且在相应的操作方法中,对于contains的使用,由已包含此员工就抛出异常,改为继续添加排班信息。
-
-
- 变化2
-
这里我实现的方案是首先直接添加,接下来调用设计的计算冲突比的函数计算添加以后的冲突比,若冲突比不为0,则说明有课程之间引起了冲突,那就删除刚刚添加过的这门课程。
-
- Git仓库结构
请在完成全部实验要求之后,利用Git log指令或Git图形化客户端或GitHub上项目仓库的Insight页面,给出你的仓库到目前为止的Object Graph,尤其是区分清楚change分支和master分支所指向的位置。
我也不知道为什么这样不清晰,在这里列出进行分分支转换的全部命令
git checkout -b change
git add *
git commit -m "change
git checkout master
最终Git仓库到目前为止的Object Graph有如下形式
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
每次结束编程时,请向该表格中增加一行。不要事后胡乱填写。
不要嫌烦,该表格可帮助你汇总你在每个任务上付出的时间和精力,发现自己不擅长的任务,后续有意识的弥补。
日期 | 时间段 | 计划任务 | 实际完成情况 |
6.28 | 17:00-22:00 | IntervalSer相关类的设计 | 完成 |
6.29 | 17:00-22:00 | 泛型L的三种类型的设计以及异常设计 | 完成 |
6.30 | 17:00-22:00 | 三种ADT子类型的设计 | 完成 |
7.1 | 17:00-22:00 | 自定义API的设计,日期类操作的设计 | 完成 |
7.2 | 17:00-22:00 | APP设计 | 未完成 |
7.3 | 17:00-22:00 | 继续App设计,写报告 | 完成 |
7.4 | 17:00-22:00 | 完成报告,提交 | 完成 |
遇到的难点 | 解决途径 |
编写APP时遇到很多的健壮性问题。 | 仔细分析每个步骤中用户可能的所有输入,针对任何非法情况都作出提示。虽然很耗费时间,但效果很好。 |
对于选择设计方案模式上存在一些疑问和不足,刚开始的选的方案后来实现起来十分复杂 | 最后也没能特别完美的解决问题 |
与好多期末考试交织在一起,时间很紧张,压力很大。 | 自我调节。 |
- 重新思考Lab2中的问题:面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?本实验设计的ADT在三个不同的应用场景下使用,你是否体会到复用的好处?
设计ADT时一定要考虑得全面且清楚,在最终决定实现方案后再编写代码,否则在后面的应用实现中发现问题时只能再重新设计。同时,在编写APP时,一定要事先考虑健壮性的问题,不然,在编写完代码之后进行测试时会遇到很多的健壮性问题,这时再去修改就会使得代码很臃肿,可读性变差,出错的可能性更高。
- 重新思考Lab2中的问题:为ADT撰写复杂的specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后的编程中坚持这么做?
有必要,可以防止自己在编程的时候出现错误,我愿意。
- 之前你将别人提供的API用于自己的程序开发中,本次实验你尝试着开发给别人使用的API,是否能够体会到其中的难处和乐趣?
能体会到难处,很困难,单页觉得如果真的有人用自己设计的API,一定很有成就感。
- 你之前在使用其他软件时,应该体会过输入各种命令向系统发出指令。本次实验你开发了一个解析器,使用语法和正则表达式去解析输入文件并据此构造对象。你对语法驱动编程有何感受?
设计十分麻烦。但是一但设计完成读入数据十分方便。
- Lab1和Lab2的大部分工作都不是从0开始,而是基于他人给出的设计方案和初始代码。本次实验是你完全从0开始进行ADT的设计并用OOP实现,经过五周之后,你感觉“设计ADT”的难度主要体现在哪些地方?你是如何克服的?
ADT的难度主要体现在抽象上。一个好的ADT既不能过于具体,也不能过于抽象。需要从大量应用场景中寻找共性,抽象的程度也很难把握。对于这种情况,只能反复的推敲,比较不同设计方案的差异并选择最好的ADT设计方案。
- “抽象”是计算机科学的核心概念之一,也是ADT和OOP的精髓所在。本实验的五个应用既不能完全抽象为同一个ADT,也不是完全个性化,如何利用“接口、抽象类、类”三层体系以及接口的组合、类的继承、设计模式等技术完成最大程度的抽象和复用,你有什么经验教训?
接口、抽象类、类的抽象程度一定是逐渐降低的,将所有应用的共性抽象为接口,然后在抽象类以及类中添加新的特性。通过接口的组合可以形成新的接口,并可以具备不同接口中的抽象。类的继承也可以增加更具体的新的特性。
- 关于本实验的工作量、难度、deadline。
工作量很大,难度不低,deadline和很多考试过于靠近,压力很大
- 到目前为止你对《软件构造》课程的评价。
逐渐理解了软件构造过程,也逐渐适应了与之前完全不同的编程过程。通过Lab3大量的代码训练,自己的编程水平也有了极大的提高。也感受到了deadline的磨人之处。