本次实验覆盖课程第 3、4、5 章的内容,目标是编写具有可复用性和可维护性的软件,主要使用以下软件构造技术:
子类型、泛型、多态、重写、重载
继承、代理、组合
常见的 OO 设计模式
语法驱动的编程、正则表达式
基于状态的编程
API 设计、API 复用
本次实验给定了五个具体应用(高铁车次管理、航班管理、操作系统进程管理、大学课表管理、学习活动日程管理),学生不是直接针对五个应用分别编程
实现,而是通过 ADT 和泛型等抽象技术,开发一套可复用的 ADT 及其实现,充分考虑这些应用之间的相似性和差异性,使 ADT 有更大程度的复用(可复用性)和更容易面向各种变化(可维护性)
略
我选择的三个应用场景:
①航班管理
②高铁车次管理
③大学课表管理
这三个场景的异同点:
①位置的数量:分别为1个、2个和多个
②仅有大学课表的位置可更改
③航班和大学课表为单个资源,高铁为有序多个资源
④仅有高铁车次可阻塞
⑤时间均在创建时设定
计划项是一个状态可变的ADT,它保存有一个计划项的时间、地点、资源等有效信息。PlanningEntry在我的设计中是一个接口,设计有各种计划项均要实现的方法以及工厂方法。
PlanningEntry的共性操作接口:
①3个工厂方法分别能够返回指定类型的PlanningEntry实现类,以Flight Schedule为例:
public static<Resource> FlightSchedule<Resource> newFlightEntry(LocalDate planningDate,Location location,Timeslot timeslot,String planningEntryNumber){
return new FlightSchedule<Resource>(planningDate,location,timeslot,planningEntryNumber);}
②状态的转换,以目标状态进行分类,能够将状态转换为RUNNING、BLOCKED、CANCELLED、ENDED四种之一(其中转换为ALLOCATED是个性化设计),分别用4个方法来实现。以run()为例:
**
* 将计划项状态转化为RUNNING已启动
* @return 当状态转化成果返回true
*/
public boolean run();
③Getter()包括了获取Location、Timeslot、State、Type、Number、Type一些共有的信息对象。
CommonPlanningEntry类实现了PlanningEntry接口中共性方法,包括了状态转换和Getter方法。
①状态转换,以run()为例:将状态转换委派给state对象的Setter操作,通过常量来进行目标状态的区分。在state对象中,首先判断该转换是否合法(访问EntryStateEnum静态常量进行判断),然后在进行状态覆盖,最后返回操作成功与否的标识:
@Override
public boolean run() {
// TODO Auto-generated method stub
return this.state.setNewState(strPlanningEntryType,"Running");
}
②Getter操作访问CommonPlanningEntry中定义的共性成员变量,包括Location、Resource等等。以getLocation()为例:
@Override
public Location getLocation() {
// TODO Auto-generated method stub
return this.location;
}
③此外,设计抽象方法getPlanningDate()等Spec相同的方法。
public abstract LocalDate getPlanningDate();
①3个子类型的不同主要在于两方面:Location、Timeslot、Resource等信息的存储模式和信息的修改。
②通过不同子类型的不同Getter来得到相应的信息细节。
以Flight Schedule为例,如下方法获得了起飞、降落机场:
/**
* 获取航班起点站
* @return 起点站名
*/
public String getOriginLocation() {
return super.getLocation().getLocationList().get(0);
}
/**
* 获取航班终点站
* @return 终点站名
*/
public String getTerminusLocation() {
int n=super.getLocation().getLocationList().size()-1;
return super.getLocation().getLocationList().get(n);
}
而在Class Schedule中如下获得班级地点:
/**
* 获取上课地点
* @return
*/
public String getClassroomLocation() {
return this.getLocation().getLocationList().get(IndexOfLocation);
}
③信息修改:根据不同计划项信息的修改特点进行设计。例如allocateResource(),飞机只能分配一个资源,而高铁为多个有序资源(用List< R >存储),活动为多个无序资源。以高铁的分配资源为例:
public boolean allocateResource(Car... resources) {
this.resources.addAll(Arrays.asList(resources));
this.ORIGIN=0;
this.TERMINUS=this.location.getLocationList().size()-1;
return this.state.setNewState(strPlanningEntryType, "ALLOCATED");
}
通过不定项的资源作为参数,然后保存到list中,获取长度、起终点标识,并设置状态。
④ClassSchedule可以在开始前设置新地点,则再该计划项子类中添加对应的Setter方法。
/**
* 在分配教师资源但并未上课的计划项中更改上课地址
* @param strNewLocation
* @return 成功更改返回true,否则false
*/
public boolean setNewLocation(String strNewLocation) {
if(this.getState().getStateString().equals("ALLOCATED")) {
this.location=new Location(strNewLocation);
return true;
}
return false;
}
相关JUnit测试已全部通过
资源有多种,因为Resource被设计为一个接口,有3个实现类:Plane、Train和Teacher。Resource接口中有3种子类的工厂方法。
三种子类存储各自特有的信息
3种子类均为immutable,设计有各个成员变量的Getter,并且根据要求重写equals等。以Plane为例:
/**
* 将飞机信息形成形成<编号,型号(座位数,机龄)>格式
*/
@Override
public String toString() {
String s=new String();
s="<"+this.number+","+this.type+"("+this.seatNum+","+this.age+")>";
return s;
}
/**
* 判断当前飞机和指定飞机是否相同
* @param that
* @return 相同返回true,否则false
*/
public boolean equals(Plane that) {
if(this.toString().equals(that.toString())) {
return true;
}
else return false;
}
相关JUnit测试已全部通过
由于选择的3种计划项位置数量各不相同,因此采用一个List来存储若干的位置,通过PlanningEntry的不同Getter来获取。
相关JUnit测试已全部通过
Time Slot和Location是共同设计的,存储有两个List,分别代表对应的位置的到达和离开时间。由此设计,可以精确到每个地点的到达和离开时间,若为第一个地点,则到达和离开时间相同;若为最后一个地点也如此;若只有一个地点,则也如此。
由此,3种不同的计划项,通过不同的Getter实现不同的特征。
/*
* AF:
* arrival[i]表示到达locations[i]的时间,leaving同理
* 对于航班和高铁来说,起点和终点的到达和离开时间相同,即arrival[0]==leaving[0],arrival[n]==leaving[n]
* 航班:size=2,leaving[0]为起点起飞时间,arrival[1]为终点降落时间,为了便于表达,arrival[0]=leaving[0],arrival[1]=leaving[1]
* 高铁:size>=2
* 课程:size=1,arrival[0]为上课时间,leaving[0]为下课时间
* RI:
* arrival和leaving数量相等
* 任何leaving[i]不可早于arrival[i]
* 两列表不可为有空项
* Safety:
* 不要暴露mutator
*/
private final List<LocalDateTime> arrivalTimeList;
private final List<LocalDateTime> leavingTimeList;
相关JUnit测试已全部通过
EntryState是一个可变对象,成员变量有类型为enum的state。
在构造方法中,通过字符串参数toUpperCase,再对应到EntryStateEnum中的某一个枚举,进行初始化。
状态是可变的,因此它需要设置一个Setter,即setNewState()。由于不同的计划项类型,可以设置的state不同,因此参数需要有计划项类型和新状态的字符串。这样的一个方法可以满足各种状态的转换。
/**
* 状态转化,将计划项状态转化为NewState
* @param strPlanningEntryType(∈{FlightSchedule,TrainSchedule,ClassSchedule})
* @param strNewState
* @return 成功返回true,否则false
*/
public boolean setNewState(String strPlanningEntryType,String strNewState) {
assert(strPlanningEntryType.toLowerCase().contains("train")
||!this.getStateString().toLowerCase().equals("blocked"));
if(this.setability(strPlanningEntryType, strNewState.toUpperCase())) {
this.stateName=strNewState.toUpperCase();
this.state=EntryStateEnum.valueOf(strNewState.toUpperCase());
return true;
}
return false;
}
判断合法性的工作交给另一个范围值为Boolean的方法setability(),而该方法又将这项工作委派给EntryStateEnum中的静态Map变量。该Map分为两种,一种是可能被Block的,一种则不行。判断是否可以Block的工作在EntryStateEnum中进行,用一个List来保存可以Block的类型的关键字(增强鲁棒性)。
/**
* 获取当前状态是否可以被转化成其他状态
* @param 当前计划项类型strPlanningEntryType(∈{FlightSchedule,TrainSchedule,ClassSchedule})
* @param 新的状态strNewState
* @return 可以转化返回true,否则false
*/
private boolean setability(String strPlanningEntryType,String strNewState) {
List<EntryStateEnum> availableStateList=new ArrayList<EntryStateEnum>
(Arrays.asList(this.state.newStateAchievable(strPlanningEntryType)));
return availableStateList.contains(EntryStateEnum.valueOf(strNewState.toUpperCase()));
}
静态存储能/不能Block的“可以到达的新状态”的Map,Key为该枚举,Value为List。通过匿名对象初始化方法来初始化。
/**
* 可以block的可达状态map,key为当前状态,value为可以到达的状态列表
*/
public static final Map<EntryStateEnum, EntryStateEnum[]> BlockableNewStateAchievable
=new HashMap<EntryStateEnum,EntryStateEnum[]>(){
private static final long serialVersionUID=1L;
{
put(WAITING,new EntryStateEnum[] {ALLOCATED,CANCELLED});
put(ALLOCATED,new EntryStateEnum[]{RUNNING,CANCELLED});
put(RUNNING,new EntryStateEnum[]{BLOCKED, ENDED });
put(BLOCKED, new EntryStateEnum[] { RUNNING, CANCELLED });
put(CANCELLED, new EntryStateEnum[] {});
put(ENDED, new EntryStateEnum[] {});
}
};
/**
* 不可block的可达状态map,key为当前状态,value为可达的状态列表
*/
public static final Map<EntryStateEnum, EntryStateEnum[]> UnblockableNewStateAchievable
=new HashMap<EntryStateEnum,EntryStateEnum[]>(){
private static final long serialVersionUID=1L;
{
put(WAITING, new EntryStateEnum[] { ALLOCATED, CANCELLED });
put(ALLOCATED, new EntryStateEnum[] { RUNNING, CANCELLED });
put(RUNNING, new EntryStateEnum[] { ENDED });
put(CANCELLED, new EntryStateEnum[] {});
put(ENDED, new EntryStateEnum[] {});
}
};
保存可以Block的计划项名称:
/**
* 可以被block的计划项,即Train
*/
public static final List<String> keyWords=new ArrayList<String>(){
private static final long serialVersionUID=1L;
{
add("Train");
}
};
建立一个属于枚举的成员方法,返回“可以到达的新状态”。调用原状态的枚举对象查询该List(即this代表当前状态):
/**
* 获取可以转化成的状态的列表
* @param strPlanningEntryType
* @return 可以转化成的状态的列表
*/
public EntryStateEnum[] newStateAchievable(String strPlanningEntryType) {
for (String str : keyWords)
if (strPlanningEntryType.contains(str)) {
//检索对应map,获取当前状态this的对应可转化状态列表
return EntryStateEnum.BlockableNewStateAchievable.get(this);
}
return EntryStateEnum.UnblockableNewStateAchievable.get(this);
}
因此,在状态模式的设计种,一次设置新状态的操作,经过:
PlanningEntryCollection
-> PlanningEntry
-> EntryState.setNewState() {
EntryState.setability() -> EntryStateEnum.newStateAchievable()}
完成一次指定操作。其中,在PlanningEntryCollection和PlanningEntry中采用外观模式包装成5个方法,分别到达5种状态。
这部分我的设计其实有所欠缺,因为board大体上还是相近的,设置一个抽象类和三个子类其实更为合适。但是在本实验中,由于对于JTable掌握过于薄弱话费了太多精力,所以直接将三种board分开写。内容思路相近,几乎是重复性操作而已。具体如下:
三个应用的board设计是类似的,以flightSchedule的board为例:
在board中我们需要传入计划项集合。
然后根据输入的航班集合、机场名和日期信息来获取当日到达以及飞离指定机场的计划项
根据输入的航班集合、飞机资源、和截止航班获取截止到指定航班使用了指定资源的计划项
并用这些计划项构建JTable
JTable建立过程大致如下:
JTable table = new JTable(answer, columnNames);
// 设置表格内容颜色
table.setForeground(Color.BLACK); // 字体颜色
table.setFont(new Font(null, Font.PLAIN, 14)); // 字体样式
table.setSelectionForeground(Color.DARK_GRAY); // 选中后字体颜色
table.setSelectionBackground(Color.LIGHT_GRAY); // 选中后字体背景
table.setGridColor(Color.GRAY); // 网格颜色
// 设置表头
table.getTableHeader().setFont(new Font(null, Font.BOLD, 14)); // 设置表头名称字体样式
table.getTableHeader().setForeground(Color.RED); // 设置表头名称字体颜色
table.getTableHeader().setResizingAllowed(false); // 设置不允许手动改变列宽
table.getTableHeader().setReorderingAllowed(false); // 设置不允许拖动重新排序各列
// 设置行高
table.setRowHeight(30);
// 第一列列宽设置为40
table.getColumnModel().getColumn(0).setPreferredWidth(40);
// 设置滚动面板视口大小(超过该大小的行数据,需要拖动滚动条才能看到)
table.setPreferredScrollableViewportSize(new Dimension(1000, 100));
table.getColumnModel().getColumn(0).setPreferredWidth(250);
table.getColumnModel().getColumn(1).setPreferredWidth(200);
table.getColumnModel().getColumn(2).setPreferredWidth(350);
table.getColumnModel().getColumn(3).setPreferredWidth(200);
return table;
该ADT是PlanningEntry的集合类。该集合类应该能够存储所有计划项、所有位置和可用资源,以及作为一个“管理者”的身份来操作这些成员变量。
在PlanningEntryCollection中实现了一些共性方法,并定义了一些抽象方法。不同子类实现有差异的方法有:添加计划项、分配资源、排序计划项;其余为相同的实现。
开始、取消、暂停、结束一个计划项这4种操作利用外观模式,将单个计划项中的成员方法进行封装。首先在所有计划项中找到该计划项,若找到则再进行对应的操作。以cancelPlanningEntry()为例:
/**
* cancel a plan
* @param planningEntryNumber
* @return true if successfully the plan is successfully canceld
*/
public boolean cancelPlanningEntry(String planningEntryNumber) {
PlanningEntry<Resource> planningEntry=this.getPlanningEntryByStrNumber(planningEntryNumber);
return planningEntry==null?false:planningEntry.cancel();
}
此外,共性方法还有计划项、资源、位置的Getter,以及删除单个资源和位置的方法。
接下来是有差异的方法。首先是新增一个计划项,以Flight Schedule为例。首先addPlanningEntry()有一个重载方法,可以将已经提取好的参数直接输入并新建:
/**
* create a new flightSchedule plan
* @param planningDatestr
* @param planningEntryNumber
* @param departureAirport
* @param arrivalAirport
* @param departureTime
* @param arrivalTime
* @return the new flight schedule
*/
public FlightSchedule<Resource> addPlanningEntry(String planningDatestr,String planningEntryNumber,
String departureAirport,String arrivalAirport,
String departureTime,String arrivalTime){
Location location=new Location(departureAirport,arrivalAirport);
Timeslot timeslot=new Timeslot(Arrays.asList(departureTime,arrivalTime),Arrays.asList(departureTime,arrivalTime));
DateTimeFormatter df=DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate planningDate=LocalDate.parse(planningDatestr);
this.collectionLocation.addAll(location.getLocationList());
PlanningEntry<Resource> flightSchedule=PlanningEntry.newFlightEntry(planningDate,location, timeslot, planningEntryNumber);
this.planningEntries.add(flightSchedule);
return (FlightSchedule<Resource>)flightSchedule;
}
通过重载的方法,提供两种不同的新建方式,方便在之后的GUI客户端新建计划项的操作。接下来实现在抽象类中定义的方法,主要是要通过正则表达式提取所需的要素,并调用上述方法。用Pattern对象定义模式,用Matcher对象进行匹配;若匹配成功,则用group方法提取参数输入上述方法。
@Override
public PlanningEntry<Resource> addPlanningEntry(String stringInfo) {
Pattern pattern=Pattern.compile(
"Flight:(.*?),(.*?)\n\\{\nDepartureAirport:(.*?)\nArrivalAirport:(.*?)\nDepatureTime:(.*?)\nArrivalTime:(.*?)\nPlane:(.*?)\n\\{\nType:(.*?)\nSeats:(.*?)\nAge:(.*?)\n\\}\n\\}\n");
Matcher matcher=pattern.matcher(stringInfo);
if(!matcher.find()) {
return null;
}
String planningDatestr=matcher.group(1);
String planningEntryNumber=matcher.group(2);
String departureAirport=matcher.group(3);
String arrivalAirport=matcher.group(4);
String departureTime=matcher.group(5);
String arrivalTime=matcher.group(6);
return this.addPlanningEntry(planningDatestr,planningEntryNumber, departureAirport, arrivalAirport, departureTime, arrivalTime);
}
分配资源也是有差异的方法,与新建计划项的方法类似。新增了几个重载方法,模块化、也方便调用。有两种Pattern,对应两种输入模式。
@Override
public boolean allocatePlanningEntry(String planningEntryNumber, String stringInfo) {
if(this.getPlanningEntryByStrNumber(planningEntryNumber)==null) {
return false;
}
Pattern pattern1=Pattern.compile("Flight:(.*?),(.*?)\n\\{\nDepartureAirport:(.*?)\nArrivalAirport:(.*?)\nDepatureTime:(.*?)\nArrivalTime:(.*?)\nPlane:(.*?)\n\\{\nType:(.*?)\nSeats:(.*?)\nAge:(.*?)\n\\}\n\\}\n");
Pattern pattern2=Pattern.compile("Plane:(.*?)\n\\{\nType:(.*?)\nSeats:(.*?)\nAge:(.*?)\n\\}\n");
Matcher matcher=pattern1.matcher(stringInfo);
if(!matcher.find()) {
matcher=pattern2.matcher(stringInfo);
if(!matcher.find()) {
return false;
}
}
String number=matcher.group(7);
String strType=matcher.group(8);
int intSeats=Integer.valueOf(matcher.group(9));
String age=matcher.group(10);
this.allocateResource(planningEntryNumber,number,strType,intSeats,age);
Plane plane=new Plane(number,strType,intSeats,age);
if(this.collectionResource.contains(plane)){
return true;
}
else return false;
}
这需要调用
public Resource allocateResource(String planningEntryNumber, String number, String strType, int intSeats,
String age) {
Plane plane=new Plane(number,strType,intSeats,age);
//****
FlightSchedule<Resource> flightSchedule=(FlightSchedule<Resource>) this.getPlanningEntryByStrNumber(planningEntryNumber);
this.collectionResource.add(plane);
return flightSchedule.allocateResource(plane)==true?plane:null;
}
最后是按时间顺序排序计划项sortPlanningEntries()。首先定义一个Comparator,并重写其compare方法,然后调用Collections.sort()方法进行排序。在Flight Schedule中的Comparator对象,compare方法通过获取时间进行比较,具体如下:
//定义一个Comparator
Comparator<PlanningEntry<Resource>> comparator=new Comparator<PlanningEntry<Resource>>() {
@Override
/*
* 比较p1计划项起飞时间是否早于p2计划项终点降落时间,早则返回-1,否则返回1
* 返回1时前一个计划项应当排在后一个计划项之后
*/
public int compare(PlanningEntry<Resource> p1,PlanningEntry<Resource> p2) {
return ((FlightSchedule<Resource>) p1).getLeavingTime()
.isBefore(((FlightSchedule<Resource>) p2).getArrivalTime())?-1:1;
}
};
下列应用已全部通过JUnit测试
要检测一组计划项之间是否存在位置冲突,主要应该检测每一个位置的若干计划项是否有时间冲突。首先要保存下每个位置的所有计划项,我使用的是一个Map、键为位置String、值为使用该位置的所有计划项的List。
接下来,遍历所有计划项。对于每个计划项:若该计划项的位置未被加入Map的键集合,则加入并将值赋值为仅有该计划项的List;否则,将该计划项加入原有的值的List中,并考察List中是否有冲突,最后更新该值。
考察List是否有冲突,要遍历任意两个计划项,是否存在这时间重叠。对于任意两个不同计划项c1、c2,分别获取它们的起始时间和结束时间,进行比较:若一方的起始时间早于另一方的结束时间且结束时间晚于起始时间,则认为冲突。
与判断地点冲突类似
提取特定资源的前序计划项,是要搜索使用同一资源、计划时间在选定计划项之前且最晚(与选定计划项时间最近)的计划项。该方法类似与在一组数据中选取符合条件的最大值。整体思路就是遍历、筛选、比较
首先,初始化“最晚时间”和“前序计划项”;
然后,遍历所有计划项,选出使用相同资源的计划项(在迭代时比较资源是否相同来进行筛选):
for (int i = 0; i < entries.size(); i++) {
if (entries.get(i).getResource().equals(e.getResource())) {
……}
在迭代中比较,若符合筛选条件,且比原最晚时间更晚,则更新:
最后返回prePlanningEntry即可。
请分小节介绍每种设计模式在你的ADT和应用设计中的具体应用。
设置3个PlanningEntry接口的工厂方法,分别新建1种计划项子类型。以Flight Schedule为例,需要输入Location、TimeSlot和计划项编号3个参数,返回一个FlightSchedule计划项类型:
public static<Resource> FlightSchedule<Resource> newFlightEntry(LocalDate planningDate,Location location,Timeslot timeslot,String planningEntryNumber){
return new FlightSchedule<Resource>(planningDate,location,timeslot,planningEntryNumber);
}
在Collection中,用一个List存储所有的计划项;
在board中直接继承Iterator父类
如设立抽象的allocate资源
然后再每个子类中根据不同资源特征进行分配
其实三类应用的设计几乎相同,只是需要注意高铁应用中多个资源和经停站的问题
从eclipse中下载WindowBuilder,用JFrame进行app开发
再比如
Apis
其实就是调用board实现功能即可。
我的理解是这个app就是各个应用的集成,之前设计的接口应当在这里有一个很好的耦合表现。
!注:本实验中判断资源冲突时采取的遍历方式非常基础,导致对于内容量比较大的file1、3、4判断时需要花费很长的时间
状态转换时通过按钮调用在planningentry中的函数即可,比如可以run一下
在running状态下allocate则会报错
现在给plan2 allocate相同的resource,可判断resource冲突
检测某一车站情况
这个应用相比前两个主要多出了location冲突问题
考虑实际,在allocated阶段可以换教室
但是running等情况下就不可以换教室了
在这个面板中我还添加了某一位老师的教学经历面板,这个功能同样在board类中实现
读取file5示例文件
对于文件的读取,可以分组进行,每组13行作为一个整体Stringinfo处理
BufferedReader bReader=null;
try {
bReader = new BufferedReader(new FileReader(new File("data/FlightSchedule//FlightSchedule_5.txt")));
} catch (FileNotFoundException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
String line="";
int cntLine=0;
StringBuilder stringInfo=new StringBuilder("");
try {
while((line=bReader.readLine())!=null) {
if(line.equals("")) {
continue;
}
stringInfo.append(line+"\n");
cntLine++;
if(cntLine%INPUT_ROWS_PER_UNIT==0) {
FlightSchedule<Resource> flightSchedule
=(FlightSchedule<Resource>) flights.addPlanningEntry(stringInfo.toString());
if(flightSchedule!=null) {
flights.allocatePlanningEntry(
flightSchedule.getPlanningEntryNumber(), stringInfo.toString());
stringInfo=new StringBuilder("");
}
}
}
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
bReader.close();
lblNewLabel_26.setText("success");
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
只考虑你选定的三个应用的变化即可。
由于本身location就采取字符串列表的形式,所以按照之前思路,三个站类似高铁取下标即可。
获取位置方法则添加
当然为了方便也可以直接输入int获取对应位置
Timeslot同理
底层构架严格意义上只需要多加三个getter即可,改动不大。
主要的改动在于之后的board考虑经停的部分,这一部分同理和高铁的想法类似,将原本分开的起飞和到达融合到一起,原本确定的equals()关系编程contains()表达
由于board并没有设计成可复用的类型,但好在代码几乎和TrainBoard照搬
接着就是按照train的逻辑调整app
(我突然发现之前trainApp里写了两个一模一样的展示板)
直接重写cancel()方法,增加判断语句限制前置状态即可。
代价非常小
课程可以有多个教师一起上课,且需要区分次序(即多名教师的优先级)
这还是按照高铁的想法,利用一个集合来存储多个资源,之于优先级可以给教师加一个属性,也可以建立一个Map,在匹配资源时连接对应的优先级大小,优先级用int表示
此处用前一种方法修改
为了尽量减少修改,各种方法向高铁的资源处理靠拢
由于本实验中代码基层考虑的是课程和航班这样单个资源的耦合,所以讲课程剥离出来改动非常大,需要层层修改代码