本次实验覆盖课程第 4-11 讲的内容,目标是编写具有可复用性和可维护性的软件,主要使用以下软件构造技术:
本次实验给定了三个具体应用(值班表管理、操作系统进程调度管理、大学课表管理),学生不是直接针对每个应用分别编程实现,而是通过 ADT 和泛型等抽象技术,开发一套可复用的 ADT 及其实现,充分考虑这些应用之间的相似性和差异性,使 ADT 有更大程度的复用(可复用性)和更容易面向各种变化(可维护性)。
本次实验无需额外配置环境。继承Lab2中的即可。
GitHub Lab3仓库地址:
https://github.com/ComputerScienceHIT/Lab3-1190200207
请仔细对照实验手册,针对每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
子类型、泛型、多态、重写、重载
继承、代理、组合
语法驱动的编程、正则表达式
API 设计、API 复用
简要介绍三个应用。
分析三个应用场景的异同,理解需求:它们在哪些方面有共性、哪些方面有差异。
值班表管理(DutyRoster):一个单位有 n 个员工,在某个时间段内(例如寒假 1 月 10 日到 3 月 6 日期间),每天只能安排唯一一个员工在单位值班,且不能出现某天无人值班的情况;每个员工若被安排值班 m 天
(m>1),那么需要安排在连续的 m 天内。值班表内需要记录员工的名字、职位、手机号码,以便于外界联系值班员。
日期 | 值班人名字 | 职位 | 手机号码 |
2021-01-10 | 孙 XX | 书记 | 13900000000 |
2021-01-11 | 孙 XX | 书记 | 13900000000 |
2021-01-12 | 孙 XX | 书记 | 13900000000 |
2021-01-13 | 刘 XX | 主任 | 13811111111 |
2021-01-14 | 张 XX | 院长 | 18266666666 |
2021-01-15 | 张 XX | 院长 | 18266666666 |
2021-01-16 | 王 XX | 副院长 | 18677777777 |
2021-01-17 | 王 XX | 副院长 | 18677777777 |
2021-01-18 | 邬 XX | 副主任 | 15533333333 |
… |
|
操作系统进程调度管理(ProcessSchedule):考虑计算机上有一个单核 CPU,多个进程被操作系统创建出来,它们被调度在 CPU 上执行,由操作系统决定在各个时段内执行哪个线程。操作系统可挂起某个正在执行的进程,在后续时刻可以恢复执行被挂起的进程。可知:每个时间只能有一个进程在执行,其他进程处于休眠状态;一个进程的执行被分为多个时间段;在特定时刻,CPU 可以“闲置”,意即操作系统没有调度执行任何进程;操作系统对进程的调度无规律,可看作是随机调度。
时间轴
同一颜色的矩形表示同一个进程
大学课表管理(CourseSchedule):看一下你自己的课表,每周一上午
8:00-10:00 和每周三上午 8:00-10:00 在正心楼 11 教室上“软件构造”课
程。课程需要特定的教室和特定的教师。在本应用中,我们对实际的课表进行简化:针对某个班级,假设其各周的课表都是完全一样的(意即同样的课程安排将以“周”为单位进行周期性的重复,直到学期结束);一门课程每周可以出现 1 次,也可以安排多次(例如每周一和周三的“软件构造”课)且由同一位教师承担并在同样的教室进行;允许课表中有空白时间段(未安排任何课程);考虑到不同学生的选课情况不同,同一个时间段内可以安排不同的课程(例如周一上午 1-2 节的计算方法和软件构造);一位教师也可以承担课表中的多门课程。
三个应用的共同点,就是其内部的每项任务均可以用一段一段连续的时间表示,这段时间有开始、终止时间点,也需要标记是属于什么的这段时间。
而三个应用的不同点有很多。例如同一段时间里能否同时存在两个任务?某大段时间内是否允许留有空白?根据这个,我们可以设计可复用的ADT。
面向可复用性和可维护性的设计:MultiIntervalSet<L>
三个应用中都存在一大段时间,其中由各个小的连续时间段组成。各个时间段可能是属于不同的主体,因此我们直接使用MultiIntervalSet作为ADT。同时,设计一个Interval<L>的辅助类,用于表示一个时间段及其所属。
MultiIntervalSet<L>的共性操作
在当前对象中插入新的时间段和标签:boolean insert(long start, long end, L label)
获得当前对象中的标签集合:Set<L> labels()
从当前对象中移除某个标签所关联的所有时间段:boolean remove(L label)
从 当 前 对 象 中 获 取 与 某 个 标 签 所 关 联 的 所 有 时 间 段 : IntervalSet<Interval<L>> intervals(L label), 返回结果表达为IntervalSet<Integer>的形式,其中的时间段按开始时间从小到大的次序排列。例如: 当前对象为{ "A"=[[0,10],[20,30]], "B"= [[10,20]] }, 那么 intervals("A") 返回的结果是{ 0=[0,10], 1=[20,30] }。
判断当前所有时间段中是否存在指定标签:boolean contain(L label)
本次设计MultiIntervalSet<L>为接口类型,先实现一个CommonMultiIntervalSet<L>作为常用操作的基类,表示一些操作的共性。CommonMultiIntervalSet<L>的特性是时间段可重叠、时间段内允许有空白。根据本次要求,设计两个特性:不允许时间段重叠、不允许时间段内有空白,分别设计为NoOverlap<L>和NoBlank<L>接口作为标记。接口没有提供任何方法,但实现这个接口即表示规约必须修改为不允许时间段有重叠和不允许时间段内有空白。同时,为了方便后续将时间段升序排列,使用List来容纳各个时间段interval,并且实现一个IntervalComparator类提供比较器,这样就可以直接调用list.sort()方法排序,这里是依据开始时间升序排列。
设计一个NoOverlapMultiIntervalSet<L>类,继承CommonMultiIntervalSet<L>并实现NoOverlap<L>接口。为了实现时间段不允许重叠的特性,重载insert方法,在每次插入时都要检查这段时间是否和已有的时间有重叠之处,如果不重叠则可以正常插入并返回true,反之什么也不做并返回false。
设计的UML图:
-
-
- 面向各应用的MultiIntervalSet子类型设计(个性化特征的设计方案)
-
- 值班表管理
值班表类继承了NoOverlapMultiIntervalSet<Emplooyee>,并扩展了新的特点。由于要设计起止时间,因此insert方法需要检查插入的时间段是否超出了设计的起止时间,并新设计一个checkDutySchedule方法检查当前是否已经满足了NoBlank接口要求的条件。重写了toString方法以输出相关的信息。
对实现的两个类分别编写测试,对于CommonMultiIntervalSetTest类,执行以下的测试策略:
对NoOverlapMultiIntervalSetTest类实施以下测试策略:
- 操作系统进程调度管理
由于是面向单进程调度,因此应该符合NoOverlap规约,没有其他要
求,可以直接扩展NoOverlapMultiIntervalSet<Process>类,不需要额外操作。
- 大学课表管理
该应用允许同一时间点在不同地点上课,即同一段时间允许有多个interval存在。也允许有空白时间段(满课不就疯了吗),因此可以直接继承CommonMultiIntervalSet类,并增加一个getArrangementByWeek方法用于输出当前课表的安排。
针对三个应用情形,设计了三个类:
域用于表示各种的相关信息。对三个应用来说,其 L 分别应为“员工”(Employee)、“进程”(Process)、 “课程”(Course),所需关注的属性分别为:
- Employee:姓名、职务、手机号码
- Process:进程 ID、进程名称、最短执行时间、最长执行时间
- Course:课程 ID、课程名称、教师名字、地点
-
并重写toString方法,以输出各种相关的有用信息。L定义为不可变类型,其域全为final,并且不提供set方法。
-
在CommonMultiIntervalSet中编写public double Similarity(MultiIntervalSet<L> s2)方法,用于计算s2与自身的相似度。其原理是计算时间轴上所有标签相同且时间重叠部分的长度,与时间轴总长度之比,总长度按最长的时间轴算。
关键部分:
-
同样在CommonMultiIntervalSet中编写public double calcConflictRatio()方法,其原理与计算相似度基本相同,只是这里只考虑自身的重叠部分,并且标签可以不同。
-
关键部分:
在NoOverlapMultiIntervalSet类中编写public double calcFreeTimeRatio()方法,利用所有时间段长度之和,与时间轴长度之比作为空闲时间比例。时间轴长度按照最早时间段的开始时间到最晚时间段的结束时间计算。
实现代码:
利用上述设计和实现的ADT,实现手册里要求的各项功能。
针对排班管理系统,所需完成的功能为:
Step 1 设定排班开始日期、结束日期,具体到年月日即可。
这一步使用GregorianCalendar用于标记日期。
Step 2 增加一组员工,包括他们各自的姓名、职务、手机号码,并可随时删除某些员工。如果某个员工已经被编排进排班表,那么他不能被删除,必须将其排班信息删掉之后才能删除该员工。员工信息一旦设定则无法修改。
利用Employee类作为员工,同时将equals方法重写,当姓名相同时则判断为同一员工,并且在删除时利用contain方法判断待删除员工是否已被安排值班。员工类为不可变类,无法修改。
Step 3 可手工选择某个员工、某个时间段(以“日”为单位,最小 1 天,可以是多天),向排班表增加一条排班记录,该步骤可重复执行多次。在该过程中, 用户可随时检查当前排班是否已满(即所有时间段都已被安排了特定员工值班)、若未满,则展示给用户哪些时间段未安排、未安排的时间段占总时间段的比例。
通过输入员工姓名、开始时间和结束时间可以增加排班记录,利用DutyIntervalSet类中增加的checkDutySchedule方法检查是否已经完成排班。完成的定义是排班已满。利用calcFreeTimeRatio方法可以计算空闲比例,并展示未安排时间段。
Step 4 除了上一步骤中手工安排,也可采用自动编排的方法,随机生成排班表。
利用随机数自动编排,在APP类中设计即可。
Step 5 可视化展示任意时刻的排班表。可视化要直观明了,可自行设计。
重写toString方法,用于打印排班表信息。
针对操作系统的进程调度管理系统,所需完成的功能为:
Step 1 增加一组进程,输入每个进程的 ID、名称、最短执行时间、最长执行时间;进程一旦设定无法再修改其信息。
设计进程的Porcess类,为不可变类型。其域中包含了所有相关信息。
Step 2 当前时刻(设定为 0)启动模拟调度,随机选择某个尚未执行结束的进程在 CPU 上执行(执行过程中其他进程不能被执行),并在该进程最大时间之前的任意时刻停止执行,如果本次及其之前的累积执行时间已落到[最短执行时间,最长执行时间]的区间内,则该进程被设定为“执行结束”。重复上述过程, 直到所有进程都达到“执行结束”状态。在每次选择时,也可“不执行任何进程”,并在后续随机选定的时间点再次进行进程选择。
利用随机数模拟调度,当进程执行结束后从待执行列表删除该进程,并记录其执行时间。
Step 3 上一步骤是“随机选择进程”的模拟策略,还可以实现“最短进程优先”的模拟策略:每次选择进程的时候,优先选择距离其最大执行时间差距最小的进程。
通过在待完成调度进程列表中寻找距离其最大执行时间差距最小的进程,进行模拟调度。
Step 4 可视化展示当前时刻之前的进程调度结果,以及当前时刻正在执行的进程。
可视化的形式要直观明了,可自行设计。
重写toString方法,使可视化更直观人性化。
针对课表管理系统,所需完成的功能为:
Step 1 设定学期开始日期(年月日)和总周数(例如 18);
应用开始时设定总周数和开始日期,确定时间轴的开始与终止点。
Step 2 增加一组课程,每门课程的信息包括:课程 ID、课程名称、教师名字、地点、周学时数(偶数);
设计Course类,其域中包含了课程的相关信息。Course设计为不可变类。根据用户的输入信息来构造Course对象。
Step 3 手工选择某个课程、上课时间(只能是 8-10 时、10-12 时、13-15 时、15-17 时、19-21 时),为其安排一次课,每次课的时间长度为 2 小时;可重复安排,直到达到周学时数目时该课程不能再安排;
应用检查用户输入的时间是否符合要求,用户只需要输入开始时间,系统自动将结束时间定为两小时以后。
Step 4 上步骤过程中,随时可查看哪些课程没安排、当前每周的空闲时间比例、重复时间比例;
重复时间比例通过调用calcConflictRatio方法获取。维护一个所有课程的列表,一个课程已安排则进行标记,通过标记可以得到未安排的课程。
Step 5 因为课程是周期性的,用户可查看本学期内任意一周的课表结果。
在courseIntervalSet类中,增加getArrangementByWeek方法,用于输出结果的字符串。
根据提供的示例,编写Parser类用于读取正则表达式:
其主要部分代码如图所示。
排班应用:可以出现一个员工被安排多段值班的情况,例如张三的值班日期为(2021-01-01, 2021-01-10), (2021-02-01, 2021-02-06);
应用设计时额外增加了检查某员工是否已经安排值班的情形,删除该检查即可。代价很小。
-
-
- 变化2
-
课表应用:不管学生选课状况如何,不能够出现两门课排在同一时间的情况(即“无重叠”)
修改课表应用继承的类,改为继承NoOverlapMultiIntervalSet类即可。修改代价也很小。修改后的类图:
-
- Git仓库结构
请在完成全部实验要求之后,利用Git log指令或Git图形化客户端或GitHub上项目仓库的Insight页面,给出你的仓库到目前为止的Object Graph,尤其是区分清楚change分支和master分支所指向的位置。
(上传报告前的git仓库结构)
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
每次结束编程时,请向该表格中增加一行。不要事后胡乱填写。
不要嫌烦,该表格可帮助你汇总你在每个任务上付出的时间和精力,发现自己不擅长的任务,后续有意识的弥补。
日期 | 时间段 | 计划任务 | 实际完成情况 |
2021-6-28 | 14:00-18:00 | 设计ADT | 部分完成 |
2021-6-29 | 14:00-18:00 | 重新设计ADT | 完成 |
2021-6-30 | 14:00-17:00 | 编写测试方法、两种实现ADT | 完成 |
2021-7-1 | 14:00-18:00 | 编写进程调度应用 | 完成 |
2021-7-2 | 14:00-18:00 | 编写课表安排应用 | 完成 |
2021-7-2 | 20:00-23:00 | 编写值班表应用 | 完成 |
2021-7-3 | 10:00-12:00 | 编写change | 完成 |
2021-7-3 | 14:00-18:00 | 编写报告 | 完成 |
遇到的难点 | 解决途径 |
正则表达式完全不会用 | 翻Java编程思想解决 |
在初版ADT中,MultiIntervalSet难以与IntervalSet配合使用 | 重构,只用MultiIntervalSet作为ADT,在更高的层面上抽象。 |
Github难以push,网络经常有问题 | 学会了科学上网 |
编程过程中要先分析需求是什么,从顶层需求出发,总结各个应用的共性与特性操作,据此编写基类ADT和特性接口,以供复用,减少重复的工作量。
在编程过程中,可能时刻需要修改ADT。要确保ADT不会过于冗杂,给之后的编程带来很多麻烦。
编写可维护的代码十分重要!一不小心可能就成了“屎山”。
- 重新思考Lab2中的问题:面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?本实验设计的ADT在三个不同的应用场景下使用,你是否体会到复用的好处?
面向ADT编程,可复用性强,可以减少重复劳动,但思维量大,要思考如何最大化可复用性,比较各个应用的相同点与不同点。而面向应用场景编程则可以随性而为,不需要考虑可复用性的问题,但工作量大。
- 重新思考Lab2中的问题:为ADT撰写复杂的specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后的编程中坚持这么做?
Specification可以时刻提醒自己在干什么。这些工作是为了保护类的实现,不让用户获取内部实现方法,只给用户提供接口,避免用户依赖ADT的实现,发生耦合。
- 之前你将别人提供的API用于自己的程序开发中,本次实验你尝试着开发给别人使用的API,是否能够体会到其中的难处和乐趣?
难处一大堆,既要方便用户(其实是自己),又要防着用户(还是自己),乐趣。。呵呵,苦中作乐。
- 你之前在使用其他软件时,应该体会过输入各种命令向系统发出指令。本次实验你开发了一个解析器,使用语法和正则表达式去解析输入文件并据此构造对象。你对语法驱动编程有何感受?
难
- Lab1和Lab2的大部分工作都不是从0开始,而是基于他人给出的设计方案和初始代码。本次实验是你完全从0开始进行ADT的设计并用OOP实现,经过五周之后,你感觉“设计ADT”的难度主要体现在哪些地方?你是如何克服的?
一开始无从下手,不知道需要提供什么方法,不知道要做些什么。
必须先分析需求,抽象出所需要的操作,形成ADT。
- “抽象”是计算机科学的核心概念之一,也是ADT和OOP的精髓所在。本实验的五个应用既不能完全抽象为同一个ADT,也不是完全个性化,如何利用“接口、抽象类、类”三层体系以及接口的组合、类的继承、设计模式等技术完成最大程度的抽象和复用,你有什么经验教训?
相顾无言,惟有泪千行。脑袋里一定要有清晰的设计,不然在实际编程过程中就乱了。
- 关于本实验的工作量、难度、deadline。
工作量大,难度高,ddl恰好在计统考试后一周,有点紧,所幸临近期末没什么课,勉勉强强能完成。
- 到目前为止你对《软件构造》课程的评价
痛并快乐着。