文章目录
前言
本次实验涵盖了课程前两次课的内容,目标是编写具备高复用性和可维护性的软件。
代码量好大,,写了好久,,debug了好久。
把实验中比较有价值的思路和整个项目的大概结构记录下来了,和实验报告不一样,主要是更粗颗粒度地总结一下。。
1. 实验目标
值班表管理、操作系统进程调度管理、大学课表管理,需要把前期准备工作全部完成了,但是App只需要开发两个。
2. 实验过程
对三个App进行分析:
共性:
1.实体的私有属性不可改变,比如员工的姓名、进程的pID,课程的cID。
2.都是基于一个时间轴上,对各个时间段进行操作。
3.同一个标签下的时间段均不允许重叠。
差异:
QAQ | 值班表 | 进程调度 | 课程安排 |
---|---|---|---|
标签与时间段的映射 | 一对一 | 一对多 | 一对多 |
是否允许不同标签下的重叠时间段 | 否 | 否 | 是 |
时间轴上是否可以有空白 | 否 | 是 | 是 |
是否包含周期性的时间段 | 否 | 是 | 是 |
2.1 对基础类型的构建总结
IntervalSet表示时间轴上的一组带有标签的时间段,带有同一个标签的时间段不能重复出现,其为一个抽象接口。
为了提高代码的复用性,我选择了方案5来设计局部共性特征,即CRP
这样,应用子类可以直接实现这个组合接口。在应用子类中,不是直接实现每个特定操作,而是通过委托机制将这些操作委托给外部的各维度具体实现类,执行相应的特定操作逻辑。
NoBlankIntervalSet<L>
该接口用于检查时间轴上是否留有空隙,直接地被NoBlankIntervalSetImpl类实现.
NoBlankIntervalSetImpl中,为了确定时间轴的起点与终点,保有成员变量start与end,以及一个IntervalSet intervalSet时间轴,通过带参构造函数构造NoBlankIntervalSetImpl,并提供返回时间轴start与end的函数。
NonOverlapIntervalSet<L>
该接口用于检查时间轴上是否存在时间段的重叠或完全覆盖,直接地被NonOverlapIntervalSetImpl类实现,在该接口中,定义方法checkIfCanInsert(),用于在每次插入前检查是否该时间段会与时间轴中含有的其他时间段重叠,若是,则不能插入。
checkIfCanInsert()方法的实现思路是在每次插入新的时间段之前,检查这个新的时间段是否会与时间轴上已有的任何时间段重叠
NonPeriodicIntervalSet<L>
检查一个时间段是否可以插入到一个非周期性时间段中,若需要插入的时间段的时间在时间轴规定范围之外,即不能插入该period,则抛出NonperiodicException异常
CommonIntervalSet<L>
其为IntervalSet的最初实现类。
在CommonIntervalSet中添加了成员变量private final Map<L, List> labelDataMap = new HashMap<>(),用于记录时间轴上的每个时间段:L为时间段的标签, List中含有单个时间段中的起始和结束时间。
CommonIntervalSet需要实现IntervalSet中声明的全部方法
IDutyIntervalSet
由于值班管理系统中,每个值班人员只值班一次,且不能出现值班时间段重叠,不能存在空隙,且非周期性,所以其应该继承NoBlankIntervalSetImpl、NonOverlapIntervalSetImpl、NonPeriodicIntervalSet接口,由DutyIntervalSet类直接实现。
与 IntervalSet 不同的是,MultiIntervalSet 描述了一组在时间轴上分布的“时间段”,且每个时间段对应的标签是可以重复的。也就是说,同一个标签对象 L 可以绑定到多个时间段上。在设计 MultiIntervalSet 时,我将它直接设计为class。
NonOverlapMultiIntervalSet
声明了一个用于插入检查的函数public void checkIfCanInsert(long start, long end, L label) throws NonlapException.
其实现类NonOverlapMultiIntervalSetImpl,其函数public void checkIfCanInsert(long start, long end, L label) throws NonlapException用于实现检查插入的时间段不会与时间轴中时间段重叠或覆盖.
NonOverlapMultiIntervalSet
声明了一个用于插入检查的函数public void checkIfCanInsert(long start, long end, L label) throws NonlapException.
其实现类NonOverlapMultiIntervalSetImpl需要一个成员变量保管所有时间段,其函数public void checkIfCanInsert(long start, long end, L label) throws NonlapException用于实现检查插入的时间段不会与时间轴中时间段重叠或覆盖。
IProcessIntervalSet
由于进程调度系统中,进程可以被调度多次,但不能出现时间段重叠,可以存在空隙,所以其应该继承NonOverlapMultiIntervalSetImpl接口,由ProcessIntervalSet类直接实现。
对于实现类ProcessIntervalSet,其保管成员变量private NonOverlapMultiInetervalSetImpl noOverlap、private final MultiIntervalSet multiIntervalset=new MultiIntervalSet<>(),用于委派检查时间段重叠、进行时间段操作。
CourseIntervalSet
其可以排多门课,多门课可以重叠或空白,有周期性,故实现较为简单,保有一个private final MultiIntervalSet multiIntervalset = new MultiIntervalSet<>();用于执行时间段操作。
2.2 可复用API设计
2.2.1计算相似度
简单的思路就是先找出两个时间轴上的公共标签集合,一定要多看API帮助文档或者多问copilot,别傻乎乎地自己搁那儿写,可以很方便地发现两个集合的公共部分用Set的retainAll方法特别快。
对在retain All得出的标签对应的两个集合,选择一个当标准,遍历里面的每个时间段并在另一个里面找重叠,加起来就行。
2.2.2计算时间冲突、空闲比例
最主要的实现思路就是“步长为1地遍历检查重叠”。
2.3 应用设计与开发
到了用自己的成果写应用的时刻,感到累且爽地
2.3.1DutyRoserApp
对比较复杂的几个功能进行总结:
1)void addDuty(String name, long start, long end)
方法要检查新的值班时间段是否与该员工已有的值班时间段连续:遍历dutySet中的所有员工,找到与当前员工相同的员工,比较新的值班时间段的开始时间是否等于已有值班时间段的结束时间加一,或者新的值班时间段的结束时间是否等于已有值班时间段的开始时间减一。如果新的值班时间段与已有的值班时间段不连续,抛出IllegalArgumentException。
最后,将新的值班时间段添加到dutySet中,并将员工的状态在PeopleWithStatus映射中设置为true,在此过程中,如果新的值班时间段与已有的值班时间段重叠,DutyIntervalSet的Insert抛出NonlapException。
2)void autoGenenateDutyset()
创建随机数生成器,用于生成每个员工的值班时间段长度,挨个排进去,这里由于Set里边的元素取出来可以看成是随机的所以就当是随机了,然后如果都排好了还没排满,就都排给最后一个。当然要是满了就立刻终止。
2.3.2 ProcessScheduleApp
较为复杂的方法:
boolean minFirstGet()
首先检查是否有进程可供调度。如果processes列表为空,即没有进程可供调度,打印消息并返回false。 若有进程可供调度,检查是否已经进行了调度。如果processSchedule非空,即已经进行了调度,打印消息并返回false。
如果上述两种情况均未发生,开始进行最短优先调度。设置一个标志flag为1,表示开始调度。
初始化时间点timePoint为0,创建一个随机数生成器rand,并创建一个临时进程列表temp_processes,用于保存尚未完全执行的进程。
当temp_processes非空时,进行以下操作:
计算上次执行进程后的休眠时间blankTime,并更新时间点timePoint。
找到剩余时间最短的进程。遍历temp_processes,对每个进程,计算其剩余时间(最大执行时间减去已执行时间),选择剩余时间最短的进程p ,计算下一个进程的执行情况:首先,生成一个随机数thisTime,表示p此次的执行时间,获取p的已执行时间executed,计算运行后p的总执行时间totalTime。
如果totalTime大于等于p的最大执行时间,表示进程已执行完毕,此次分配的时间超出。此时,需要调整p的实际已执行时间,并从temp_processes中移除p。
如果totalTime大于等于p的最小执行时间,表示进程执行完毕,分配时间未超出,从temp_processes中移除p。
尝试将进程p的执行时间段插入到processSchedule中。如果出现异常,打印相应的错误消息。
更新p的已执行时间,并更新时间点timePoint。
最后,打印"调度完毕"的消息,并返回true。
要注意到随机和最短优先的实现是极其相似的
2.4 基于语法的数据读入
方法 | 功能 |
---|---|
void readFile(int index) | 根据指定文件生成排班表 |
long checkLineParser(String str) | 检查一个时间信息字符串是否符合格式,若符合,将日期信息转换为long形式 |
void getLineInfo(String originStr) | 根据文件中逐行信息生成排班表,若文件格式错误或信息错误,进程直接结束 |
getLineInfo
在确定读入顺序的情况下,对文件的每一行进行检查。
由于文件中需要忽略空白字符,故首先将该字符串去掉空白字符
对于含有"Period{"的一行,截取为时间轴开头与结尾对应信息部分,用checkLineParser进行检查。
由于"Employee{“与"Roster{”,由于信息格式不同,故不需要以其做区分,可以直接跳过。
员工信息以name{职位,电话}格式读入,职位与姓名不含数字,电话格式为XXX-XXXX-XXXX,故为电话设置正则表达式:“\d{3}-\d{4}-\d{4}”,姓名与职位正则表达式为:“[a-zA-Z ]+”。
排班信息以name{排班开始时间,排班结束时间}格式读入。同理,设置姓名与日期组的正则表达式 “[a-zA-Z]+“以及”\d{4}-\d{2}-\d{2},\d{4}-\d{2}-\d{2}”,在本行寻找到符合条件的日期后。寻找符合条件的姓名。
总结
面向ADT编程具有更高的复用性,而面向具体应用编程虽然思路清晰,但复用性较低,会导致大量的重复工作。通过此次实验,我深刻体会到了复用的诸多好处。复用能够在编程过程中减少大量的代码,从而提高开发效率。
设计ADT的主要难度在于构思。需要考虑如何进行抽象化,如何设计函数的参数与返回值,并理清调用与委派之间的关系。
代码量好大,报告也工作量好大,但是还蛮有成就感的。这就是学计算机的人以后的生活吗。
软构是很好的一门课,学了很多东西,但是ddl也确实蛮紧的。