本次实验给定了三个具体应用(值班表管理、操作系统进程调度管理、大学课表管理),学生不是直接针对每个应用分别编程实现,而是通过 ADT 和泛型等抽象技术,开发一套可复用的 ADT 及其实现,充分考虑这些应用之间的相似性和差异性,使 ADT 有更大程度的复用(可复用性)和更容易面向各种变化(可维护性)。
待开发的三个应用场景
值班表管理(DutyRoster):一个单位有 n 个员工,在某个时间段内(例如寒假 1 月 10 日到 3 月 6 日期间),每天只能安排唯一一个员工在单位值班,且不能出现某天无人值班的情况;每个员工若被安排值班 m 天(m>1),那么需要安排在连续的 m 天内。值班表内需要记录员工的名字、职位、手机号码,以便于外界联系值班员。
操作系统进程调度管理(ProcessSchedule):考虑计算机上有一个单核CPU,多个进程被操作系统创建出来,它们被调度在 CPU 上执行,由操作系统决定在各个时段内执行哪个进程。操作系统可挂起某个正在执行的进程,在后续时刻可以恢复执行被挂起的进程。可知:每个时间只能有一个进程在执行,其他进程处于休眠状态;一个进程的执行被分为多个时间段;在特定时刻,CPU 可以“闲置”,意即操作系统没有调度执行任何进程;操作系统对进程的调度无规律,可看作是随机调度。
大学课表管理(CourseSchedule):看一下你自己的课表,每周一上午8:00-10:00 和每周三上午 8:00-10:00 在正心楼 209 教室上“软件构造”课程。课程需要特定的教室和特定的教师。在本应用中,我们对实际的课表进行简化:针对某个班级,假设其各周的课表都是完全一样的(意即同样的课程安排将以“周”为单位进行周期性的重复,直到学期结束);一门课程每周可以出现 1 次,也可以安排多次(例如每周一和周三的“软件构造”课)且由同一位教师承担并在同样的教室进行;允许课表中有空白时间段(未安排任何课程);考虑到不同学生的选课情况不同,同一个时间段内可以安排不同的课程;一位教师也可以承担课表中的多门课程。
ADT共性提取:都是将不同对象绑定到不同时间段
差异:
-
值班表管理:每个时间段只能安排一个对象,且该对象只能对应一个连续的时间段,不能出现空闲时间段。
-
操作系统进程调度管理:每个时间段只能安排一个对象,但该对象可以对应多个不连续的时间段,可以出现空闲时间段。
-
大学课表管理:每个时间段可以安排多个对象,且一个对象可以对应多个时间段,可以出现空白时间段,包含周期性的时间段。
ADT设计——IntervalSet<L>
共性操作
IntervalSet<L>是一个 mutable 的 ADT,描述了一组在时间轴上分布的“时间段”(interval),每个时间段附着一个特定的标签,标签不重复且是 immutable的。
- 创建一个空对象: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()
共性操作实现类
CommonIntervalSet<L>是IntervalSet<L>的实现类,实现了局部共性特征
rep:
private final Map<L, Interval> intervals = new HashMap<>();
private final Set<L> labels = new HashSet<>();
//Abstraction function:
// AF(intervals) = IntervalSet中不同标签对应的时间段
// AF(labels) = IntervalSet中的所有标签
//Representation invariant:
// 每个标签只能绑定到唯一的时间段
// 时间段有且仅有一个起点,一个终点
// 时间段起点对应时刻小于等于终点且大于等于0
//Safety from rep exposure:
// 成员变量均用private final修饰,防止其被外部修改
// 在涉及返回内部变量时,采用防御性拷贝的方式,return一个新的变量
内部类——Interval,表示为一个时间段
/**
* 内部类表示一个时间段。
*/
private static class Interval {
final long start;
final long end;
Interval(long start, long end) {
this.start = start;
this.end = end;
}
}
checkrep:
private void checkRep() {
assert labels.size() == intervals.size(); // 确保标签不重复
for (Interval interval : intervals.values()) {
assert interval.start >= 0;
assert interval.end >= 0;
assert interval.start <= interval.end;
}
}
方法实现:
-
empty() 该静态方法在抽象类中已实现
-
void insert(long start, long end, L label) 先判断输入是否合法,然后将标签和新建的Interval对象以键值对的形式加入intervals中,将新标签加入labels中,最后checkrep
-
Set<L> labels() 使用防御式拷贝的形式返回一个新的HashSet
-
boolean remove(L label) 先判断intervals.remove(label)是否为空,若不为空,则将labels中的该标签也删除
-
long start (L label) 先执行Interval interval = intervals.get(label),若可以找到,则返回interval.start,若找不到,则报错
-
long end (L label) 与start方法同理
-
boolean isEmpty()
public boolean isEmpty() {
return intervals.isEmpty() && labels.isEmpty();
}
- String toString()
public String toString() {
StringBuilder re = new StringBuilder();
for (Map.Entry<L, Interval> entry : intervals.entrySet()) {
String temp = "标签:" + entry.getKey() + "; " + "时间段:" + entry.getValue().start + "——" + entry.getValue().end + "\n";
re.append(temp);
}
checkRep();
return re.toString();
}
面向各应用的IntervalSet子类型设计
本实验采用方案6——decorator设计模式进行设计,将 CommonIntervalSet 看作是原始的、未被装饰的对象,将“是否允许时间轴上有空白”、“是否允许不同的interval之间有重叠”这两个维度看作是两种“装饰”(每个维度的不同特征取值可以产生不同的“装饰”效果)。不仅可以避免单纯使用 inheritance 和 delegation造成的组合爆炸和维护复杂,还可以使得功能组合灵活多变,装饰类和被装饰类独立维护,降低耦合性。
装饰类基类为IntervalSetDecorator<L>,其实例变量为:
protected final IntervalSet<L> intervalSet;
其构造方法需传入IntercalSet实例
public IntervalSetDecorator(IntervalSet<L> decoratedSet) {
this.intervalSet = decoratedSet;
}
其具体方法实现直接调用实例变量对应的方法即可,举一例:
@Override
public void insert(long start, long end, L label) throws IllegalArgumentException {
intervalSet.insert(start, end, label);
}
对于面向各应用的IntervalSet子类型,设计了两个子装饰类
- NoBlankIntervalSetDecorator<L> 不允许有空白装饰类
其定义了一个方法为public boolean checkFullCoverage(),用于检查是否存在空白,若没有空白,返回true,否则返回false
实现方法:首先定义一个变量
Map<Long, Long> map = new HashMap<>();
用于存储所有的时间段,然后使用迭代器遍历labels标签集合,找到时间点的最大最小值,随后从最小值开始,以步长为1遍历每个键值对,若每次都能遍历到,则没有空白,若存在一次未遍历到,则有空白。
- NonOverlapIntervalSetDecorator<L> 不允许时间段重叠装饰类
其定义了一个checkRep()方法并重写了一个insert方法,用于保证不存在重叠时间段
insert实现方法:在insert的最后加上checkRep()
checkRep实现方法:首先定义一个变量
List<Map.Entry<Long, Long>> intervalList = new ArrayList<>();
这是一个存储所有时间段键值对的列表
随后遍历labels标签集合,获取每一个时间段,将所有时间段存储在该列表中,然后按每一个时间段的键值将列表排序,最后,根据前一个键值对的值是否大于后一个键值对的键进行是否重叠的判断,若大于,则发出重叠报错
ADT设计——MultiIntervalSet<L>
共性操作
MultiIntervalSet<L>较IntervalSet<L>的区别为同一个标签对象 L 可被绑定到多个时间段上,因此其抽象方法与IntervalSet<L>基本相同,区别在于MultiIntervalSet<L>没有start和end方法,而是使用
IntervalSet<Integer> intervals(L label)
这一方法来返回所有该标签关联的时间段
共性操作实现类
CommonMultiIntervalSet<L>是MultiIntervalSet<L>的实现类,实现了局部共性特征
rep:
private final Map<L, IntervalSet<Integer>> map = new HashMap<>();
// Abstraction function:
// AF(map) = 标签与其关联的时间段构成的键值对
// Representation invariant:
// 每个标签只能绑定到唯一的时间段
// 时间段有且仅有一个起点,一个终点
// 时间段起点对应时刻小于等于终点且大于等于0
// Safety from rep exposure:
// 所有成员均为 private
// 返回集合或对象时,使用防御性拷贝
该rep的设计思路为:
将每一个标签以及它所对应的所有时间段以键值对的形式存入map中。每一个时间段的标签为其被添加到IntervalSet<Integer>中的序号,第一个加入的为0,依次类推。每一个时间段的值为时间范围。
checkrep:
private void checkRep() {
for (IntervalSet<Integer> intervals : map.values()) {
Set<Integer> labels = intervals.labels();
for (Integer i : labels) {
long start = intervals.start(i);
long end = intervals.end(i);
assert start >= 0 && end >= 0 && start <= end;
}
}
}
CommonIntervalSet中的方法即按照其父类的方法依次实现:
- empty() 该静态方法在抽象类中已实现
- MultiIntervalSet(IntervalSet<L> initial) 直接返回新的CommonMultiIntervalSet
- void insert(long start, long end, L label)
首先,使用
IntervalSet<Integer> intervals = map.computeIfAbsent(label, k -> new CommonIntervalSet<>());
找到map中响应的标签对应的时间段,若没找到,则新建一个新的
然后,需要判断要插入的时间段和已有的时间段是否有重复,因为要求具有相同 label 的多个 interval 之间仍不允许有重叠
// 检查是否与现有的时间段重叠
for (Integer index : intervals.labels()) {
long existingStart = intervals.start(index);
long existingEnd = intervals.end(index);
// 检查新时间段是否与现有时间段重叠
if ((start < existingEnd) && (end > existingStart)) {
throw new IllegalArgumentException("Overlap detected with existing interval for the same label.");
}
}
最后,直接使用intervals中的insert方法插入即可,插入的标签为intervals.labels().size()
- Set<L> labels()
返回map.keySet(),并采用防御式拷贝新建一个新集合返回
- boolean remove(L label)
直接使用map.remove(label)移出即可
- IntervalSet<Integer> intervals(L label)
使用map.get(label)得到其对应的时间段对象,然后采用防御式拷贝的方式得到一个新对象返回
- String toString()
StringBuilder result = new StringBuilder();
for (Map.Entry<L, IntervalSet<Integer>> entry : map.entrySet()) {
result.append("Label: ").append(entry.getKey()).append(", Intervals: ").append(entry.getValue());
}
checkRep();
return result.toString();
面向各应用的MultiIntervalSet子类型设计
该子类型仍采用decorator设计模式进行设计,将 CommonIntervalSet 看作是原始的、未被装饰的对象,将是否允许时间轴上有空白、是否允许不同的 interval 之间有重叠、是否包含周期性的时间段这三个维度看作是三种“装饰”(每个维度的不同特征取值可以产生不同的“装饰”效果)。不仅可以避免单纯使用 inheritance 和 delegation造成的组合爆炸和维护复杂,还可以使得功能组合灵活多变,装饰类和被装饰类独立维护,降低耦合性。
装饰类基类为MultiIntervalSetDecorator<L>,其实例变量为:
protected final MultiIntervalSet<L> decoratedSet;
其构造方法需传入MultiIntervalSet实例
public MultiIntervalSetDecorator(MultiIntervalSet<L> decoratedSet) {
this.decoratedSet = decoratedSet;
}
其具体方法实现直接调用实例变量对应的方法即可,举一例:
@Override
public IntervalSet<Integer> intervals(L label) {
return decoratedSet.intervals(label);
}
对于面向各应用的IntervalSet子类型,设计了三个子装饰类
- NoBlankMultiIntervalSetDecorator<L> 不允许有空白装饰类
其定义了一个方法为public boolean checkFullCoverage(),用于检查是否存在空白,若没有空白,返回true,否则返回false
实现方法:首先定义一个变量
List<Map.Entry<Long, Long>> intervals = new ArrayList<>();
用于存储所有的时间段,然后使用两个for循环语句收集所有标签的所有时间段,存入intervals中,随后按开始时间对这些时间段进行排序,最后,从第一个时间段开始,依次将前一个时间段的末尾与后一个时间段的开头比较,若末尾小于开头,则存在空白
- NoOverlapMultiIntervalSetDecorator<L> 不允许时间段重叠装饰类
其定义了一个checkRep用于检查是否存在时间段的重叠,然后在父类原有insert方法的基础上加上了checkRep
checkRep实现方法:
与NoBlankMultiIntervalSetDecorator<L>中定义的方法类似,先将所有时间段取出到一个列表中,排序,然后比较上一个时间段的末尾和下一个时间段的开头,若末尾大于开头,则有重叠
- PeriodicMultiIntervalSetDecorator<L> 是否包含周期性的时间段
其rep为:
private final long period;
// AF(period) = 周期性时间段插入装饰器
// RI: period > 0
// Safety from rep exposure: all fields are private and final
定义其checkRep为:
private void checkRep() {
if (period <= 0) {
throw new IllegalStateException("周期数必须为正数");
}
}
重写了insert方法,加入循环周期插入操作
long currentStart = start;
long currentEnd = end;
// 按照周期性时间段插入
while (currentEnd < 100) {
super.insert(currentStart, currentEnd, label);
currentStart += period;
currentEnd += period;
}
注:该博客仅将实验三中最重要的部分——ADT的设计,进行了分析总结。