2021年春季学期
计算学部《软件构造》课程
Lab 3实验报告
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
本次实验覆盖课程第前两次课的内容,目标是编写具有可复用性和可维护性的软件,主要使用以下软件构造技术:
- 子类型、泛型、多态、重写、重载
- 继承、代理、组合
- 常见的OO设计模式
- 语法驱动的编程、正则表达式
- 基于状态的编程
- API设计、API复用
本次实验给定了三个具体应用(值班表管理、操作系统进程调度管理、大学课表管理),学生不是直接针对每个应用分别编程实现,而是通过ADT和泛型等抽象技术,开发一套可复用的ADT及其实现,充分考虑这些应用之间的相似性和差异性,使ADT有更大程度的复用(可复用性)和更容易面向各种变化(可维护性)。
实验环境使用的idea, eclipse luna jdk8
在这里给出你的GitHub Lab3仓库的URL地址(Lab3-学号)。
https://github.com/ComputerScienceHIT/HIT-Lab3-1190201223-txb.
。
请仔细对照实验手册,针对每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
简要介绍三个应用。
- 值班表管理(DutyRoster):一个单位有n个员工,在某个时间段内(例如寒假1月10日---3月6日期间),每天只能安排唯一1个员工在单位值班,且不能出现某天无人值班的情况;每个员工若被安排值班m天(m>1),那么需要安排在连续的m天内。值班表内需要记录员工的名字、职位、手机号码,以便于外界联系值班员。
- 操作系统进程调度管理(ProcessSchedule):考虑计算机上有一个单核CPU,多个进程被操作系统创建出来,它们被调度在CPU上执行,由操作系统来调度决定在各个时段内执行哪个线程。操作系统可挂起某个正在执行的进程,在后续时刻可以恢复执行被挂起的进程。可知:每个时间只能有一个进程在执行,其他进程处于休眠状态;一个进程的执行被分为多个时间段;在特定时刻,CPU可以“闲置”,意即操作系统没有调度执行任何进程;操作系统对进程的调度无规律,可看作是随机调度。
- 大学课表管理(CourseSchedule):看一下你自己的课表,每一上午8:00-10:00和每周三上午8:00-10:00在正心楼13教室上“软件构造”课程。课程需要特定的教室和特定的教师。在本应用中,我们对实际的课表进行简化:针对某个班级,假设其各周的课表都是完全一样的(意即同样的课程安排将以“周”为单位进行周期性的重复,直到学期结束);一门课程每周可以出现1次,也可以安排多次(例如每周一和周三的“软件构造课”)且由同一位教师承担并在同样的教室进行;允许课表中有空白时间段(未安排任何课程);考虑到不同学生的选课情况不同,同一个时间段内可以安排不同的课程(例如周一上午1-2节的计算方法和软件构造);一位教师也可以承担课表中的多门课程;
- 面向可复用性和可维护性的设计:IntervalSet<L>
考虑上节给出的三个应用,其中都包含了具有不同特征的“时间段集合”对象,为了提高软件构造的可复用性和可维护性,可为其设计和构造一套统一的ADT。即IntervalSet<L>描述了一组在时间轴上分布的“时间段”(interval),每个时间段附着一个特定的标签,且标签不重复。
-
-
- IntervalSet<L>的共性操作
-
共性操作是都创建了一个空对象empty,把新的时间段start、end和标签label插入到在当前时间段中,这样就可以获得当前对象的标签集合,之后将某个标签关联时间段从当前对象移除remove, 返回标签开始时间和结束时间即可。
实现方法是单独定义一个类来表示开始时间和结束时间,并返回[开始时间,结束时间]这样的一个范围。在CommonIntervalSet类实现过程中,定义一个protected类型的timeline,创建空对象可直接用remove方法实现。
通过获得当前标签集合可直接获取timeline的键集合,直接返回即可;插入新时间和标签只需在timeline中put即可。
而如果想要返回开始时间和结束时间,或者想要移除某个标签关联的时间段等操作, 都可通过遍历timeline的键实现;
上述截图是代码实现的过程。
为了提高代码的复用性,我选择了方案5来完成局部共性特征的设计,即CRP,通过接口组合实现局部共性特征的复用,通过delegation 机制进行改造。每个维度分别定义自己的接口,针对每个维度的不同特征取值,分别实现针对该维度接口的不同实现类,实现其特殊操作逻辑。进而,通过接口组合,将各种局部共性行为复合在一起,形成满足每个应用要求的特殊接口(包含了该应用内的全部特殊功能),从而该应用子类可直接实现该组合接口。在应用子类内,不是直接实现每个特殊操作,而是通过delegation 到外部每个维度上的各具体实现类的相应特殊操作逻辑。具体的实现方法为:首先定义多个接口:
1.检查多个时间段之间有没有空隙时间,具体的实现方法是用一个排列函数对多个时间段的开始时间start进行从大到小的排序,之后进行空隙时间的检验,如果开始的时间和用函数排序后的第一个开始时间(也就是最早的起始时间)不相等,则说明多个时间段内存在空隙,返回False,同理,如果结束的时间与排序后的最后一个结束时间不相等,说明在多个时间段内也会存在时间空隙的情况,也会返回False,最后如果检测到排序的时间段中有null存在也返回false,只有在排序后每个时间段的结束时间刚好等于下一个时间段的开始时间才说明无时间空隙,返回true;
2.检查一个标签中的几个时间段是否会出现时间重叠的现象:判断所插入的时间段的开始时间和结束时间是否在一个时间段内,如果是则无时间重叠,返回true,否则返回false。
3.增加“非周期”行为。、
如果插入时间段的结束时间大于整体开始时间与持续时间的和或者插入时间段的开始时间小于整体开始时间,则会产生错误,说明该时间段无法加入该interval
最后再定义一个IDutyIntervalSet接口继承上面三个接口。具体实现过程如下方截图所示:
-
-
- 面向各应用的IntervalSet子类型设计(个性化特征的设计方案)
-
DutyIntervalSet<L>:
各个函数及其作用:
DutyIntervalSet(long start, long end) | 声明一个检验没有时间重叠和检验时间空隙的接口 |
Insert(long start, long end, L label) | 排班的函数实现,,通过检查是否有时间重叠来确定是否插入对应的时间标签,若没有重叠则将时间标签插入该时间段,表示为该老师安排好工作 |
checkNoBlank() | 检查是否有时间空隙的函数实现 |
remove(L label) | 移除时间标签的函数实现,若时间表中无该标签,返回false即可 |
start(L label) | 找到对应时间标签的开始时间并返回 |
end(L label) | 找到对应时间标签的结束时间并返回 |
toString() | 返回总的时间中所有时间标签和对应的开始结束时间 |
-
- 面向可复用性和可维护性的设计:MultiIntervalSet<L>
- MultiIntervalSet<L>的共性操作
- 面向可复用性和可维护性的设计:MultiIntervalSet<L>
在MultiIntervalSet中,需要的操作方法有如下图所示:
函数 | 方法 | 参数 |
empty | 返回一个空的时间段,其中泛型可以是employee,pid或者course | 无 |
insert | 向multiInterval集合中插入一条新的multiInterval | start-interval的开始时间点 end-interval的结束时间点 label-interval的标签 |
remove | 在multiInterval已有的集合中移除一个时间段,如果当前对象有这个时间段,返回ture,如果没有返回false | label-所需要移除的那个multiInterval的标签 |
labels | 返回所有时间段的标签返回的是multiInterval集合中存在的multiInterval集合 | 无 |
intervals | 从当前对象中获取与某个标签所关联的所有时间段 | 指定要返回的所有时间段的共有的那个标签 |
convert | 得到multiInterval中所有的interval,并且按照开始的时间进行相应的排序 | 无 |
toString | 打印所有时间标签和对应的所有时间段 | 无 |
对AF,RI以及Safety from rep exposure的论述为:
测试结果如下图:
为了提高代码的复用性,我选择了方案5来完成局部共性特征的设计,即CRP,通过接口组合实现局部共性特征的复用,通过delegation 机制进行改造。每个维度分别定义自己的接口,针对每个维度的不同特征取值,分别实现针对该维度接口的不同实现类,实现其特殊操作逻辑。进而,通过接口组合,将各种局部共性行为复合在一起,形成满足每个应用要求的特殊接口(包含了该应用内的全部特殊功能),从而该应用子类可直接实现该组合接口。在应用子类内,不是直接实现每个特殊操作,而是通过delegation 到外部每个维度上的各具体实现类的相应特殊操作逻辑。
具体的实现方法为:首先定义多个接口:
1.检查多个标签是否出现重叠的状况:
实现方法是构造一个插入函数insert对每个标签进行标签重叠检查。
2.定义一个接口来继承NonOverlapMultiIntervalSet,具体实现如下图:
-
-
- 面向各应用的MultiIntervalSet子类型设计(个性化特征的设计方案)
-
1.ProcessIntervalSet<L>
具体实现代码如下:
函数功能如下表:
函数 | 方法 | 参数 |
ProcessIntervalSet() | 声明一个检验没有时间标签重叠的接口 | 无 |
insert | 声明一个检验没有时间标签重叠的接口 | start-interval的开始时间点 end-interval的结束时间点 label-interval的标签 |
remove | 在multiInterval已有的集合中移除一个时间段,如果当前对象有这个时间段,返回ture,如果没有返回false | label-所需要移除的那个multiInterval的标签 |
Set<L >lables() | 返回总时间表中所有时间段的标签。 | 无 |
intervals | 从当前对象中获取与某个标签所关联的所有时间段 | 指定要返回的所有时间段的共有的那个标签 |
toString | 打印所有时间标签和对应的所有时间段 | 无 |
测试结果如下:
2. CourseIntervalSet<L>
具体实现代码如下:
函数功能如下表:
函数 | 方法 | 参数 |
CourseIntervalSet() | 声明一个检验增加“非周期”的接口 | 无 |
insert | 声明一个检验没有时间标签重叠的接口 | start-interval的开始时间点 end-interval的结束时间点 label-interval的标签 |
remove | 在multiInterval已有的集合中移除一个时间段,如果当前对象有这个时间段,返回ture,如果没有返回false | label-所需要移除的那个multiInterval的标签 |
Set<L >lables() | 返回总时间表中所有时间段的标签。 | 无 |
intervals | 从当前对象中获取与某个标签所关联的所有时间段 | 指定要返回的所有时间段的共有的那个标签 |
toString | 打印所有时间标签和对应的所有时间段 | 无 |
测试结果如下:
这里面L的含义是每个interval所用到的标签,是一个泛型类型,在进行抽象层面的设计时用L进行抽象化,在具体的类设计时将L赋为具体的类型,Employee、Process、Course分别是代表值班人员、进程、课程的标签。interval进行过程中可以依据相应的指定标签进行插入项、移除项、获取项的开始时间与结束时间。并且可以在指定类型下增加相应的个性化信息,例如课程的上课地点、上课的老师、每节课的时长等等。这种泛型的实现就体现了可复用性的设计思想,抽象层面的类可以用来复用,只需要将其中的泛型L进行相应的修改即可。
1.课程的复用设计:
设计的内容包括课程编码、课程名字、上课的老师、上课的教室以及课程在哪周进行。
2.值班人员的复用设计:
设计的内容包括值班教师的姓名、值班老师的职位以及值班老师的电话号码。
3. 进程的复用设计:
具体设计代码如下图:
设计的内容包括进程的ID,名字,最短执行时间以及最长执行时间。
具体计算方法:按照时间轴从早到晚的次序,针对同一个时间段内两个对象里的interval,若它们标注的label等价,则二者相似度为1,否则为0;若同一时间段内只有一个对象有interval或二者都没有,则相似度为0。将各interval的相似度与interval的长度相乘后求和,除以总长度,即得到二者的整体相似度。
具体思路为:先在两个set中找到所需要的最长的时间段作为分母,也就是说需要对两个set进行遍历,分别得到其总的时间段,之后在使用max函数得到最长的长度。在找到总时间段之后,我们还需要找到overlap的时间段,对标签重合的部分有相似度1,那么就需要找到overlap的两个部分中最大的起始时间与最小的结束时间,两者的差值作为重合度,之后再用重合度除以找到的总时间段就是我们需要的两个段的相似度。
时间冲突是指同一个时间段内安排了两个不同的 interval 对象。用发生冲突的时间段总长度除于总长度,得到冲突比例,是一个[0,1]之间的值。
思路:先找到总的时间段长度,但本次只需寻找到一个set的最终时间即可。在找到总的时间长度之后,需要寻找冲突的时间段长度,采用“桶”方法来进行计数,如果有冲突,就进行加1操作,这样就可以在进行测试时将interval与multiInterval分开来进行,从而得到时间冲突比例。
具体代码实现如下:
空闲时间是指某时间段内没有安排任何 interval 对象。用空闲的时间段总长度除于总长度可得到空闲比例,是一个[0,1]之间的值。
思路:和上面两个问题一样,我们要先找到总的时间段长度,但本次只需寻找到一个set的最终时间即可。找到总的时间长度之后,需要寻找冲突的时间段长度,继续采用“桶”方法来进行计数,在空闲的地方就进行加一操作。进行测试时可以按照interval与multiInterval分开来进行,最终的结果。具体代码实现如下:
利用上述设计和实现的ADT,实现手册里要求的各项功能。
在程序初始时我们可以选择直接进入排班系统还是通过输入文件后进入系统,之后系统开始进行相应的排班的开始日期、结束日期的设置,并且需要用户给出值班人员的信息,包括总的值班人数、值班人的姓名、职务和手机号码。
当这些都输入完成后,系统会给用户提供出一个菜单供用户选择,具体菜单如下图:
如果要自动安排员工的话,输入2系统就会自动的将之前给出的起始和终止时间这个时间段填满,并告诉用户已经随机排满并会重新弹出菜单页面让用户选择,用户可以选择查看已分配的排班表,具体过程如下图:
具体代码实现过多,详见文件scr->App->DutyPosterApp.java
在操作系统的进程调度管理系统初始时,我们需要输入进程数,在这里我输入了一个3,表示有三个进程。之后系统会让用户依次输入每个进程的信息,比如ID、名称、最短时间和最长时间。
当这些都输入完成后,系统会显示当前时刻为0,然后提供菜单供用户选择,具体菜单如下:
以此输入菜单上各个选项,就可以得到操作系统的进程调度管理,具体实现如下:
具体代码实现过多,详见文件scr->App-> ProcessScheduleApp.java
课表管理系统首先要输入一个日期,且规定其必须为周一,否则需要重新输入。当输入为周一后,开始课表安排,首先要先输入学期的总周数是多少,也就是学期的时间,(因为18周太多了,输入过于繁琐,于是我以三周为例)输入后会给出菜单让用户进行选择具体页面如下
之后我们需要新建课程,之后将新建的课程进行排表,排表后就可以查看哪些课程没安排、当前每周的空闲时间比例、重复时间比例以及某天的课程了。具体进程如下图:
可以扩展一个功能从一个外部文本文件读入数据并使用正则表达式对其进行解析,也就是语法驱动编程。通过解析可以获得排班、课程和进程的信息。文本读入时要注意按照text中数据的特征进行分割。将“{”符号之前的字符串存在第一个数组中将“{”到“,”之间的字符串存在第二个数组,将“,”到“}”的字符串存在第三个数组中,以此类推来获得信息。
可以通过判断有无间隙时间来判断排班的程序运行是否正确,用一个排列函数对多个时间段的开始时间start进行从大到小的排序,之后进行空隙时间的检验,如果开始的时间和用函数排序后的第一个开始时间(也就是最早的起始时间)不相等,则说明多个时间段内存在空隙,返回False,同理,如果结束的时间与排序后的最后一个结束时间不相等,说明在多个时间段内也会存在时间空隙的情况,也会返回False,最后如果检测到排序的时间段中有null存在也返回false,只有在排序后每个时间段的结束时间刚好等于下一个时间段的开始时间才说明无时间空隙,返回true,说明排班表安排的是合理的。
-
- Git仓库结构
请在完成全部实验要求之后,利用Git log指令或Git图形化客户端或GitHub上项目仓库的Insight页面,给出你的仓库到目前为止的Object Graph,尤其是区分清楚change分支和master分支所指向的位置。
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
每次结束编程时,请向该表格中增加一行。不要事后胡乱填写。
不要嫌烦,该表格可帮助你汇总你在每个任务上付出的时间和精力,发现自己不擅长的任务,后续有意识的弥补。
日期 | 时间段 | 计划任务 | 实际完成情况 |
6.28 | 13:00-23:00 | 理解lab3的实验内容并找到解题思路 | 完成 |
6.29 | 7:30-23:30 | 完成ADT的基本设计 | 未完成 |
6.30 | 14:00-24:00 | 完成ADT的基本设计 | 完成 |
7.1 | 7:30-23:30 | 完成排班管理系统 | 完成 |
7.2 | 7:30-23:30 | 完成操作系统调度系统 | 完成 |
7.3 | 7:30-23:30 | 完成课表系统 | 完成 |
7.4 | 7:30-23:30 | 写报告 | 完成 |
遇到的难点 | 解决途径 |
读实验指导书的时候过于粗心大意,想到思路不验证对错就开始瞎写,然后写到一半发现好像不大对,有点想当然,进行不下去了 | 向室友打听思路并进行对照后发现自己的思路确实不对,于是开始重写 |
时间不够用 | 起早贪黑写,就差在梦里做实验了 |
永远不要把实验压到最后一周做,不然会后悔的
- 重新思考Lab2中的问题:面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?本实验设计的ADT在五个不同的应用场景下使用,你是否体会到复用的好处?
面向ADT编程复用性高,面向应用编程复用性低,而且两者的思路完全不一样。体会到了,很方便,减少了很多重复代码的编写,很省时间,一劳永逸。
- 重新思考Lab2中的问题:为ADT撰写复杂的specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后的编程中坚持这么做?
防止内部变量被外部修改。我愿意在以后的编程中坚持这么做。
- 之前你将别人提供的API用于自己的程序开发中,本次实验你尝试着开发给别人使用的API,是否能够体会到其中的难处和乐趣?
难处体会到了,确实很难,而且不知道怎么解决,最后磕磕绊绊才在dalao的帮助下勉强搞出来,乐趣没体会到(是我太菜了)
- 你之前在使用其他软件时,应该体会过输入各种命令向系统发出指令。本次实验你开发了一个解析器,使用语法和正则表达式去解析输入文件并据此构造对象。你对语法驱动编程有何感受?
语法驱动编程提高了私密性,但是需要耗费一定的时间去解析。
- Lab1和Lab2的大部分工作都不是从0开始,而是基于他人给出的设计方案和初始代码。本次实验是你完全从0开始进行ADT的设计并用OOP实现,经过五周之后,你感觉“设计ADT”的难度主要体现在哪些地方?你是如何克服的?
准确地说是三周!难度主要体现在开始的时候没思路,太抽象了。好不容易有了思路却又实现不了,很难受。硬着头皮写呗,还能咋克服。
- “抽象”是计算机科学的核心概念之一,也是ADT和OOP的精髓所在。本实验的五个应用既不能完全抽象为同一个ADT,也不是完全个性化,如何利用“接口、抽象类、类”三层体系以及接口的组合、类的继承、设计模式等技术完成最大程度的抽象和复用,你有什么经验教训?
在以后面对问题的时候,最好先想出一个可行方案后再开始弄,而不是做到一半发现不可行之后又重新开始,很难受。
- 关于本实验的工作量、难度、deadline。
工作量太大,难度太高,三周时间真的不太够,还好要结课了,这可能是在做实验的时候唯一的精神动力。
- 到目前为止你对《软件构造》课程的评价。
挺好的,要是能在开课前提前教教我们Java就更好了。。。