软件构造课程心得——软件构造实验三(Lab3)
1 实验目标概述
本次实验覆盖课程第3、4、5章的内容,目标是编写具有可复用性和可维护 性的软件,主要使用以下软件构造技术:
子类型、泛型、多态、重写、重载
继承、代理、组合
常见的OO设计模式
语法驱动的编程、正则表达式
基于状态的编程
API设计、API复用
本次实验给定了五个具体应用(高铁车次管理、航班管理、操作系统进程管 理、大学课表管理、学习活动日程管理),学生不是直接针对五个应用分别编程 实现,而是通过 ADT 和泛型等抽象技术,开发一套可复用的ADT及其实现,充分考虑这些应用之间的相似性和差异性,使ADT有更大程度的复用(可复用性)和更容易面向各种变化(可维护性)。
2 实验环境配置
实验环境与之前相同,不再详细叙述。
3 实验过程
3.1 待开发的三个应用场景
列出你所选定的三个应用。
1、航班管理
2、高铁车次管理
3、大学课程表管理
分析三个应用场景的异同,理解需求:它们在哪些方面有共性、哪些方面有差异。
经过分析,我们可以看到,这三个应用的状态改变有共性的方面:创建、启动、结束、分配资源、取消等,我们可以考虑将这些改变状态的方法放在同一个类中,共三种应用场景复用。在资源方面,航班管理和大学课程表管理都是单一资源,而高铁车次管理则是多个有次序的资源,可以考虑实现两个有关资源的接口分别对应这两种不同需求。物理位置上,这三种应用场景都不相同,航班管理有分别是起始地点和终点的两个地点,高铁车次管理除了始发地和终点之外还有一系列中间经停的地点,而大学课程表管理则只需一个地点,我们需要对这不同的三种情况做处理。时间属性上,航班管理和大学课程表管理均只需要一对起止时间对,而高铁车次管理则需要一系列的起止时间对用于中间的阻塞过程。
3.2 面向可复用性和可维护性的设计:PlanningEntry
3.2.1 PlanningEntry的共性操作
PlanningEntry是一个接口,定义了对通用的状态进行改变、获取计划项的名称以及获取计划项的状态的方法。这些方法的具体实现在其实现类CommonPlanningEntry中,其中CommonPlanningEntry的rep有两个,分别为状态state(为之后要提到的EntryState类型的对象)以及String类型的name。三个应用场景公用的改变类型的allocate、start、cancel、end等方法和获取计划项名称的方法可以直接在这个实现类中实现,而其余各个计划项有所区别的获取状态信息以及作为排序依据的compareTo方法则作为抽象方法,由具体的应用场景子类来具体实现。
3.2.2 局部共性特征的设计方案
三个具体应用场景的局部共性特征有计划项名称——均为String类型、状态——均为EntryState的子类型,因此,我们需要将这些特征值相关的内容放在共性的CommonPlanningEntry中,对于除了有关于block状态转换的方法,都在共性的类中进行实现,从而降低开发的工程量,提高代码的复用率。
3.2.3 面向各应用的PlanningEntry子类型设计(个性化特征的设计方案)
这一部分将针对三个应用场景分别进行描述,前面提到计划项有三个核心组成,分别为地点、时间和资源,所以总的设计思路就是为这三项内容分别实现能够满足不同需求的接口,实现具体Entry类时,除了继承CommonPlanningEntry这一共性方法外,再调用符合自己特征的接口,从而完成Entry子类的组装。
FlightEntry:
根据之前的分析,Flight的位置特征为两个位置,时间特征为一个起止时间对,资源特征为单一资源。因此需要设计的三个接口分别为:双位置接口、不可阻塞时间接口、单一资源接口,在这三个接口中定义需要的对三个特征的方法,再定义这三个接口的实现类,实现这三个接口。为了使代码清晰整洁容易理解,将这三个接口封装在一个总的接口FlightPlanningEntry中。完成了以上准备工作之后实现FlightEntry,FlightEntry的数据域为三个接口的具体实现类,有关时间、地点、资源的操作均委托给接口实现类来完成,compareTo根据Flight的起始时间来完成,getPresentStateName则由state的再flight中的具体含义来完成。
TrainEntry:
TrainEntry的实现与FlightEntry的实现类似,需要完成的三个接口为:多位置接口、可阻塞时间接口、多个有序资源接口。唯一需要注意的是,由于TrainEntry可阻塞,因此block和restart方法需要在TrainEntry中实现,用以使TrainEntry的状态可以改变为BLOCKED。
CourseEntry:
CourseEntry需要的三个接口为:单一可变位置接口、单一资源接口、不可阻塞时间接口,其中方法的实现也与上述应用类的方法实现类似,不再赘述。
3.3 面向复用的设计:R
以上三个具体应用类用到的资源R分别为:飞机资源、车厢资源、教师资源。下面分别进行详述。
飞机资源(Plane):
飞机资源有飞机编号、飞机机型、座位数、机龄等特征。在Plane类中实现getter方法获得这些信息,同时重写hashCode和equals方法。Plane的数据如下所示:
private String planeID;
private String planeType;
private int seatNumber;
private float planeAge;
车厢资源(Carriage):
车厢资源有车厢编号、类型、定员数、出厂年份等属性。在Carriage类中实现getter方法来获得这些信息,同时重写hashCode和equals方法。Carriage的数据如下所示:
private String carriageID;
private String carriageType;
private int seatNumber;
private int productYear;
教师资源(Teacher):
教师资源有身份证号、姓名、性别、职称等属性。在Teacher中实现getter方法来获得这些信息,同时重写hashCode和equals方法。Teacher的数据如下所示:
private String teacherID;
private String name;
private String gender;
private String position;
3.4 面向复用的设计:Location
一个“位置”对象的属性包括:经度、纬度、名称、是否可共享使用,同时由于一些地点没有经纬度信息,或者在一些应用场景下经纬度信息不重要,因此我们为Location提供两套构造方法,一种是需要提供全部的属性包括经纬度、名称和是否可共享,另一种则只需提供名称和是否可共享即可,在这种情况下,构造器会默认将经纬度全部设为0。
3.5 面向复用的设计:Timeslot
Timeslot是一个带有起始时间和结束时间的ADT,因此我们在Timeslot中会放两个LocalDateTime的数据来表示起始时间和终止时间。选取LocalDateTime是由于这个类是immutable的,使用时无需考虑数据被恶意篡改。为了后面使用及测试的方便,Timeslot也提供两种构造方法,一种是直接将两组年月日小时分钟信息传入,另一种是传入两个LocalDateTime类型的数据。
3.6 面向复用的设计:EntryState及State设计模式
EntryState是一个定义了状态类所有方法的接口,其他所有的状态均是EntryState的实现类。
其中共有6个状态,分别如下:
WAITING:已创建具体计划项,等待分配资源
ALLOCATED:已分配资源但未开始
RUNNING:计划项正在执行中
BLOCKED:计划项已阻塞
CANCELLED:计划项已取消
ENDED:计划项已运行结束
每一个状态类型中都有方法来改变状态,在满足条件的情况下对状态进行转换。具体的转换方式如下:
3.7 面向应用的设计:Board
为了将数据和表示相分离,并更大程度上对代码进行复用,设计了Data类来存储计划项,关于数据的操作均通过Data类来完成,而Board则只负责数据的可视化。由于在getPlan等操作上各计划项仍然有细微的差别,因此选择将个性化的方法放在具体的子类CourseData、FlightData、TrainData中,实际Board中使用的数据在这些子类中。在可视化表格前需要对数据进行处理,以FlightBoard为例。
更新计划项:
public void updateState() {
for(FlightEntry fe : flightdata) {
if(fe.getStartTime().isAfter(presentTime)) { }
else {
fe.start();
if(fe.getEndTime().isBefore(presentTime)) {
fe.end();
}
}
}
}
整合数据到表格:
public void generateDataToTable() {
updateState();
DateTimeFormatter df = DateTimeFormatter.ofPattern("HH:mm");
Duration duration;
for(FlightEntry fe : flightdata) {
if(fe.getEndLoc().equals(presentLoc)) {
duration = Duration.between(fe.getEndTime(), presentTime);
if(Math.abs(duration.toHours()) < 1) {
Vector<String> row = new Vector<String>();
row.add(df.format(fe.getEndTime()));
row.add(fe.getPlanningName());
row.add(fe.getStartLoc().getLocName() + "-" + fe.getEndLoc().getLocName());
row.add(fe.getPresentStateName());
model1.addRow(row);
}
}
if(fe.getStartLoc().equals(presentLoc)) {
duration = Duration.between(fe.getStartTime(), presentTime);
if(Math.abs(duration.toHours()) < 1) {
Vector<String> row = new Vector<String>();
row.add(df.format(fe.getStartTime()));
row.add(fe.getPlanningName());
row.add(fe.getStartLoc().getLocName() + "-" + fe.getEndLoc().getLocName());
row.add(fe.getPresentStateName());
model2.addRow(row);
}
}
}
}
3.8 Board的可视化:外部API的复用
Board使用了JTable进行数据可视化。FlightBoard中由于有抵达航班和出发航班两个表,所以使用了两个JTable,TrainBoard同理。同时为了表格的美观和布局的协调,使用了Y轴箱式布局(BoxLayout)。两个表格都有滚轮结构,能够满足多组数据的显示需要,由于数据在展示前进行了排序,因此计划项的排列是按照出发时间升序排列的。下面以FlightBoard为例,介绍可视化方法。
初始化FlightBoard:
public FlightBoard(Data<FlightEntry> flightdata, Location presentLoc) {
this.flightdata = flightdata;
this.presentLoc = presentLoc;
this.presentTime = LocalDateTime.now();
data1 = new String[0][4];
data2 = new String[0][4];
head1 = new String[]{"落地时间", "航班", "起始地-目的地", "状态"};
head2 = new String[]{"起飞时间", "航班", "起始地-目的地", "状态"};
model1 = new DefaultTableModel(data1, head1);
model2 = new DefaultTableModel(data2, head2);
table1 = new JTable(model1);
table2 = new JTable(model2);
panel1 = new JScrollPane(table1) {
private static final long serialVersionUID = 1L;
@Override
public Dimension getPreferredSize() {
return new Dimension(450, 200);
}
};
panel2 = new JScrollPane(table2) {
private static final long serialVersionUID = 1L;
@Override
public Dimension getPreferredSize() {
return new Dimension(450, 200);
}
};
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
totalLabel = new JLabel(df.format(presentTime) + "(当前时间) "
+ presentLoc.getLocName() + "机场");
label1 = new JLabel("抵达航班");
label2 = new JLabel("出发航班");
totalPanel = new JPanel();
arrivePanel = new JPanel();
leavePanel = new JPanel();
}
设置总体框架:
public void setFrame() {
this.setLayout(new FlowLayout());
setTitle("Flight Board");
setBounds(520, 200, 500, 520);
setVisible(true);
// setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
add(totalPanel);
add(arrivePanel);
add(leavePanel);
}
设置面板:
public void setPanel() {
totalPanel.setLayout(new BoxLayout(totalPanel, BoxLayout.Y_AXIS));
arrivePanel.setLayout(new BoxLayout(arrivePanel, BoxLayout.Y_AXIS));
leavePanel.setLayout(new BoxLayout(leavePanel, BoxLayout.Y_AXIS));
totalPanel.add(totalLabel);
arrivePanel.add(Box.createHorizontalStrut(83));
arrivePanel.add(label1);
arrivePanel.add(panel1);
leavePanel.add(Box.createHorizontalStrut(83));
leavePanel.add(label2);
leavePanel.add(panel2);
}
设置表格格式:
public void setTableFormat() {
DefaultTableCellRenderer tcr = new DefaultTableCellRenderer();
tcr.setHorizontalAlignment(SwingConstants.CENTER);
table1.setDefaultRenderer(Object.class, tcr);
table2.setDefaultRenderer(Object.class, tcr);
}
实现效果如下:
3.9 可复用API设计及Façade设计模式
3.9.1 检测一组计划项之间是否存在位置独占冲突
首先判断传入的计划项列表中的计划项类型,如果是航班或者车次则直接返回false,如果是课程,则遍历计划项集合,两两对比,遇到位置相同的则比对时间是否有冲突,如果有则返回true,表示存在位置独占冲突,否则返回false。以下为判断具体过程:
for(CourseEntry p : courses) {
for(CourseEntry q: courses) {
if(p == q) continue;
else if(p.getLocation().equals(q.getLocation())) {
LocalDateTime ps = p.getStartTime();
LocalDateTime pe = p.getEndTime();
LocalDateTime qs = q.getStartTime();
LocalDateTime qe = q.getEndTime();
if(qs.isBefore(ps) && qe.isAfter(ps)) {
return true;
}
else if(qs.isAfter(ps) && qs.isBefore(pe)) {
return true;
}
}
}
}
return false;
3.9.2 检测一组计划项之间是否存在资源独占冲突
首先判断传入的计划项列表中的计划项类型,Flight和Course可以直接判断是否有相同资源,而Train需要判断其两个计划项的资源列表中是否有重复出现的资源,之后将有相同资源或者重复资源的的计划项的时间进行比对,确定是否有资源独占冲突。
Flight的判断:
if(entries.get(0) instanceof FlightEntry) {
List<FlightEntry> flights = new ArrayList<>();
for(PlanningEntry<R> p : entries) {
flights.add((FlightEntry)p);
}
for(FlightEntry p : flights) {
for(FlightEntry q : flights) {
if(p == q) continue;
else if(p.getResource().equals(q.getResource())) {
LocalDateTime ps = p.getStartTime();
LocalDateTime pe = p.getEndTime();
LocalDateTime qs = q.getStartTime();
LocalDateTime qe = q.getEndTime();
if(qs.isBefore(ps) && qe.isAfter(ps)) {
return true;
}
else if(qs.isAfter(ps) && qs.isBefore(pe)) {
return true;
}
}
}
}
}
Train的判断:
if(entries.get(0) instanceof TrainEntry) {
List<TrainEntry> trains = new ArrayList<>();
boolean hasSameElement;
for(PlanningEntry<R> p : entries) {
trains.add((TrainEntry)p);
}
for(TrainEntry p : trains) {
for(TrainEntry q : trains) {
hasSameElement = false;
if(p == q) continue;
else {
for(Carriage c : p.getResources()) {
if(q.getResources().contains(c)) {
hasSameElement = true;
}
}
if(hasSameElement) {
LocalDateTime ps = p.getTotalStartTime();
LocalDateTime pe = p.getTotalEndTime();
LocalDateTime qs = q.getTotalStartTime();
LocalDateTime qe = q.getTotalEndTime();
if(qs.isBefore(ps) && qe.isAfter(ps)) {
return true;
}
else if(qs.isAfter(ps) && qs.isBefore(pe)) {
return true;
}
}
}
}
}
}
Course的判断:
if(entries.get(0) instanceof CourseEntry) {
List<CourseEntry> courses = new ArrayList<>();
for(PlanningEntry<R> p : entries) {
courses.add((CourseEntry)p);
}
for(CourseEntry p : courses) {
for(CourseEntry q : courses) {
if(p == q) continue;
else if(p.getResource().equals(q.getResource())) {
LocalDateTime ps = p.getStartTime();
LocalDateTime pe = p.getEndTime();
LocalDateTime qs = q.getStartTime();
LocalDateTime qe = q.getEndTime();
if(qs.isBefore(ps) && qe.isAfter(ps)) {
return true;
}
else if(qs.isAfter(ps) && qs.isBefore(pe)) {
return true;
}
}
}
}
}
3.9.3 提取面向特定资源的前序计划项
先判断计划项的类型,之后在列表中寻找是否有相同资源的计划项,如果有则判断时间,如果有当前计划项的前置计划项则将其返回。
Flight的判断:
if(e instanceof FlightEntry) {
List<FlightEntry> flights = new ArrayList<>();
for(PlanningEntry<R> p : entries) {
flights.add((FlightEntry)p);
}
for(FlightEntry p : flights) {
if(p == e) continue;
else if(p.getResource().equals(r) && p.getEndTime().isBefore(((FlightEntry)e).getStartTime())) {
return (PlanningEntry<R>) p;
}
}
}
Train的判断:
if(e instanceof TrainEntry) {
List<TrainEntry> trains = new ArrayList<>();
for(PlanningEntry<R> p : entries) {
trains.add((TrainEntry)p);
}
for(TrainEntry p : trains) {
if(p == e) continue;
else if(p.getResources().contains(r) && p.getTotalEndTime().isBefore(((TrainEntry) e).getTotalStartTime())) {
return (PlanningEntry<R>) p;
}
}
}
Course的判断:
if(e instanceof CourseEntry) {
List<CourseEntry> courses = new ArrayList<>();
for(PlanningEntry<R> p : entries) {
courses.add((CourseEntry)p);
}
for(CourseEntry p : courses) {
if(p == e) continue;
else if(p.getResource().equals(r) && p.getEndTime().isBefore(((CourseEntry)e).getStartTime())) {
return (PlanningEntry<R>) p;
}
}
}
3.10 设计模式应用
3.10.1 Factory Method
为三个具体Entry类实现工厂方法,三个工厂方法类均实现两个工厂方法,一个是直接传入Entry类的接口实现类,另一个是传入实现接口实现类需要的参数,在工厂方法中组装成接口实现类。具体实现如下:
FligthFactory:
public class FlightFactory {
/**
* 通过给定一定的条件实现FlightEntry。
*
* @param name 航班名称
* @param dle 实现好的两个位置接口的实现类
* @param ue 实现好的时间不可阻塞接口的实现类
* @param sre 实现好的计划项单一资源接口的实现类
* @return 新的航班计划项
*/
public FlightEntry getEntry(String name, DoubleLocationEntryImpl dle, UnblockableEntryImpl ue, SingularResourceEntryImpl<Plane> sre) {
return new FlightEntry(name, dle, ue, sre);
}
/**
* 通过给定一定的条件实现FlightEntry。
*
* @param name 航班名称
* @param start 起飞地点
* @param end 降落地点
* @param timeslot 计划起止时间
* @return 新的航班计划项
*/
public FlightEntry getEntry(String name, Location start, Location end, Timeslot timeslot) {
DoubleLocationEntryImpl dle = new DoubleLocationEntryImpl(start, end);
UnblockableEntryImpl ue = new UnblockableEntryImpl(timeslot);
SingularResourceEntryImpl<Plane> sre = new SingularResourceEntryImpl<Plane>();
return new FlightEntry(name, dle, ue, sre);
}
}
TrainFactory:
public class TrainFactory {
/**
* 新建一个列车计划项。
*
* @param name 列车名称
* @param mle 实现好的多个位置的接口的实现类
* @param msre 实现好的多个有序资源接口的实现类
* @param be 实现好的多个有序资源接口的实现类
* @return 新的火车计划项
*/
public TrainEntry getEntry(String name, MultipleLocationEntryImpl mle, MultipleSortedResourceEntryImpl<Carriage> msre,
BlockableEntryImpl be) {
return new TrainEntry(name, mle, msre, be);
}
/**
* 新建一个列车计划项。
*
* @param name 列车名称
* @param start 始发站
* @param end 终点站
* @param midLocList 中间经停车站列表
* @param totalTimeslot 总出发终止时间
* @param blockTimeslotList 经停起始时间
* @return 新的火车计划项
*/
public TrainEntry getEntry(String name, Location start, Location end, List<Location> midLocList,
Timeslot totalTimeslot, List<Timeslot> blockTimeslotList) {
MultipleLocationEntryImpl mle = new MultipleLocationEntryImpl(start, end, midLocList);
MultipleSortedResourceEntryImpl<Carriage> msre = new MultipleSortedResourceEntryImpl<Carriage>();
BlockableEntryImpl be = new BlockableEntryImpl(totalTimeslot, blockTimeslotList);
return new TrainEntry(name, mle, msre, be);
}
}
CourseFactory:
public class CourseFactory {
/**
* 通过给定一定的条件实现CourseEntry。
*
* @param name 课程名称
* @param scle 实现好的单一可更换位置接口的实现类
* @param sre 实现好的计划项单一资源接口的实现类
* @param ue 实现好的时间不可阻塞接口的实现类
* @return 新的课程计划项
*/
public CourseEntry getEntry(String name, SingularChangeableLocationEntryImpl scle, SingularResourceEntryImpl<Teacher> sre,
UnblockableEntryImpl ue) {
return new CourseEntry(name, scle, sre, ue);
}
/**
* 通过给定一定的条件实现CourseEntry。
*
* @param name 课程名称
* @param loc 上课教室
* @param timeslot 上下课时间
* @return 新的课程计划项
*/
public CourseEntry getEntry(String name, Location loc, Timeslot timeslot) {
SingularChangeableLocationEntryImpl scle = new SingularChangeableLocationEntryImpl(loc);
SingularResourceEntryImpl<Teacher> sre = new SingularResourceEntryImpl<Teacher>();
UnblockableEntryImpl ue = new UnblockableEntryImpl(timeslot);
return new CourseEntry(name, scle, sre, ue);
}
}
3.10.2 Iterator
由于board的数据都放在了Data类中,因此原本要给board加的迭代器也就加到了Data类中。添加迭代器,首先要让Data类实现iterable接口,之后完成接口中的iterator方法,完成代码如下:
@Override
public Iterator<L> iterator() {
return new DataIterator();
}
/**
* 为Data类添加的Iterator类。
*/
private class DataIterator implements Iterator<L> {
int cursor = 0;
@Override
public boolean hasNext() {
return cursor != dataList.size();
}
@Override
public L next() {
int i = cursor;
if(i >= dataList.size())
throw new NoSuchElementException();
cursor++;
return dataList.get(i);
}
}
3.10.3 Strategy
为此前的API设计实现不同的策略,我们这里选取的是为测试位置冲突实现不同的策略。
首先实现一个策略接口CheckLocationConflictStrategy,其中定义了策略中要用到的方法checkLocationConflict,这里的目的是统一不同的策略,使不同的策略能够被API中的方法调用。之后要重载API中的checkLocationConflict方法,使之能够接受参数列表为CheckLocationConflictStrategy类型的参数,重载的方法中调用策略类型中实现的checkLocationConflict方法。过程如下:
public interface CheckLocationConflictStrategy<R> {
/**
* 检测计划项列表中是否有位置冲突。
*
* @return {@code true}当计划项中有位置冲突(不可共享的位置在同一时间被不同计划项占用);
* {@code false}当计划向中没有位置冲突
*/
public boolean checkLocationConflict();
}
实现的具体策略类的大体实现如下所示:
public class FirstStrategy<R> implements CheckLocationConflictStrategy<R> {
List<PlanningEntry<R>> entries;
public FirstStrategy(List<PlanningEntry<R>> entries) {
this.entries = entries;
}
@Override
public boolean checkLocationConflict() {
if(entries.isEmpty()) return false;
if(!(entries.get(0) instanceof CourseEntry)) {
return false;
}
List<CourseEntry> courses = new ArrayList<>();
for(PlanningEntry<R> p : entries) {
courses.add((CourseEntry)p);
}
for(CourseEntry p : courses) {
for(CourseEntry q: courses) {
if(p == q) continue;
else if(p.getLocation().equals(q.getLocation())) {
LocalDateTime ps = p.getStartTime();
LocalDateTime pe = p.getEndTime();
LocalDateTime qs = q.getStartTime();
LocalDateTime qe = q.getEndTime();
if(qs.isBefore(ps) && qe.isAfter(ps)) {
return true;
}
else if(qs.isAfter(ps) && qs.isBefore(pe)) {
return true;
}
}
}
}
return false;
}
}
至此就可以调用策略方法了,客户端可以自由选择策略来进行判断,而达到的效果是相同的。
3.11 应用设计与开发
为了使app本身的操作整洁可观,因此将操作放到了app的外面,是为Operation。而三个operation均用到且可复用的操作进一步被提取出来,形成ScheduleOperation。ScheduleOperation中包含了对resource列表和location列表的操作,以及checkLocationAndResourseConflict()、getAllPlanWithResource(R resource)、viewBoardOfLoc(String
locName)的抽象方法。ScheduleOperation的子类FlightSchedule、TrainOperation、CourseOperation主要调用了之前在Data类以及APIs还有Entry中的方法,在外面包装了条件判断,形成了各应用要用到的方法,供具体的app类来使用。
3.11.1 航班应用
通过命令行来进行操作,输入数字进行功能选择,若输入的数字满足条件,则进入相应的分支执行相应的操作,如有必要则提示要输入的信息以及格式。FlightScheduleApp中的操作如下所示:
public void help() {
System.out.println("\n操作序号:");
System.out.println(" 1、增加飞机资源");
System.out.println(" 2、删除飞机资源");
System.out.println(" 3、增加位置");
System.out.println(" 4、删除位置");
System.out.println(" 5、增加新的航班");
System.out.println(" 6、取消航班");
System.out.println(" 7、为航班分配飞机");
System.out.println(" 8、启动航班");
System.out.println(" 9、结束航班");
System.out.println(" 10、查看航班状态");
System.out.println(" 11、检测航班列表中可能存在的位置和资源独占冲突");
System.out.println(" 12、列出使用某一飞机的所有计划项");
System.out.println(" 13、可视化展示当前时刻某一位置的信息板");
System.out.println(" 14、提示操作序号");
System.out.println(" 15、退出");
}
具体的操作流程如下所示:
while(true) {
System.out.println("\n请输入您的选择:");
choose = in.nextLine();
try {
if(choose.equals("1")) {
System.out.println("请输入要添加的飞机资源信息:飞机编号、飞机型号、座位数、机龄(不同信息用空格分开)");
String messageLine = in.nextLine();
String[] messages = messageLine.split(" ");
Plane plane = new Plane(messages[0], messages[1], Integer.valueOf(messages[2]), Float.valueOf(messages[3]));
if(fsa.addResource(plane)) {
System.out.println("添加成功!");
} else {
System.out.println("添加失败!");
}
} else if(choose.equals("2")) {
System.out.println("请输入要删除的飞机资源信息:飞机编号、飞机型号、座位数、机龄(不同信息用空格分开)");
String messageLine = in.nextLine();
String[] messages = messageLine.split(" ");
Plane plane = new Plane(messages[0], messages[1], Integer.valueOf(messages[2]), Float.valueOf(messages[3]));
if(fsa.delResource(plane)) {
System.out.println("删除成功!");
} else {
System.out.println("删除失败!");
}
} else if(choose.equals("3")) {
System.out.println("请输入要添加的位置名称:");
String messageLine = in.nextLine();
Location location = new Location(messageLine, true);
if(fsa.addLocation(location)) {
System.out.println("添加成功!");
} else {
System.out.println("添加失败!");
}
} else if(choose.equals("4")) {
System.out.println("请输入要删除的位置名称:");
String messageLine = in.nextLine();
Location location = new Location(messageLine, true);
if(fsa.delLocation(location)) {
System.out.println("删除成功!");
} else {
System.out.println("删除失败!");
}
} else if(choose.equals("5")) {
System.out.println("请输入要添加的新的航班信息:航班名称、起始位置、终点位置、起飞时间、降落时间(不同信息用空格隔开,时间格式为:yyyy-MM-dd-hh-mm)");
String messageLine = in.nextLine();
String[] messages = messageLine.split(" ");
String[] startTime = messages[3].split("-");
String[] endTime = messages[4].split("-");
int y1 = Integer.valueOf(startTime[0]);
int mon1 = Integer.valueOf(startTime[1]);
int d1 = Integer.valueOf(startTime[2]);
int h1 = Integer.valueOf(startTime[3]);
int min1 = Integer.valueOf(startTime[4]);
int y2 = Integer.valueOf(endTime[0]);
int mon2 = Integer.valueOf(endTime[1]);
int d2 = Integer.valueOf(endTime[2]);
int h2 = Integer.valueOf(endTime[3]);
int min2 = Integer.valueOf(endTime[4]);
if(fsa.addPlan(messages[0], new Location(messages[1], true), new Location(messages[2], true),
new Timeslot(y1, mon1, d1, h1, min1, y2, mon2, d2, h2, min2))) {
System.out.println("添加成功!");
} else {
System.out.println("添加失败!");
}
} else if(choose.equals("6")) {
System.out.println("请输入要取消的航班信息:航班名称、起飞时间、降落时间(不同信息用空格隔开,时间格式为:yyyy-MM-dd-hh-mm)");
String messageLine = in.nextLine();
String[] messages = messageLine.split(" ");
String[] startTime = messages[1].split("-");
String[] endTime = messages[2].split("-");
int y1 = Integer.valueOf(startTime[0]);
int mon1 = Integer.valueOf(startTime[1]);
int d1 = Integer.valueOf(startTime[2]);
int h1 = Integer.valueOf(startTime[3]);
int min1 = Integer.valueOf(startTime[4]);
int y2 = Integer.valueOf(endTime[0]);
int mon2 = Integer.valueOf(endTime[1]);
int d2 = Integer.valueOf(endTime[2]);
int h2 = Integer.valueOf(endTime[3]);
int min2 = Integer.valueOf(endTime[4]);
fsa.cancelPlan(messages[0], new Timeslot(y1, mon1, d1, h1, min1, y2, mon2, d2, h2, min2));
System.out.println("当前该航班状态为:" + fsa.getState(messages[0], new Timeslot(y1, mon1, d1, h1, min1, y2, mon2, d2, h2, min2)));
} else if(choose.equals("7")) {
System.out.println("请输入要分配的航班名称、起飞时间、降落时间、飞机编号、飞机型号、座位数、机龄(不同信息用空格分开)");
String messageLine = in.nextLine();
String[] messages = messageLine.split(" ");
String[] startTime = messages[1].split("-");
String[] endTime = messages[2].split("-");
int y1 = Integer.valueOf(startTime[0]);
int mon1 = Integer.valueOf(startTime[1]);
int d1 = Integer.valueOf(startTime[2]);
int h1 = Integer.valueOf(startTime[3]);
int min1 = Integer.valueOf(startTime[4]);
int y2 = Integer.valueOf(endTime[0]);
int mon2 = Integer.valueOf(endTime[1]);
int d2 = Integer.valueOf(endTime[2]);
int h2 = Integer.valueOf(endTime[3]);
int min2 = Integer.valueOf(endTime[4]);
if(fsa.distributeResource(messages[0], new Timeslot(y1, mon1, d1, h1, min1, y2, mon2, d2, h2, min2),
new Plane(messages[3], messages[4], Integer.valueOf(messages[5]), Float.valueOf(messages[6])))) {
System.out.println("分配成功!");
} else {
System.out.println("分配失败!");
}
} else if(choose.equals("8")) {
System.out.println("请输入要启动的航班信息:航班名称、起飞时间、降落时间(不同信息用空格隔开,时间格式为:yyyy-MM-dd-hh-mm)");
String messageLine = in.nextLine();
String[] messages = messageLine.split(" ");
String[] startTime = messages[1].split("-");
String[] endTime = messages[2].split("-");
int y1 = Integer.valueOf(startTime[0]);
int mon1 = Integer.valueOf(startTime[1]);
int d1 = Integer.valueOf(startTime[2]);
int h1 = Integer.valueOf(startTime[3]);
int min1 = Integer.valueOf(startTime[4]);
int y2 = Integer.valueOf(endTime[0]);
int mon2 = Integer.valueOf(endTime[1]);
int d2 = Integer.valueOf(endTime[2]);
int h2 = Integer.valueOf(endTime[3]);
int min2 = Integer.valueOf(endTime[4]);
fsa.startPlan(messages[0], new Timeslot(y1, mon1, d1, h1, min1, y2, mon2, d2, h2, min2));
System.out.println("当前该航班状态为:" + fsa.getState(messages[0], new Timeslot(y1, mon1, d1, h1, min1, y2, mon2, d2, h2, min2)));
} else if(choose.equals("9")) {
System.out.println("请输入要结束的航班信息:航班名称、起飞时间、降落时间(不同信息用空格隔开,时间格式为:yyyy-MM-dd-hh-mm)");
String messageLine = in.nextLine();
String[] messages = messageLine.split(" ");
String[] startTime = messages[1].split("-");
String[] endTime = messages[2].split("-");
int y1 = Integer.valueOf(startTime[0]);
int mon1 = Integer.valueOf(startTime[1]);
int d1 = Integer.valueOf(startTime[2]);
int h1 = Integer.valueOf(startTime[3]);
int min1 = Integer.valueOf(startTime[4]);
int y2 = Integer.valueOf(endTime[0]);
int mon2 = Integer.valueOf(endTime[1]);
int d2 = Integer.valueOf(endTime[2]);
int h2 = Integer.valueOf(endTime[3]);
int min2 = Integer.valueOf(endTime[4]);
fsa.endPlan(messages[0], new Timeslot(y1, mon1, d1, h1, min1, y2, mon2, d2, h2, min2));
System.out.println("当前该航班状态为:" + fsa.getState(messages[0], new Timeslot(y1, mon1, d1, h1, min1, y2, mon2, d2, h2, min2)));
} else if(choose.equals("10")) {
System.out.println("请输入要查看状态的航班信息:航班名称、起飞时间、降落时间(不同信息用空格隔开,时间格式为:yyyy-MM-dd-hh-mm)");
String messageLine = in.nextLine();
String[] messages = messageLine.split(" ");
String[] startTime = messages[1].split("-");
String[] endTime = messages[2].split("-");
int y1 = Integer.valueOf(startTime[0]);
int mon1 = Integer.valueOf(startTime[1]);
int d1 = Integer.valueOf(startTime[2]);
int h1 = Integer.valueOf(startTime[3]);
int min1 = Integer.valueOf(startTime[4]);
int y2 = Integer.valueOf(endTime[0]);
int mon2 = Integer.valueOf(endTime[1]);
int d2 = Integer.valueOf(endTime[2]);
int h2 = Integer.valueOf(endTime[3]);
int min2 = Integer.valueOf(endTime[4]);
System.out.println(fsa.getState(messages[0], new Timeslot(y1, mon1, d1, h1, min1, y2, mon2, d2, h2, min2)));
} else if(choose.equals("11")) {
if(fsa.checkLocationAndResourseConflict()) {
System.out.println("存在冲突!");
} else {
System.out.println("不存在冲突!");
}
} else if(choose.equals("12")) {
System.out.println("请输入要查找的飞机资源信息:飞机编号、飞机型号、座位数、机龄(不同信息用空格分开)");
String messageLine = in.nextLine();
String[] messages = messageLine.split(" ");
Plane plane = new Plane(messages[0], messages[1], Integer.valueOf(messages[2]), Float.valueOf(messages[3]));
System.out.println("占用" + plane.getPlaneID() + "的航班有:");
for(PlanningEntry<Plane> plan : fsa.getAllPlanWithResource(plane)) {
System.out.println(plan.getPlanningName() + "\t" + plan.getPresentStateName());
}
System.out.println("是否继续查找某一航班的前序计划项?(y\\n)");
messageLine = in.nextLine();
if(messageLine.toLowerCase().equals("y")) {
PlanningEntryAPIs<Plane> help = new PlanningEntryAPIs<>();
System.out.println("请输入查找前序计划项的航班信息:航班名称、起飞时间、降落时间(不同信息用空格隔开,时间格式为:yyyy-MM-dd-hh-mm)");
messageLine = in.nextLine();
messages = messageLine.split(" ");
String[] startTime = messages[1].split("-");
String[] endTime = messages[2].split("-");
int y1 = Integer.valueOf(startTime[0]);
int mon1 = Integer.valueOf(startTime[1]);
int d1 = Integer.valueOf(startTime[2]);
int h1 = Integer.valueOf(startTime[3]);
int min1 = Integer.valueOf(startTime[4]);
int y2 = Integer.valueOf(endTime[0]);
int mon2 = Integer.valueOf(endTime[1]);
int d2 = Integer.valueOf(endTime[2]);
int h2 = Integer.valueOf(endTime[3]);
int min2 = Integer.valueOf(endTime[4]);
FlightEntry fe = fsa.getPlan(messages[0], new Timeslot(y1, mon1, d1, h1, min1, y2, mon2, d2, h2, min2));
FlightEntry fe2 = (FlightEntry) help.findPreEntryPerResource(plane, fe, fsa.getAllPlanWithResource(plane));
if(fe2 != null) {
System.out.println("前序计划项为:" + fe2.getPlanningName());
} else {
System.out.println("未找到前序计划项!");
}
}
} else if(choose.equals("13")) {
System.out.println("请输入要查看看板的位置:");
fsa.viewBoardOfLoc(in.nextLine());
} else if(choose.equals("14")) {
fsa.help();
} else if(choose.equals("15")) {
System.out.println("感谢您的使用!");
in.close();
System.exit(0);
} else {
System.out.println("请输入正确的选项!");
}
} catch(Exception e) {
System.out.println("输入内容不符合规范!");
}
}
3.11.2 高铁应用
与FlightScheduleApp的实现方式类似。
具体实现的功能为:
public void help() {
System.out.println("\n操作序号:");
System.out.println(" 1、增加资源");
System.out.println(" 2、删除资源");
System.out.println(" 3、增加位置");
System.out.println(" 4、删除位置");
System.out.println(" 5、增加新的车次");
System.out.println(" 6、取消车次");
System.out.println(" 7、为车次分配一节车厢");
System.out.println(" 8、启动车次");
System.out.println(" 9、阻塞车次");
System.out.println(" 10、重新启动车次");
System.out.println(" 11、结束车次");
System.out.println(" 12、查看车次状态");
System.out.println(" 13、检测车次列表中可能存在的位置和资源独占冲突");
System.out.println(" 14、列出使用某一车次的所有计划项");
System.out.println(" 15、可视化展示当前时刻某一位置的信息板");
System.out.println(" 16、提示操作序号");
System.out.println(" 17、退出");
}
3.11.3 课表应用
与FlightScheduleApp的实现方式类似。
具体实现的功能为:
public void help() {
System.out.println("\n操作序号:");
System.out.println(" 1、增加教师");
System.out.println(" 2、删除教师");
System.out.println(" 3、增加教室");
System.out.println(" 4、删除教室");
System.out.println(" 5、增加新的课程");
System.out.println(" 6、取消课程");
System.out.println(" 7、为课程分配老师");
System.out.println(" 8、开始课程");
System.out.println(" 9、为某课程更换教室");
System.out.println(" 10、结束课程");
System.out.println(" 11、查看课程状态");
System.out.println(" 12、检测课程列表中可能存在的位置和资源独占冲突");
System.out.println(" 13、列出某一教师的所有课程");
System.out.println(" 14、可视化展示当前时刻某一位置的信息板");
System.out.println(" 15、提示操作序号");
System.out.println(" 16、退出");
}
3.12 基于语法的数据读入
修改“航班”应用以扩展该功能。新增FlightScheduleApp的构造方法,可以从文件读入信息。
public FlightScheduleApp(String fileName) {
// fileName e.g. "FlightSchedule_5"
this.fso = generateFlightApp(fileName);
if(fso == null) {
this.illegal = true;
}
}
下面实现generateFlightApp方法,根据实验手册中的语法规则使用正则表达式来进行信息提取。
Pattern flightNamePattern = Pattern.compile("^Flight:(\\d{4})-(\\d{2})-(\\d{2}),([A-Z]{2}\\d{2,4})$");
Pattern departureAirportPattern = Pattern.compile("^DepartureAirport:([a-zA-Z]+)$");
Pattern arrivalAirportPattern = Pattern.compile("^ArrivalAirport:([a-zA-Z]+)$");
Pattern departureTimePattern = Pattern.compile("^DepatureTime:(\\d{4})-(\\d{2})-(\\d{2})\\s(\\d{2}):(\\d{2})$");
Pattern arrivalTimePattern = Pattern.compile("^ArrivalTime:(\\d{4})-(\\d{2})-(\\d{2})\\s(\\d{2}):(\\d{2})$");
Pattern planeIDPattern = Pattern.compile("^Plane:([N|B]\\d{4})$");
Pattern planeTypePattern = Pattern.compile("^Type:([a-zA-Z\\d]+)$");
Pattern planeSeatsPattern = Pattern.compile("^Seats:(([5-9]\\d)|([1-5]\\d{2})|(600))$");
Pattern planeAgePattern = Pattern.compile("^Age:((([1-2]?[0-9])(\\.\\d)?)|(30|30.0))$");
以上的正则表达式分别用来提取航班的日期、名称、起降机场、起降时间以及飞机的信息。由于给的文件13行构成一个Entry的完整信息,因此记录行数,以13为周期每次提取一行中的相应信息,同时对信息进行语法判断。最后与之前录入的信息进行比对,看是否有同样编号的航班的时间不相同、地点不同的情况,一旦发现上述过程有不符合语法的信息,则退出文件读取,同时提示输入正确的文件。判断文件中新读入的FlightEntry信息是否合法的方法如下所示,放在FlightScheduleOperation方法中。
/**
* 改变计划项名称为planName的计划项的位置
*
* @param planName 计划项名称
* @param postLoc 改变后的位置
* @return 改变成功返回{@code true},否则返回{@code false}
*/
public boolean changeLocation(String planName, Location postLoc) {
CourseEntry ce = courseData.getPlan(planName);
if(!super.getLocationSet().contains(postLoc)) {
System.out.println("位置" + postLoc.getLocName() + "不存在!");
return false;
}
if(ce != null) {
ce.changeLocation(postLoc);
return true;
}
return false;
}
此外,以上的类和方法进行了junit测试,测试均通过,测试结果如下:
测试覆盖度如下:
注意到对src的测试率达到了34.9%,其中app和board的测试率较低是由于app中有大量的客户端代码,而board中有大量的gui代码,其他类中覆盖度有一些在85%左右是因为有一些方法只是通过委托调用了已经测试的过的方法,已无测试必要,因此综上分析,测试基本全面。
3.13 应对面临的新变化
3.13.1 变化1 :FlightEntry的改变
之前的设计可以应对变化,航班支持经停,那么在构造FlightEntry中是就应当用多地点和时间可阻塞的接口,由于需要支持经停,因此除了FlightEntry本身需要更改,其余的FlightScheduleApp、FlightScheduleOperation、FlightBoard都需要进行更改,工作量略大。
修改过程:
首先修改调用的接口,改变数据域中的接口实现类。
然后将随之发生改变的工厂方法以及FlightData以及FlightBoard类进行相应的修改,最后修改app中的提示以及执行过程。最后修改test中的相应测试即可。
如图,实现了经停的功能。
3.13.2 变化2:TrainEntry的改变
之前的设计可以应对变化,当分配了车厢则车次不可取消,只需在取消的时候判断一下当前的状态,只有状态为WAITING时才可以取消计划,较为简单。
修改过程:
重写TrainEntry的cancel方法:
实现结果:
当创建了新的车次而没有分配车厢时,可以取消车次。
而当分配了车厢后,无法再将改车次取消。
3.13.3 变化3:CourseEntry的改变
之前的设计可以应对变化,可以有多个教师一起上课且需要区分次序,那么将课程的资源接口改成与TrainEntry相同的MultipleSortedResourceEntry即可,相对工作量不大。
修改过程:
首先修改调用的接口,改变数据域中的接口实现类。
然后将随之发生改变的工厂方法以及CourseData以及CourseBoard类进行相应的修改。最后修改test中的相应测试即可。
实现结果:
实现了多个教师按照次序进行上课。