2021年春季学期
计算学部《软件构造》课程
Lab 3实验报告
3.2 面向可复用性和可维护性的设计:IntervalSet<L>· 4
3.2.1 IntervalSet<L>的共性操作··· 4
3.2.3 面向各应用的IntervalSet子类型设计(个性化特征的设计方案)··· 5
3.3 面向可复用性和可维护性的设计:MultiIntervalSet<L>· 6
3.3.1 MultiIntervalSet<L>的共性操作··· 6
本次实验覆盖课程第 2、 3 章的内容,目标是编写具有可复用性和可维护性的软件,主要使用以下软件构造技术:
- 子类型、泛型、多态、重写、重载
- 继承、代理、组合
- 语法驱动的编程、 正则表达式
- API 设计、 API 复用
本次实验给定了三个具体应用(值班表管理、 操作系统进程调度管理、大学
课表管理),学生不是直接针对每个应用分别编程实现,而是通过 ADT 和泛型等抽象技术,开发一套可复用的 ADT 及其实现,充分考虑这些应用之间的相似性和差异性,使 ADT 有更大程度的复用(可复用性) 和更容易面向各种变化(可维护性)。
- 实验环境配置
简要陈述你配置本次实验所需环境的过程,必要时可以给出屏幕截图。
特别是要记录配置过程中遇到的问题和困难,以及如何解决的。
使用Eclipse完成本次实验,配置过程未遇到困难。
在这里给出你的GitHub Lab3仓库的URL地址(Lab3-学号)。
https://github.com/ComputerScienceHIT/HIT-Lab3-1190201826
请仔细对照实验手册,针对每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
简要介绍三个应用。
分析三个应用场景的异同,理解需求:它们在哪些方面有共性、哪些方面有差异。
(1) 值班表管理(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 |
… |
(2) 操作系统进程调度管理(ProcessSchedule): 考虑计算机上有一个单核CPU,多个进程被操作系统创建出来,它们被调度在 CPU 上执行, 由操
作系统决定在各个时段内执行哪个线程。操作系统可挂起某个正在执行
的进程,在后续时刻可以恢复执行被挂起的进程。 可知: 每个时间只能
有一个进程在执行,其他进程处于休眠状态;一个进程的执行被分为多
个时间段;在特定时刻, CPU 可以“闲置”,意即操作系统没有调度执行
任何进程;操作系统对进程的调度无规律,可看作是随机调度。
(3) 大学课表管理( CourseSchedule): 看一下你自己的课表,每一上午
10:00-12:00 和每周三上午 8:00-10:00 在正心楼 42 教室上“软件构造”课
程。 课程需要特定的教室和特定的教师。 在本应用中,我们对实际的课
表进行简化:针对某个班级,假设其各周的课表都是完全一样的(意即
同样的课程安排将以“周”为单位进行周期性的重复,直到学期结束);
一门课程每周可以出现 1 次,也可以安排多次(例如每周一和周三的“软
件构造”)且由同一位教师承担并在同样的教室进行;允许课表中有空白
时间段(未安排任何课程);考虑到不同学生的选课情况不同,同一个时
间段内可以安排不同的课程(例如周一上午 3-4 节的“计算方法” 和“软
件构造”); 一位教师也可以承担课表中的多门课程
共性:都是针对时间段进行操作,所以要考虑时间段的各种属性。
差异性:
- 是否允许时间轴上有空白。若“是”,则时间轴上某些时间段没有跟任何
标签关联;若“否”,则时间轴上的任何时间点都隶属于某个标签的时间
段。例如:在应用 1 中,不允许有空白;在应用 2 和应用 3 中,允许有
空白。 //一种直观的想法是在 ADT 的 RI 中将“有无空白”的约束定义
出来并在 checkRep()中加以检查,但这是不合理的: 该 ADT 是 mutable
的,其实例对象可不断调用 insert(...)向其中插入 interval,只有
到用户彻底插入结束之后才能够判断“有无空白”的约束是否被遵循,
而不是在每次 insert 之后均要判定。因此,对该约束的判定需要在具
体子类中表达: 当不再增加新的时间段时,检查时间轴上特定范围内是
否存有空白,以判断其是否合法。 - 是否允许不同的 interval 之间有重叠。若“允许”,则多个 interval
可能存在重叠,但具有相同 label 的多个 interval 之间仍不允许有重
叠;若“不允许”,则任何两个 interval 之间不会有任何重叠(即某个
时间点只能属于一个时间段)。例如:在应用 1 和应用 2 中,不允许有重
叠;在应用 3 中,允许有重叠。 //这个需要在 ADT 的 RI 和 checkRep()
中检查, 并根据“允许”或“不允许”, 从 ADT 中派生出不同的子类型。 - 是否包含周期性的时间段。周期性的时间段是指某个时间段按特定的周
期长度重复出现,例如应用 3 中以“一周”为单位重复某个课程,但应
用 1 和应用 2 中不存在这种情况。//可派生出子类型,针对每个时间段,
标识其重复与否。
-
- 面向可复用性和可维护性的设计:IntervalSet<L>
该节是本实验的核心部分。
-
-
- IntervalSet<L>的共性操作
-
/**
* Create an empty object:empty()
* @param <L> interval label, can be any immutable
* @return an empty IntervalSet
*/
public static<L> CommonIntervalSet<L> empty(){
return new CommonIntervalSet<L>();
}
/**
* Insert a label with interval
* @param start start time
* @param end end time (end > start)
* @param label the interval's label (not the same as other interval's label)
*/
public void insert(long start,long end,L label);
/**
* return the start time of the label's interval
* @param label the label you want get the start time
* @return the start time of the label's interval
*/
public long start (L label);
/**
* return the end time of the label's interval
* @param label the label you want get the end time
* @return the end time of the label's interval
*/
public long end (L label);
/**
* get the labels of this object
* @return the set of the labels
*/
public Set<L> labels();
/**
* remove the label's interval
* @param label the label you want remove
* @return if the object has this label return true, else false
*/
public boolean remove(L label);
/**
* return Appropriate form
* @return Appropriate form
*/
public String toString();
采用方案二,将针对不同特征取值的具体操作分别放在三个应用的子类中加以实现。例如:在实现排课和进程调度的子类中实现特殊的 insert 方法,其他应用的子类不需实现该方法。
该方法简单来说就是“一锅炖”,优点是思路清晰,目标明确,能更好的面向应用,缺点是:某些方法的代码可能是重复的、分散在多个类中, 可维护性和可复用性差,将来一旦面临变化就需要修改多处代码。
-
-
- 面向各应用的IntervalSet子类型设计(个性化特征的设计方案)
-
为了支持三个应用,你需要基于上一小节的某个设计方案分别构造/派生出更具体的、面向应用的 ADT 子类型,每个子类型可根据应用需要来选择是基于IntervalSet<L>派生还是基于 MultiIntervalSet<L>派生。
DutyIntervalSet:代表一个排班表;
ProcessIntervalSet:代表一个操作系统对进程的调度记录;
CourseIntervalSet:代表某个班级的特定课表。
针对从 IntervalSet<L>和 MultiIntervalSet<L>出发所派生出的所有ADT 子类型, 撰写 AF、 RI、 Safety from rep exposure,以及每个方法的 spec。 为它们设计和编写 JUnit 测试用例,并撰写 testing strategy。
最终得到的面向应用的ADT中:DutyIntervalSet继承了CommonIntervalSet类
测试结果如下:
-
- 面向可复用性和可维护性的设计:MultiIntervalSet<L>
- MultiIntervalSet<L>的共性操作
- 面向可复用性和可维护性的设计:MultiIntervalSet<L>
/**
* Create an empty object:empty()
* @param <L> interval label, can be any immutable
* @return an empty MultiIntervalSet
*/
public static<L> CommonMultiIntervalSet<L> empty1(){
return new CommonMultiIntervalSet<L>();
}
/**
* Insert a label with interval
* @param start start time
* @param end end time (end > start)
* @param label the interval's label (can be the same as other interval's label)
*/
public void insert(long start, long end, L label);
public long start(L label);
public long end(L label);
/**
* get the labels of this object
* @return the set of the labels
*/
public Set<L> labels();
/**
* remove the label's intervals
* @param label the label you want remove
* @return if the object has this label return true, else false
*/
public boolean remove(L label);
/**
* get all intervals of this object's label
* IntervalSet<Integer> intervals(L label), the return's form is
* IntervalSet<Integer>, and the returned intervals is from small to large
* eg: if { "A"=[[0,10],[10,20]], "B"=
* [[10,20]] }, then intervals("A") should return { 0=[0,10],1=[10,20] }.
* @param label the label you want get the intervals
* @return the labels' all intervals
*/
IntervalSet<Integer> intervals(L label);
/**
* return Appropriate form
* @return Appropriate form
*/
public String toString();
采用方案二,将针对不同特征取值的具体操作分别放在三个应用的子类中加以实现。例如:在实现排课和进程调度的子类中实现特殊的 insert 方法,其他应用的子类不需实现该方法。
该方法简单来说就是“一锅炖”,优点是思路清晰,目标明确,能更好的面向应用,缺点是:某些方法的代码可能是重复的、分散在多个类中, 可维护性和可复用性差,将来一旦面临变化就需要修改多处代码。
-
-
- 面向各应用的MultiIntervalSet子类型设计(个性化特征的设计方案)
-
为了支持三个应用,你需要基于上一小节的某个设计方案分别构造/派生出更具体的、面向应用的 ADT 子类型,每个子类型可根据应用需要来选择是基于IntervalSet<L>派生还是基于 MultiIntervalSet<L>派生。
DutyIntervalSet:代表一个排班表;
ProcessIntervalSet:代表一个操作系统对进程的调度记录;
CourseIntervalSet:代表某个班级的特定课表。
针对从 IntervalSet<L>和 MultiIntervalSet<L>出发所派生出的所有ADT 子类型, 撰写 AF、 RI、 Safety from rep exposure,以及每个方法的 spec。 为它们设计和编写 JUnit 测试用例,并撰写 testing strategy。
最终得到的面向应用的ADT中:ProcessMultiIntervalSet、 CourseSchedule、ChangedDutyIntervalSet、ChangedCourseSchcdul继承了CommonMultiIntervalSet类。
测试结果如下:
对三个应用来说,其 L 分别应为“员工”(Employee)、“进程”(Process)、
“课程”(Course), 所需关注的属性分别为:
- Employee:姓名、职务、手机号码
- Process:进程 ID、进程名称、 最短执行时间、最长执行时间
Course: 课程 ID、课程名称、 教师名字、地点
计算两个 MultiIntervalSet 对象的相似度:
double Similarity(MultiIntervalSet<L> s1, MultiInterval Set<L> s2)具体计算方法: 按照时间轴从早到晚的次序, 针对同一个时间段内两个对象里的 interval,若它们标注的 label 等价,则二者相似度为 1,否则为 0;若同一时间段内只有一个对象有 interval 或二者都没有,则相似度为 0。 将各interval 的相似度与 interval 的长度相乘后求和,除以总长度,即得到二者的整体相似度。
针对上图中的两个对象:
{ A = [[0,5],[20,25]], B = [[10,20],[25,30]] }
{ A = [[20,35]], B = [[10,20]], C = [[0,5]] }
它们的相似度计算如下:
( 5×0 + 5×0 + 10×1 + 5×1 + 5×0 + 5×0 ) ÷ ( 35 - 0 ) = 15 /
35 ≈ 0.42857
在CommonMultiIntervalSet的ADT设计里,我们计算了相似度,部分代码如下:
发现一个 IntervalSet<L>或 MultiIntervalSet<L>对象中的时间冲
突比例(仅针对应用 3)
double calcConflictRatio(IntervalSet<L> set)
double calcConflictRatio(MultiIntervalSet<L> set)
所谓的“冲突”,是指同一个时间段内安排了两个不同的 interval 对象。 用发生冲突的时间段总长度除于总长度,得到冲突比例,是一个[0,1]之间的值
具体应用Courseschedule的ADT设计里,我们计算了相似度,部分代码如下:
计算一个 IntervalSet<L>或 MultiIntervalSet<L>对象中的空闲时
间比例
double calcFreeTimeRatio(IntervalSet<L> set)
double calcFreeTimeRatio(MultiIntervalSet<L> set)
所谓的“空闲”,是指某时间段内没有安排任何 interval 对象。用空闲的时间段总长度除于总长度,得到空闲比例,是一个[0,1]之间的值
具体应用Courseschedule的ADT设计里,我们计算了相似度,部分代码如下:
在为DutyIntervalSet计算空闲时间比例时,我们放到了应用中具体实现。
测试结果如下:
-
-
- 应用设计与开发
-
利用上述设计和实现的ADT,实现手册里要求的各项功能。
针对排班管理系统,所需完成的功能为:
Step 1 设定排班开始日期、结束日期,具体到年月日即可。
Step 2 增加一组员工,包括他们各自的名字、职务、手机号码,并可随时删
除某些员工。 如果某个员工已经被编排进排班表,那么他不能被删除,必须将其
排班信息删掉之后才能删除该员工。 员工信息一旦设定则无法修改。
Step 3 可手工选择某个员工、某个时间段(以“日” 为单位,最小 1 天,可
以是多天),向排班表增加一条排班记录, 该步骤可重复执行多次。在该过程中,
用户可随时检查当前排班是否已满(即所有时间段都已被安排了特定员工值班)、
若未满,则展示给用户哪些时间段未安排、未安排的时间段占总时间段的比例。
Step 4 除了上一步骤中手工安排,也可采用自动编排的方法,随机生成排班
表。
Step 5 可视化展示任意时刻的排班表。 可视化要直观明了,可自行设计。
下面演示排班流程:
- 设定排班表起止时间
- 增加员工
- 添加排班信息
可以发现,若是排班信息有空隙,会提示继续排班并显示空闲时间比例。
4.查看当前排班表
- 删除员工
可以发现,若是员工有排班信息,不能删除他,必须要先删掉排班信息,才能删除员工,这与实验指导书的要求是相贴切的。
- 随机生成排班表
针对操作系统的进程调度管理系统,所需完成的功能为:
Step 1 增加一组进程,输入每个进程的 ID、名称、最短执行时间、最长执
行时间;进程一旦设定无法再修改其信息。
Step 2 当前时刻(设定为 0)启动模拟调度,随机选择某个尚未执行结束的
进程在 CPU 上执行(执行过程中其他进程不能被执行),并在该进程最大时间之
前的任意时刻停止执行,如果本次及其之前的累积执行时间已落到[最短执行时
间, 最长执行时间]的区间内,则该进程被设定为“执行结束”。重复上述过程,
直到所有进程都达到“执行结束”状态。 在每次选择时,也可“不执行任何进程”,
并在后续随机选定的时间点再次进行进程选择。
Step 3 上一步骤是“随机选择进程”的模拟策略,还可以实现“最短进程优
先”的模拟策略: 每次选择进程的时候,优先选择距离其最大执行时间差距最小
的进程。
Step 4 可视化展示当前时刻之前的进程调度结果,以及当前时刻正在执行
的进程。可视化的形式要直观明了,可自行设计。
下面演示操作系统的进程调度管理系统运行流程:
- 添加进程信息
- 执行随机选取进程的策略
- 执行最短进程优先的策略
针对课表管理系统,所需完成的功能为:
Step 1 设定学期开始日期(年月日) 和总周数(例如 18);
Step 2 增加一组课程,每门课程的信息包括: 课程 ID、课程名称、教师名
字、地点、周学时数(偶数);
Step 3 手工选择某个课程、上课时间(只能是 8-10 时、 10-12 时、 13-15 时、
15-17 时、 19-21 时),为其安排一次课,每次课的时间长度为 2 小时;可重复安
排,直到达到周学时数目时该课程不能再安排;
Step 4 上步骤过程中,随时可查看哪些课程没安排、当前每周的空闲时间比
例、重复时间比例;
Step 5 因为课程是周期性的,用户可查看本学期内任意一天的课表结果。
下面展示课表管理系统的操作流程:
1.设定学期初始信息
2.添加课程
3.为课程安排上课时间
- 查看某天课表
- 查看未安排的课程及空闲时间重复时间信息
针对 “值班表管理”应用,为其扩展一个功能:从一个外部文本文件读入数据并使用正则表达式 parser 对其进行解析,从中抽取信息,构造 值班表对象。输入文件的语法示例如下所示,你可从以下地址下载示例文件用于 你的程序测试: https://github.com/rainywang/Spring2021_HITCS_SC_Lab3 在以下示例中,不带有下划线的文字部分为固定说明部分,带有下划线的文 字部分为可变化信息。“//”后续文字是解释说明,并非语法的构成部分。Lab Manuals Lab-3 Reusability and Maintainability oriented Software Construction 文件里描述的 Employee、Period、Roster 的次序是不确定的,违反该次序不 能被看作非法。 在读取文件过程中,若发现有违反本例中解释说明部分给出的规则,则该文 件不符合语法,需结束读取并提示用户选择其他合法的文件。 另外,请忽略文件中的所有缩进,它并非语法的组成部分。
其实相较于旧的Duty任务,就是加了个对文件的处理,我们先通过parse取出Period或者Roster或者Period,之后再讨论情况,若是Employee类型,取出姓名职位电话,如果是Period,取出开始日期,结束日期。
正常读入:
不正常读入:
手工输入的话与老Duty一样,在这就不演示了。
可以应对变化。
应对:通过将面向应用的ADT ChangedDutyIntervalSet继承CommonMultiIntervalSet类实现一对多的情况,同时,在APP中通过两个二维数组来存储一个Employee所对应的多个时间段,来实现与之前相同的功能。
代价:总共修改代码不到10行,只是思考的时间成本较久一些,可以接受。
-
-
- 变化2
-
新的课表应用:不管学生选课状况如何,不能够出现两门课排在同一时间的
情况(即“无重叠”)
可以应对变化。
应对:通过改写面向应用的ADT ChangedCourceSchedule来实现对无重叠的要求,具体的就是新增一个标记位,标记每个时间段是否被某课程占用,之后增加对标记位的检查,如果其大于0,则报错提示不能选择这个时间段。
代价:总共修改代码不到一百行,思路清晰,时间成本低,可以接受。
-
- Git仓库结构
请在完成全部实验要求之后,利用Git log指令或Git图形化客户端或GitHub上项目仓库的Insight页面,给出你的仓库到目前为止的Object Graph,尤其是区分清楚change分支和master分支所指向的位置。
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
每次结束编程时,请向该表格中增加一行。不要事后胡乱填写。
不要嫌烦,该表格可帮助你汇总你在每个任务上付出的时间和精力,发现自己不擅长的任务,后续有意识的弥补。
日期 | 时间段 | 计划任务 | 实际完成情况 |
2021/6/28 | 9:00-23:00 | 完成底层Interval和MultiInterval | 基本完成 |
2021/6/30 | 9:00-23:00 | 完成各面向应用的ADT | 基本完成 |
2021/7/2 | 9:00-23:00 | 完成APP以及复用的实现 | 全部完成 |
2021/7/3 | 9:00-23:00 | 完成change | 全部完成 |
遇到的难点 | 解决途径 |
Git切换分支时误操作导致分支覆盖让我以为丢失了文件 | 通过git reset --hard 版本号 进行版本回退找回丢失文件 |
在设计changed中值班表问题时,想了很久应该怎么让一个标签对应多个时间段 | 通过设置两个全局数组存储开始与结束时间 |
继承树设计需要具有合理性
APP要人性化同时保证其正确性
- 重新思考Lab2中的问题:面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?本实验设计的ADT在三个不同的应用场景下使用,你是否体会到复用的好处?
差异:面向ADT的编程:适合大规模的工程性质的项目;直接面向应用场景的编程:适合简单的小规模的任务
复用的好处:减少代码量
- 重新思考Lab2中的问题:为ADT撰写复杂的specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后的编程中坚持这么做?
意义:对每个ADT进行限制,使其更加安全。
我愿意在以后的编程中坚持这么做。
- 之前你将别人提供的API用于自己的程序开发中,本次实验你尝试着开发给别人使用的API,是否能够体会到其中的难处和乐趣?
要考虑更多东西,感受到创造底层ADT的乐趣
- 你之前在使用其他软件时,应该体会过输入各种命令向系统发出指令。本次实验你开发了一个解析器,使用语法和正则表达式去解析输入文件并据此构造对象。你对语法驱动编程有何感受?
只需要使用几个正则表达式就可以帮助我们进行分析匹配我们想要的文本,感受到了解析器的厉害之处。
- Lab1和Lab2的大部分工作都不是从0开始,而是基于他人给出的设计方案和初始代码。本次实验是你完全从0开始进行ADT的设计并用OOP实现,经过五周之后,你感觉“设计ADT”的难度主要体现在哪些地方?你是如何克服的?
抽象一个具体功能是有难度的同时需要考虑这个功能的复用性以及功能与功能的衔接,设计APP前要对ADT进行自己的检查验证。
- “抽象”是计算机科学的核心概念之一,也是ADT和OOP的精髓所在。本实验的五个应用既不能完全抽象为同一个ADT,也不是完全个性化,如何利用“接口、抽象类、类”三层体系以及接口的组合、类的继承、设计模式等技术完成最大程度的抽象和复用,你有什么经验教训?
通过对继承树的研究并思考,来进行合理的委派与修饰,同时,设想好每个ADT实现的功能,根据功能来针对性设计数据结构。
- 关于本实验的工作量、难度、deadline。
工作量很大,难度很大,ddl还可以
- 到目前为止你对《软件构造》课程的评价。
学习到了很多的代码理念和设计模式,很不错!