3.2 面向可复用性和可维护性的设计:IntervalSet<L>
3.2.3 面向各应用的IntervalSet子类型设计(个性化特征的设计方案)
3.3 面向可复用性和可维护性的设计:MultiIntervalSet<L>
3.3.1 MultiIntervalSet<L>的共性操作
1实验目标概述
本次实验覆盖课程第 4-12讲的内容,目标是编写具有可复用性和可维护性的软件,主要使用以下软件构造技术:
l子类型、泛型、多态、重写、重载
l继承、代理、组合
l语法驱动的编程、正则表达式
lAPI 设计、API 复用
本次实验给定了三个具体应用(值班表管理、操作系统进程调度管理、大学课表管理),学生不是直接针对每个应用分别编程实现,而是通过 ADT 和泛型等抽象技术,开发一套可复用的 ADT 及其实现,充分考虑这些应用之间的相似性和差异性,使 ADT 有更大程度的复用(可复用性)和更容易面向各种变化(可维护性)。
2实验环境配置
按照Lab 0中的步骤将仓库clone到本地,按提交格式命名与组织。
3实验过程
请仔细对照实验手册,针对每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
3.1待开发的应用场景
- 选择开发其中两个应用:值班表管理(DutyRoster)
操作系统进程调度管理(ProcessSchedule)
简要介绍两个应用。
应用简述如下:
1.值班表管理(DutyRoster)
一个单位有 n 个员工,在某个时间段内(例如乡镇中学国庆节 10 月 1日到 10 月 1 日期间放假,需要教职工进行值班,防止出现火灾、盗窃等意外事件),每天只能安排唯一一个员工在单位值班,且不能出现某天无人值班的情况;每个员工若被安排值班 m 天(m>1),那么需要安排在连续的 m 天内(方便居住较远的教职工,不用来回跑)。值班表内需要记录员工的名字、职位、手机号码,以便于外界联系值班员和事故的责任规划。
总结其设计要求如下:
1)设置总值班时间段 setDate()
2)添加员工 addEmployee()
3)删除员工 deleteEmployee()
4)排班设置时间 manualRoster() autoRoster();
5)删除排班 deleteRoster()
6)排满检查 checkFullRoster();
2.操作系统进程调度管理(ProcessSchedule)
考虑计算机上有一个单核CPU,多个进程被操作系统创建出来,它们被调度在 CPU 上执行,由操作系统决定在各个时段内执行哪个线程。操作系统可挂起某个正在执行的进程,在后续时刻可以恢复执行被挂起的进程。可知:每个时间只能有一个进程在执行,其他进程处于休眠状态;一个进程的执行被分为多个时间段;在特定时刻,CPU 可以“闲置”,意即操作系统没有调度执行任何进程;操作系统对进程的调度无规律,可看作是随机调度。
总结其设计要求如下:
1)添加进程 addProcess()
2)随机选择进程
3)最短进程优先
共性:都涉及在一段时间上,截取时间片段与某个标签进行对应
差异:
值班表 | 操作系统进程 | |
搭载信息 | 员工名字,职位,手机号码 | 进程 ID、进程名称、最短执行时间、最长执行时间 |
标签与时间段的对应 | 一个标签只能对应一个时间段 | 一个标签可以对应多个时间段 |
分配的时间段之间可否有空闲 | 不可 | 可 |
是否包含周期性的时间段 | 否 | 否 |
是否允许不同的 interval 之间有重叠 | 否 | 否 |
3.2面向可复用性和可维护性的设计:IntervalSet<L>
该节是本实验的核心部分。
3.2.1IntervalSet<L>的共性操作
IntervalSet<L>是一种用于描述时间轴上一系列时间段的数据结构。每个时间段都带有一个独特的标签,并且这些标签不能重复。我们将共同的操作封装在IntervalSet<L>接口中,以提高时间段管理的效率。具体的共性操作如下:
创建一个空对象 | empty() |
在当前对象中插入新的时间段和标签 | void insert(long start,long end, L label) |
获得当前对象中的标签集合 | Set<L> labels() |
从当前对象中移除某个标签所关联的时间段 | boolean remove(L label) |
返回某标签对应的时间段的开始时间 | long start (L label) |
返回某标签对应的时间段的结束时间 | long end (L label) |
判断IntervalSet是否为空 | boolean isEmpty() |
以字符串形式打印相关信息 | String toString() |
其中,empty()方法设置为静态工厂方法,其余方法设置为实例方法。
3.2.2局部共性特征的设计方案
为了提高代码的可重用性,我们选择了方案6来设计IntervalSet的局部共性特征。在这个方案中,我们将CommonIntervalSet视为未经装饰的原始对象,而将是否重叠、是否存在空闲以及是否具有周期性这三个维度视为三种不同的“装饰”。每个维度的不同特征取值可以产生不同的“装饰”效果。我们利用设计模式中的装饰器方法来实现这一点。通过为CommonIntervalSet对象逐层添加不同的装饰,即可实现所需的组合特征。
这种装饰器设计模式的优点在于,它使得装饰类和被装饰类能够独立地演进,彼此之间的耦合性较低,因而易于维护和扩展。具体而言,针对IntervalSet,我们利用了装饰器设计模式,实现了IntervalSet<L>接口的抽象装饰类IntervalSetDecorator,并将IntervalSet对象作为其实例变量,从而实现了装饰的功能:
在这之后,创建扩展了 IntervalSetDecorator类的实体装饰类。对于IntervalSet及其可能的应用场景,设计了两个实体装饰类:NoBlankIntervalSet和NonOverlapIntervalSet,分别用于判断IntervalSet中是否存在空闲,是否存在重叠。对这两个实体装饰类的判断算法分别阐释如下:
1.NoBlankIntervalSet
在NoBlankIntervalSet中,进行IntervalSet中是否存在空闲的判断。判断是否存在空闲的思路如下:
l首先利用迭代器对intervalSet进行遍历,从而找到intervalSet中时间段的最小值与最大值min,max;并在遍历的同时将每组标签对应的start-end保存在Map<Long,Long>中,以便后续使用。
l从min到max,步长为1遍历每个键值对, 若存在某个值不在任何一个键值对区间中,则返回true,表示该intervalSet中存在空白,否则遍历完成后返回false。:
2.NonOverlapIntervalSet
在NonOverlapIntervalSet中,进行IntervalSet中是否存在重复时间段的判断。判断是否存在重复时间段的大致思路如下:
l我们遍历每个标签,并记录已经覆盖的时间点。然后遍历下一个标签中的时间点,如果某个时间点已经存在于 HashSet 中,则说明存在重叠,返回 true;如果遍历到最后也没有时间点同时存在于两个标签的时间段中,则返回 false,表示不存在重叠。具体算法如下:
3.2.3面向各应用的IntervalSet子类型设计(个性化特征的设计方案)
CommonIntervalSet<L>是IntervalSet<L>接口的具体实现类,implements了IntervalSet<L>中的方法,对其中的共性方法进行了实现。
在CommonIntervalSet<L>,包含两个关键成员变量:timeschedule和labels。timeschedule是一个HashMap,功能是以键值对的形式保存每个时间段。
对于timeschedule,键是泛型L,而值是长度为2的List<Long>,在List<Long>中,第一个位置保存时间段的起始值,第二个位置保持时间段的终点。由于IntervalSet具有每个时间段对应标签不可重复的特性,所以不用担心Map添加同样标签对应的键值对导致的覆盖问题。
对于labels,其为一个HashSet,保存着已经添加进IntervalSet的泛型L。
CommonIntervalSet<L>的AF,RI,以及Safety from Exposure如下:
checkRep()函数
每个函数的具体实现:
1.void insert(long start, long end, L label)
insert函数首先判断异常情况,若出现异常则直接return,不执行后续步骤,若正常则继续执行,构造出L与List<Long>,以键值对形式加入全局变量schedule,更新labels。
2.Set<L> labels()
采取防御性拷贝的方式,为客户端返回一个保存有labels同样信息的新的HashSet,防止暴露自身变量。
3.boolean remove(L label)
遍历schedule,检索键是否是label,若是,则删除对应的键值对,并从labels中移除该label,然后返回true,表示删除成功。若未检索到label,则返回false。
4.long start(L label)
遍历schedule,检索其中是否存在键为label的,若是则返回对应List<Long>的第一个值,即label对应时间段的起始值。若遍历完毕仍未检索到,则返回-1。
5.long end(L label)
遍历schedule,检索其中是否存在键为label的,若是则返回对应List<Long>的第二个值,即label对应时间段的终点值。若遍历完毕仍未检索到,则返回-1。
6.boolean isEmpty()
根据schedule与labels是否为空来判断当前IntervalSet是否为空
7.boolean checkBlank()与boolean checkOverlap()
利用decorator设计模式,创建一个新的实体装饰类,委派给其检查空闲/重复的内部方法,从而得到关于当前IntervalSet的是否有空闲/是否有重复的具体信息。
8.String toString()
以字符串形式打印IntervalSet的信息,即:标签+对应的时间段。
3.3面向可复用性和可维护性的设计:MultiIntervalSet<L>
3.3.1MultiIntervalSet<L>的共性操作
MultiIntervalSet<L>描述了一组在时间轴上分布的“时间段”(interval),且每个时间段对应的标签是可重复的,同一个标签对象 L 可被绑定到多个时间段上。构造接口,将MultiIntervalSet的共性的操作都放入MultiIntervalSet<L>接口中封装起来。具体的共性操作如下:
判断MultiIntervalSet是否为空 | boolean isEmpty() |
在当前对象中插入新的时间段和标签 | void insert(long start,long end, L label) |
获得当前对象中的标签集合 | Set<L> labels() |
从当前对象中移除某个标签所关联的时间段 | boolean remove(L label) |
从当前对象中获取与某个标签所关联的所有时间段 | IntervalSet<Integer> intervals(L label) |
判断时间轴是否允许空白 | boolean checkBlank() |
判断是否允许不同的 multiInterval 之间有重叠 | boolean checkOverlap() |
判断是否包含周期性的时间段 | boolean checkPeriodic() |
以字符串形式打印相关信息 | String toString() |
并设置为实例方法。
3.3.2局部共性特征的设计方案
针对MultiIntervalSet的局部共性特征进行设计时,同样采用Decorator设计模式,实现MultiIntervalSet<L>接口的抽象装饰类MultiIntervalSetDecorator,其中以MultiIntervalSet对象作为它的实例变量。
在这之后,创建了扩展了 MultiIntervalSetDecorator 类的实体装饰类。针对 MultiIntervalSet 及其可能的应用场景,例如进程调度和课表安排,设计了三个特征的实体装饰类:NoBlankMultiIntervalSet、NonOverlapMultiIntervalSet 和 NonPeriodicMultiIntervalSet。这些装饰类分别用于判断 MultiIntervalSet 中是否存在空闲时间段、是否存在时间段重叠、是否具有周期性。
在这部分中,实体装饰类的具体实现与之前 IntervalSet 中类似。仅阐述一些实现细节上的不同之处:
1.NoBlankMultiIntervalSet
在NoBlankMultiIntervalSet中,进行MultiIntervalSet中是否存在空闲的判断。判断是否存在空闲的思路如下:
l首先利用迭代器对intervalSet进行遍历,从而找到intervalSet中时间段的最小值与最大值min,max;并在遍历的同时将每组标签对应的start-end保存在Map<Long,Long>中,以便后续使用。与IntervalSet不同的是,这里采用MultiIntervalSet的intervals方法来获取某个标签对应的所有时间段。
l从min到max,步长为1遍历每个键值对,这部分与IntervalSet基本相同。
2.NonOverlapMultiIntervalSet
l在NonOverlapMultiIntervalSet中,进行MultiIntervalSet中是否存在重复时间段的判断。这部分与NonOverlapIntervalSet的区别也主要在遍历寻找min,max值,故不再赘述。
3.NonPeriodicMultiIntervalSet
这部分的大体思路也与之前相同,故不多加阐述
3.3.3面向各应用的MultiIntervalSet子类型设计 (个性化特征的设计方案)
为MultiIntervalSet<L>设计其具体的实现类CommonMultiIntervalSet<L>。在CommonMultiIntervalSet中,主要利用了委派的方法,基于IntervalSet实现其相关功能。
在CommonMultiIntervalSet<L>中,设置了如下成员变量:
1.multi
multi是一个List,保存着不同的IntervalSet。IntervalSet中储存着标签与对应的时间段。
2.labels
保存标签,即已经添加进MultiIntervalSet的泛型L。
AF,RI,以及Safety from Exposure:
checkRep():
分点阐述每个函数的具体实现:
1.boolean isEmpty()
根据multi和label调用isEmpty()方法的返回结果,来判断CommonMultiIntervalSet是否为空。
2.void insert(long start, long end, L label)
基本思路是:multi的时间段保存在List的IntervalSet中,因为IntervalSet不能保存重复的标签,所以需要对multi进行遍历判断。若在其中的IntervalSet中发现了重复的标签,则新建一个IntervalSet,将标签与对应时间段的键值对加入新的IntervalSet,再将新的IntervalSet加入到multi中。
3.Set<L> labels()
采用防御性拷贝的方式,返回一份新的Set<L>。
4.boolean remove(L label)
从当前MultiIntervalSet中移除某个标签所关联的所有时间段。在实现时,用增强for循环对multi中的IntervalSet<L>进行检索,若在当前IntervalSet<L>中发现了label对应的时间段,则通过IntervalSet的remove()方法删掉该时间段,若当前IntervalSet<L>中没有该时间段,则退出循环(因为根据之前设计的insert()方法,若当前IntervalSet<L>没有该标签,则之后的IntervalSet<L>必定也没有)。返回值为true说明成功进行了删除,返回值false说明没有找到label对应的时间段。
5.IntervalSet<Integer> intervals(L label)
该方法的作用是从当前对象中获取与某个标签所关联的所有时间段,并将其保存在IntervalSet<Integer>中。
首先调用自身的isEmpty()方法进行判断,若为空则直接返回null;然后进行下一步,将对应label的intervalSet加入List<List<Long>> map。这里叫map的原因是该数据结构模拟的是键值对集合,即map,但其与Map的区别为Map中键是唯一的,若重复添加同一个键对应的不同值,则后值会覆盖新值。所以为避免这个问题,采用自行设计的List<List<Long>>数据结构来表示map。这部分的具体实现如下:
完成以后,将该label对应的intervalSet按从小到大的次序加入list,具体实现方式为遍历map,寻找到其中start的最小值,将该“键值对”加入待返回的intervalSet,然后将其从map中删去,直到map为空为止。
6.boolean checkBlank()、checkOverlap()、checkPeriodic()
这几个部分与IntervalSet中对应部分类似,直接返回一个新装饰对象该方法的返回值。
7.String toString()
在这个方法中,首先对multi是否为空进行检查,然后遍历multi的每一个IntervalSet,然后将toString的工作委派给IntervalSet来完成。具体实现如下:
3.4面向复用的设计:L
IntervalSet<L>和 MultiIntervalSet<L>中的泛型参数 L,可以是任何 immutable的类。对于要开发的三个具体应用来说,L 分别应为“员工”(Employee)、“进程”(Process)。于是分别实现ADT。
3.4.1ADT的共有属性
由于这三个ADT均为immutable的,所以对于他们,均只设置Getter方法,不设置Setter方法。
由于同类ADT之间可能出现相互比较的情况,故对于每个ADT,均需要重写equals()与hashCode()方法。
另外,为了表示统一,三个ADT一律只设计带参构造方法,不设置无参构造。
3.4.2员工(Emplpyee)
员工类包含三个私有成员变量,其字段与意义如下所示:
Employee中,为每个成员变量设置Getter方法。
员工类重写了equals()与hashCode()方法,依据是成员变量中name字符串:
3.4.3进程(Process)
进程类包含三个私有成员变量,其字段与意义如下所示:
为每个成员变量设置Getter方法,并重写了equals()与hashCode()方法,依据是成员变量中的ID:
3.5可复用API设计
3.5.1计算相似度
计算相似度的函数为:
public double Similarity(MultiIntervalSet<L> s1, MultiIntervalSet<L> s2)
具体实现思路如下:
1.首先,分别对 s1 和 s2 调用 labels() 方法,以获取它们的共有标签,并将结果保存在 Set<L> labels 中。。
2.类似之前的方法,遍历时间段,确定时间轴的起始值 min 和结束值 max。
3.对于每个共有标签,在 s1 中进行遍历以找到其对应的时间段。此处的查找是确定性的,因为共有标签已知。记当前共有标签在 s1 中的某个时间段为 [baseStart, baseEnd]。接着在 s2 中遍历相同标签下的时间段进行比较,记选取的时间段为 [compareStart, compareEnd]。若 compareStart > baseEnd 或 compareEnd < baseStart,说明两个时间段完全不重叠,继续下一次循环。否则,计算重叠部分的起始值 accStart(两个起始值的最大值)和结束值 accEnd(两个结束值的最小值),并将该重叠长度累加到返回变量中。
4.完成所有共有标签的循环后,得到最终的相似度值,并将其返回。
步骤3对应的核心函数实现如下:
3.5.2计算时间冲突比例
计算时间冲突比例的函数有两个,运用了方法重载,以便能够对IntervalSet<L>和MultiIntervalSet<L>都进行处理:
public double calcConflictRatio(IntervalSet<L> set)
public double calcConflictRatio(MultiIntervalSet<L> set)
具体实现思路如下:
1.首先,确定时间轴的起点和终点 min 和 max,并将每组标签对应的起始和结束时间保存在键值对中。考虑到可能存在起点重复的情况,我们选择通过 List<List<Long>> 结构来表示键值对。
2.接着,我们设置一个标志变量 flag,将冲突时间初始值设为 0。通过从 min 到 max 的循环,逐步遍历所有键值对。如果某个时间点同时存在于两个键值对中,则表示出现了时间冲突,将冲突时间变量加一。
3.最后,循环结束后,我们将冲突时间除以总时间(即 max - min + 1)的值,得到冲突比例,并将其返回。
关键函数如下:
3.5.3计算空闲时间比例
计算空闲冲突比例的函数有两个,同样运用了方法重载:
public double calcFreeTimeRatio (IntervalSet<L> set)
public double calcFreeTimeRatio (MultiIntervalSet<L> set)
具体实现思路如下:
1.首先,确定时间轴的起点和终点 min 和 max,并将每组标签对应的起始和结束时间保存在键值对中。考虑到可能存在起点重复的情况,我们选择通过 List<List<Long>> 结构来表示键值对。
2.接着,我们设置一个标志变量 flag,将冲突时间初始值设为 0。通过从 min 到 max 的循环,逐步遍历所有键值对。如果某个时间点同时存在于两个键值对中,则表示出现了时间冲突,将冲突时间变量加一。
3.最后,循环结束后,我们将冲突时间除以总时间(即 max - min + 1)的值,得到冲突比例,并将其返回。
关键函数如下:
3.6应用设计与开发
利用上述设计和实现的ADT,实现手册里要求的各项功能。
3.6.1排班管理系统
针对排班管理系统的功能要求,设计了具体的ADT子类型DutyIntervalSet与客户端DutyRosterApp。以下分别阐述。
lDutyIntervalSet
DutyIntervalSet中设置了如下成员变量:
LocalDate类型对象:start(排班的起始时间)与end(排班的结束时间)
List employees(上班员工),schedule(排班的时间段)是IntervalSet类型。
AF,RI,Safety from exposure如下:
DutyIntervalSet中设计了如下方法:
public boolean setDate(String s1, String s2) | 排班开始日期、结束日期,具体到年月日 |
public boolean addEmployee(String info) | 增加一组员工,名字、职务、手机号码 |
public boolean deleteEmployee(String name) | 删除员工。已经被编排进排班表员工,不能删除,须将其排班信息删掉之后才能删除该员工。 |
public boolean manualRosters(String name, String sStart, String sEnd) | 手工选择员工、时间段(以“日”为单位,最小 1 天,可以是多天),向排班表增加一条排班记录 |
public boolean autoRosters() | 自动根据现有员工进行排班 |
public boolean deleteRoster(String name, String start) | 删除特定员工的排班 |
public void checkFullRoster() | 检查排班是否排满,并返回相应的信息 |
public void rosterVisualization() | 可视化当前排班表 |
public void clear() | 清空数据 |
private boolean checkDate(int year, int month, int day) | 检查日期是否合法 |
private boolean checkInRoster(Employee e) | 检查某员工是否在排班表中 |
private long getBetweenDays(LocalDate start, LocalDate end) | 计算两个时间点之间间隔的天数 |
private List<Integer> dateConversion(long startPoint, long endPoint) | 根据时间轴上的起始与终止时间,返回对应的实际日期 |
private Map<Long, Long> saveMap() | 将用户时间段保存在map中 |
private double calcFreeTimeRatio(IntervalSet<Employee> set) | 计算一个 IntervalSet<Employee> 对象中的空闲时间比例 |
public LocalDate splitDate(String date) | 将日期分割转化为LocalDate类型 |
下面对各个方法进行阐述:
1.public boolean setDate(String s1, String s2)
读入两个字符串s1与s2,即输入起始时间和结束时间。输入格式为YYYY-MM-DD。首先检查已经有排班,输入格式错误,起始时间在终止时间后等情况,无误后使用splitDate分割输入字符串进行,并保存至代表起始时间和终止时间的LocalDate类型成员中。完成后输出提示信息。
2.public boolean addEmployee(String info)
按照name{duty, phone}的格式读入字符串info,对员工是否已存在(通过employee类重写的equals()方法实现),输入格式是否正确进行检查,若无误则新增该员工,并打印提示信息。
3.public boolean deleteEmployee(String name)
检查非法情况,然后根据输入的name在employees中进行匹配,若匹配到则返回true,并将其移除,否则返回false。
4.public boolean manualRosters(String name, String sStart, String sEnd)
输入员工的名字与排班时间,对员工进行排班。首先检查异常情况:排班起始结束未设定、输入格式错误、员工不存在、员工已在排班表中、日期有误等,如果无误,则向schedule中加入,在加入完成后调用intervalSet的checkOverlap方法,检测加入后是否出现日期重叠的现象,若是,则删去刚刚加入的时间段,并打印错误信息,返回false,若不是,则说明时间段没有重叠,可以正常加入。
5.public boolean autoRosters()
如果排班总时长days可以被人数n整除,则每个人排days/n天班;如果days不能被n整除,则最后一个人排 days%n 天班,其他人排 days/n 天班。具体实现如下:
6.public boolean deleteRoster(String name, String start)
在完成对异常情况的检查后将schedule中的特定时间段删除,并打印提示信息返回。
7.public void checkFullRoster()
首先对排班是否排满进行检查。调用intervalSet的checkBlank方法。同时,由于checkBlank方法无法得知具体排班的最小时间和最大时间之外是否还有空闲时间,所以需要再通过检查具体排班的最小时间和最大时间是否等于start与end来进行准确的判断。
若排班未排满,则检测空闲时间块并返回具体的空闲时间段。具体实现如下:
首先获取到键值对形式保存的时间段,然后从排班起点start到结尾end依次遍历,步长为1,获得每个空缺的时间点,将空缺的时间点保存到List<Long> interval中。
其次,对interval中的时间点进行合并操作。通过算法找到每个空闲时间段的起点和终点,将他们以键值对的形式加入Map<Long, Long> intervalMap,从而得到空闲时间段的集合
最后,打印空闲时间段的信息并返回。
核心部分函数如下:
8.public void rosterVisualization()
对当前排班表进行可视化。打印输出相关信息。
9.public void clear()
清空该DutyRosterIntervalSet的所有数据
为DutyIntervalSet编写了测试用例,测试策略见DutyIntervalSetTest类的Testing Strategy部分。经过测试,DutyIntervalSet的代码覆盖度较高。
lDutyRosterApp
DutyRosterApp应用了DutyIntervalSet类,为其构造了命令行前端页面。
可以依次输入如下命令来检查其功能:
Y
1
2024-05-19
2024-05-29
2
a{boss,111-1111-1111}
b{manager,222-2222-2222}
c{engineer,333-3333-3333}
7
5
6
7
0
或者下面这一组:
Y
1
2024-05-19
2024-05-29
2
a{boss,111-1111-1111}
b{manager,222-2222-2222}
c{engineer,333-3333-3333}
4
a
2024-05-19
2024-05-21
4
b
2024-05-22
2024-05-26
4
c
2024-05-27
2024-05-29
7
8
6
b
2024-05-22
7
8
0
3.6.2操作系统的进程调度管理系统
针对操作系统的进程调度管理系统的功能要求,设计了具体的ADT子类型ProcessIntervalSet与客户端ProcessScheduleApp。以下分别阐述。
lProcessIntervalSet
ProcessIntervalSet中设置了如下成员变量:
Schedule(MultiIntervalSet类,保存着排班的时间段;
List processes(上班的员工)
Excutedtime(每个进程的已执行时间,键为Process,值为已执行时间)
AF,RI,Safety from exposure:
ProcessIntervalSet中设计了如下方法:
public boolean addProcess(String info) | 添加进程 |
public boolean RAschedule() | 随机选择进程进行调度 |
public boolean SPschedule() | 按“最短进程优先”的原则进行调度 |
public void visualization() | 可视化进程调度系统情况 |
private List<List<long[]>> toList() | 将所有的时间段保存在list中,list.get(0):进程的ID list.get(1):[起始时间,终止时间] |
下面对各个方法进行阐述:
1.public boolean addProcess(String info)
读入字符串info,以“ID-名称-最短执行时间-最大执行时间”的表示进程信息。检查完异常情况后更新processes与executedTime,将新进程添加至processes,并在executedTime置其对应值为0。
2.public boolean RAschedule()
随机选择进程进行调度。首先检查异常情况,无误后设置起始时间点timepoint为0,进行如下循环直到每个进程都执行到最大时间:
ž随机设置休眠时间。设置随机数种子long sleepTime = rand.nextInt(10),每次执行前都在timePoint的原值基础上累加一个当次的sleepTime,代表内核的随机休眠时间。
ž设置一个随机数种子,从[0,temp_process.size()]中随机选取一个数作为此次选取进程的序号,得到此次选取的进程。
ž利用随机数long thisTime = (long) (rand.nextDouble() * maxTime)得到此次的进程执行时间。rand.nextDouble()返回一个[0,1]的浮点数值,在此代表时间执行比例。
ž因为累加上此次执行时间后最大执行时间可能超出,所以需要进行条件判断,从而得到真实的此次执行时间,具体算法如下:
ž若当前进程执行完毕,则将其从temp_processes中删除,循环执行这样的操作直到temp_processes为空。
3.public boolean SPschedule()
按“最短进程优先”的原则进行调度。该方法大体上与RAschedule()类似,区别在于找到下一个该执行的进程的思路。在这里,通过遍历进程,用每个进程的最大执行时间减去已执行时间得到每个进程的剩余执行时间,选取剩余执行时间最短的进程作为下一个要执行的进程。进程选取部分的代码如下:
4.public void visualization()
进行进程调度情况的可视化,打印相关信息。
5.private List<List<long[]>> toList()
由于每个进程可能执行多次,所以在这里不能使用Map,只能自行构造List<List<long[]>>来模拟键值对。
为ProcessIntervalSet编写了测试用例,测试策略见ProcessIntervalSetTest类的Testing Strategy部分。经过测试,ProcessIntervalSetTest的代码覆盖度较高。
lProcessScheduleApp
ProcessScheduleApp应用了ProcessIntervalSet类,为其构造了命令行前端页面。菜单页面如下:
可以依次输入如下命令来检查其功能:
1
0-init-100-100
1
1-GUI-50-70
1
2-Remote Connect-10-40
1
3-Local Service-40-90
2
4
0
或另一组:
1
0-init-100-100
1
1-GUI-50-70
1
2-Remote Connect-10-40
1
3-Local Service-40-90
3
4
0
3.7基于语法的数据读入
设计文件test1-4.txt,放入目录src/subADT/txt下。
在DutyRoster文件加下新建一个parserInput类,在其中编写读取文件的程序及方法。
parserInput的主程序如图所示构成。作用是读取输入字符串并跳转到checkInput()方法,直到程序正常退出。
在private static void checkInput(String str) throws IOException中,首先检查输入格式是否正确,再检查输入是否为q,是则直接退出。然后根据选择的文件序号构造出文件路径,传入parser()方法中。
private static void parser(String filePath) throws IOException为parser()方法的方法声明。在该方法中,首先读取txt文件,将其内容按行保存在List<String> list中。在这之后,按如下思路进行处理:
1.根据每个段的起始字符不同(Employee{、Period、Roster{),标记每个段对应的起始行,便于后续分割。每个段的起始行分别记为employeeStart, periodStart, rosterStart。
2.提取每个段对应的字符串内容。从每个段的起始行开始,遍历每个段,将每个段的有效信息提取到对应的List中。在遍历的过程中使用了正则语法,具体的正则提取式如下(以Employees为例):
在遍历的同时,进行错误情况的判定,具体方法为根据正则表达式提取的有效行保留在一个List中,同时把该段的每一行都保存在另一个List中,遍历完成后比较两个List的长度,若不相等则说明其中有的行的信息不符合正则表达式的规则,说明格式有误。
以Employees为例,提取部分的具体代码如下
3.将提取得到的字符串输入对应的方法完成功能实现。在这一部分中,将前面得到的每一部分的有效字符串转换成对应方法的输入格式,按setDate(),addEmployee(),manualRoster()的顺序输入函数,并随时检查是否有异常情况,如有异常则及时终止。
之后,修改DutyRosterApp类,为其中添加读取文件的功能。具体实现为直接调用parserInput的main函数:之后的工作交给parserInput执行:
4实验进度记录
日期 | 时间段 | 计划任务 | 实际完成情况 |
2024-5-9 | 18:30-20:00 | 分析任务 | 按时完成 |
2024-5-9 | 20:30-23:30 | 接口、测试案例的编写 | 按时完成 |
2024-5-14 | 13:00-14:30 | 具体实现的编写1IntervalSet | 按时完成 |
15:00-16:30 | 具体实现的编写2IntervalSet | 按时完成 | |
2024-5-14 | 19:00-21:00 | 具体实现的编写1MultiIntervalSet | 按时完成 |
2024-5-14 | 21:30-23:30 | 具体实现的编写1MultiIntervalSet2 | 按时完成 |
2024-5-16 | 1:00-2:30 | 编写DutyInterval | 按时完成 |
2024-5-16 | 2:30-3:00 | 编写测试 | 按时完成 |
2024-5-21 | 16:00-17:30 | 编写Process | 按时完成 |
2024-5-21 | 18:00-20:00 | 修改代码 | 按时完成 |
2024-5-21 | 22:00-23:00 | 完成报告 | 按时完成 |
5实验过程中遇到的困难与解决途径
遇到的难点 | 解决途径 |
处理时间段输入时,不好分出函数,总容易出现复用性不高、重复问题 | 尽量将多次用到的代码段提取出来,尽量实现复用,太过精细的部分要做取舍。 |
ADT判断空,遍历,通过相邻子时间段是否有空隙判断是否有未覆盖的位置,但是输入的时间段不一定时顺序的,涉及排序 | 放弃顺序寻找空袭,在全时间段搜索,看是否能找到一个时间段没有包含进来 |
6实验过程中收获的经验、教训、感想
6.1实验过程中收获的经验和教训
6.2针对以下方面的感受
(1)重新思考Lab2中的问题:面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?本实验设计的ADT在三个不同的应用场景下使用,你是否体会到复用的好处?
面向ADT的编程和直接面向应用场景的编程有明显的差异。面向ADT的编程更注重数据结构和算法的设计,以及对接口和抽象的考虑,使得代码更加灵活、可复用性更高。而直接面向应用场景的编程则更加注重解决特定问题,可能会牺牲一些通用性和灵活性,但会更加直观和具体。
(2)重新思考Lab2中的问题:为ADT撰写复杂的specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后的编程中坚持这么做?
为ADT撰写复杂的specification、invariants、RI、AF等工作的意义在于确保ADT的正确性和可靠性。这些规范和约束帮助我们更好地理解ADT的行为和特性,同时也有助于我们在编程过程中发现和解决潜在的问题。
尽管这些工作可能会增加一些开发的时间和精力成本,但是在长期来看,能够有效地提高代码的质量和可靠性,减少后期维护和调试的工作量。因此,我愿意在以后的编程中坚持这么做。
(3)之前你将别人提供的API用于自己的程序开发中,本次实验你尝试着开发给别人使用的API,是否能够体会到其中的难处和乐趣?
(4)你之前在使用其他软件时,应该体会过输入各种命令向系统发出指令。本次实验你开发了一个解析器,使用语法和正则表达式去解析输入文件并据此构造对象。你对语法驱动编程有何感受?
通过开发给别人使用的API,我深刻体会到了其中的难处和乐趣。在开发过程中,需要考虑到不同用户的需求和使用场景,尽可能地提供清晰简洁、易于理解和使用的接口。同时,还需要考虑到API的稳定性、扩展性和兼容性,确保在不同的环境下都能够正常运行。这需要综合考虑各种因素,需要不断地进行设计和调整,但是看到最终产品能够帮助用户解决问题,带来了很大的满足感。
(5)Lab1和Lab2的大部分工作都不是从0开始,而是基于他人给出的设计方案和初始代码。本次实验是你完全从0开始进行ADT的设计并用OOP实现,经过五周之后,你感觉“设计ADT”的难度主要体现在哪些地方?你是如何克服的?
设计ADT的难度主要体现在对问题领域的理解和抽象能力,以及对数据结构和算法的熟练程度。在设计ADT时,需要考虑到不同的使用场景和需求,同时保持接口的简洁清晰,确保代码的可维护性和可扩展性。我通过不断学习和实践,不断改进和完善ADT的设计。
(6)“抽象”是计算机科学的核心概念之一,也是ADT和OOP的精髓所在。本实验的五个应用既不能完全抽象为同一个ADT,也不是完全个性化,如何利用“接口、抽象类、类”三层体系以及接口的组合、类的继承、设计模式等技术完成最大程度的抽象和复用,你有什么经验教训?
在利用接口、抽象类、类三层体系以及设计模式等技术完成最大程度的抽象和复用时,我认为关键是要深入理解问题的本质和需求,找到合适的抽象层次和接口设计,同时结合具体的场景和实际需求,灵活运用各种技术手段进行设计和实现。
在实践中,不断地尝试和总结经验教训,积累起来的经验和技能会帮助我们更好地应对各种挑战和问题。
(7)关于本实验的工作量、难度、deadline。
十分困难!!!!超级困难!!!
(8)到目前为止你对《软件构造》课程的评价。
真的很实用,但是学起来既有趣又十分的痛苦,甚至痛不欲生!!!